n4nAI

API integration

How-to

Debugging SSE connections with browser dev tools

Learn how to debug SSE connections dev tools in the browser: inspect live streams, replay events, and fix broken Server-Sent Events integrations step by step.

n4n Team5 min read1,049 words

Audio narration

Coming soon — every post will get a voice note here.

Server-Sent Events look trivial until they silently drop mid-stream or buffer for ten seconds. To debug SSE connections dev tools in Chrome, Firefox, or Safari give you raw visibility into the frames, but the interface hides the important bits behind a few non-obvious tabs. This guide walks through a concrete workflow to catch stuck connections, malformed events, and CORS issues without guessing.

Step 1: Capture the connection in the Network panel

Open DevTools (F12), go to the Network tab, and trigger your stream. In Chrome, click the filter bar and select EventStream; if that filter is missing, type is:eventstream in the filter box. Firefox labels the request type as text/event-stream in the Type column. Safari requires Develop → Show Web Inspector → Network, and you may need to enable the Develop menu first under Preferences → Advanced.

Check the Preserve log checkbox. SSE connections often outlive page navigations, and a full reload will wipe the request from the list if you don’t. A healthy SSE request stays in Pending state and never reaches 100% finished. If the request completes immediately, the server is not holding the connection open. That single observation eliminates half the bugs when you debug SSE connections dev tools for the first time.

Step 2: Read the raw frames in the Response tab

Click the request, then open the Response tab. Chrome shows incremental bytes as they arrive; click View source to see exact \n\n separators. A correct stream looks like this:

: comment line (ignored)
retry: 3000
id: 1
data: {"choices":[{"delta":{"content":"Hello"}}]}

id: 2
data: {"choices":[{"delta":{"content":" world"}}]}

event: done
data: [DONE]

The SSE spec defines four field names: data, event, id, and retry. A line starting with a colon is a comment. Each event ends with a blank line. Common failure: the server sends data: without a trailing blank line, or uses \r\n\r\n inconsistently. The browser parser is strict—one missing newline and the event never fires. When you debug SSE connections dev tools, copy the raw text and paste it into a local file to diff against the spec.

Also watch for a UTF-8 BOM at the start of the stream. Some Python frameworks prepend it; the browser will treat the first data: line as corrupted and drop the opening event.

Step 3: Use the Messages tab to see parsed events

Chrome adds a Messages subtab for text/event-stream responses (Chrome 93+). It lists each dispatched event with its event type, data payload, and id. If your onmessage handler fires but addEventListener('done') does not, the Messages tab will show whether the server actually sent event: done or just data: [DONE].

Firefox lacks a dedicated Messages view; use the Response tab and count data: lines. Either way, this step confirms the browser parsed the stream the way your code expects. You can right-click the request and save as HAR to replay the exact byte sequence offline.

Step 4: Reproduce the stream with curl to isolate the browser

DevTools can’t tell you if the problem is CORS, cookies, or the network layer. Run the same request from a terminal:

curl -N -H "Authorization: Bearer $KEY" \
  -H "Accept: text/event-stream" \
  --http1.1 \
  https://api.example.com/v1/chat/completions \
  -d '{"model":"gpt-4o","stream":true,"messages":[{"role":"user","content":"hi"}]}'

The -N flag disables curl’s buffering; --http1.1 forces HTTP/1.1 so you can rule out HTTP/2 multiplexing stalls. If curl streams cleanly but the browser shows nothing, inspect the Headers tab for Access-Control-Allow-Origin. EventSource ignores fetch CORS mode nuances; a missing CORS header kills the connection silently. When you debug SSE connections dev tools next time, always keep this curl window open as a reference.

Step 5: Instrument your client code

EventSource only supports GET. Most LLM gateways need POST, so you likely use fetch with a ReadableStream. Log every chunk and track connection state:

const ctrl = new AbortController();
const res = await fetch('/v1/chat/completions', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ model: 'gpt-4o', stream: true, messages: [{ role: 'user', content: 'hi' }] }),
  signal: ctrl.signal
});
const reader = res.body.getReader();
const dec = new TextDecoder();
while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  console.log('CHUNK:', dec.decode(value));
}

If you must use GET, a minimal EventSource wrapper helps:

<script>
const es = new EventSource('/stream');
es.onopen = () => console.log('open', es.readyState);
es.onmessage = (e) => console.log('msg', e.data);
es.addEventListener('done', (e) => console.log('done', e.data));
es.onerror = (e) => console.error('error', e);
</script>

Watch es.readyState: 0 = connecting, 1 = open, 2 = closed. A flip to 2 without a done event means the server hung up early. When you debug SSE connections dev tools with custom fetch clients, the Network panel still captures frames, but you lose the Messages tab parsing—your console logs are the only structured view.

Step 6: Check required response headers

Select the request, open HeadersResponse Headers. A spec-compliant SSE server sends at least:

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

If Content-Type is application/json or missing, Chrome might parse it if the filter is forced, but Firefox will not. A Transfer-Encoding: chunked is also expected for HTTP/1.1. Reverse proxies like Nginx sometimes buffer; add X-Accel-Buffering: no on the server side and verify it appears here. Cloudflare and similar CDNs may buffer by default—look for a CF-Cache-Status header that says DYNAMIC rather than HIT.

Step 7: Throttle and break the network on purpose

Open the Network Conditions tab (Chrome: gear icon in Network panel) and set throttling to “Slow 3G”. A stream that works on localhost may stall when latency spikes. You should see gaps between Messages tab entries. If your client throws onerror and immediately retries, you’ll get duplicate connections—look for multiple pending requests with the same URL.

Use the Request Blocking feature to simulate an outage. The onerror handler should fire with readyState === 2. This is where you confirm your reconnection logic (or the gateway’s fallback) works. If you’re hitting an inference gateway such as n4n.ai, its automatic fallback will reroute to a healthy provider and the stream resumes with a new model field in the usage event—watch for that in the Messages tab.

Step 8: Verify incremental rendering in the DOM

SSE is useless if the UI batches updates. Add a quick assertion:

let last = 0;
es.onmessage = (e) => {
  const now = performance.now();
  console.log(`delta ${Math.round(now - last)}ms`, e.data.slice(0, 20));
  last = now;
};

Open the Console and confirm timestamps spread across seconds, not one burst at the end. If they burst, the server buffered despite correct headers—check for gzip middleware that ignores streaming, or a WSGI server that doesn’t flush.

Step 9: Confirm success criteria

You have a working stream when all of the following hold:

  1. Network panel shows the request as Pending until the server sends a terminal event.
  2. Messages tab lists each data: frame as a separate row with correct event type.
  3. curl -N and browser show byte-identical payloads (ignoring headers).
  4. No CORS errors in Console.
  5. readyState stays 1 until the done event, then transitions to 2.
  6. Throttling to Slow 3G still delivers frames incrementally, not in one chunk.

If those pass, you’ve finished the loop to debug SSE connections dev tools end to end. Anything else is application logic.

Step 10: A minimal verification snippet

Drop this in your page to self-check during manual QA:

function checkStream(url) {
  return new Promise((resolve, reject) => {
    const es = new EventSource(url);
    let n = 0;
    es.onmessage = () => { n++; };
    es.addEventListener('done', () => {
      es.close();
      resolve(n > 0);
    });
    es.onerror = () => {
      es.close();
      reject(new Error('stream errored'));
    };
    setTimeout(() => { es.close(); reject(new Error('timeout')); }, 10000);
  });
}

Call checkStream('/stream') from the console. A resolved promise with true means frames arrived and the stream closed cleanly. That’s the definitive proof your SSE integration is healthy, and the exact checkpoint to return to whenever you debug SSE connections dev tools after a backend change.

Tagsssedebuggingbrowserstreaming

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →