SSE Responses Arrive All at Once Behind a CDN? Troubleshooting Buffering and Timeouts
Create Time:2026-10-08 12:23:17
浏览量
1051

Your AI application generates a response a piece at a time, but the browser stays silent for several seconds and then displays everything at once. Another stream delivers messages promptly, only to disconnect after a quiet period. These symptoms need different investigations: delayed batches call for finding the layer that holds data, while idle disconnects call for checking connection timeouts.

This guide covers HTTP endpoints returning text/event-stream. WebSocket uses a different connection and upgrade model, so the configuration below is not a complete WebSocket setup. The commands and application code are diagnostic examples, not measurements from your production environment.

SSE data accumulating in an edge gateway buffer compared with events delivered individually in real time

Check that the application sends complete events

SSE uses UTF-8 text, with a blank line marking the end of an event. If the server writes a data: line without the terminating blank line, the client continues waiting for the event to finish. [1]

data: First response segment

In application code, that is typically data: First response segment\n\n. A browser's native EventSource parser dispatches messages at event boundaries. If your client uses fetch() with a custom stream parser, check whether it reads the entire response before updating the page. HTTP chunks and SSE events do not necessarily have a one-to-one relationship.

The following minimal Node.js server removes model generation, database queries, and framework behavior from the test. It writes one event per second, sends five events, and then ends the response. flushHeaders() sends the response headers early; delivery to the browser still depends on the proxies and network downstream. [2]

const http = require('node:http');

http.createServer((req, res) => {
  if (req.url !== '/events-test') {
    res.writeHead(404).end();
    return;
  }
  res.writeHead(200, {
    'Content-Type': 'text/event-stream; charset=utf-8',
    'Cache-Control': 'no-store',
    'X-Accel-Buffering': 'no'
  });
  res.flushHeaders();
  let n = 0;
  const timer = setInterval(() => {
    res.write(`data: ${JSON.stringify({ n: ++n, at: Date.now() })}\n\n`);
    if (n === 5) {
      clearInterval(timer);
      res.end();
    }
  }, 1000);
  res.on('close', () => clearInterval(timer));
}).listen(8080, '127.0.0.1');

The server listens on loopback only, for testing through an origin reverse proxy. It has no authentication or business-level limits, so do not expose it directly to the internet. Native EventSource may reconnect when the response ends. Use curl for this test, or explicitly close the browser client.

Compare the same endpoint at the origin and the CDN

curl can buffer its own output. Add --no-buffer when checking when each event arrives, so terminal buffering does not look like a CDN problem. [3]

curl --no-buffer --max-time 15 \
  -D sse-edge-headers.txt \
  https://api.example.com/events-test

curl --no-buffer --max-time 15 \
  --resolve api.example.com:443:203.0.113.10 \
  -D sse-origin-headers.txt \
  https://api.example.com/events-test

The first command follows normal DNS resolution through the CDN. The second pins the same hostname to a test origin. Replace the hostname and 203.0.113.10, which is a documentation address. --resolve keeps the hostname in the URL, allowing the test to retain the same Host and TLS server name. The origin must present a matching certificate and allow connections from the test machine. [3]

If the origin accepts only CDN traffic, use an approved test environment with an equivalent entry point. Do not open the entire origin firewall or add -k to bypass certificate verification. Saved response headers may contain sensitive information; redact them before sharing.

Observe when the client actually receives output, rather than relying only on timestamps inside the messages:

ObservationWhere to investigate next
Direct origin requests also arrive in batchesApplication writes, event terminators, framework behavior, and origin proxy buffering
Origin requests stream normally, but CDN requests arrive in batchesCDN streaming support, response buffering, compression, and edge processing rules
curl receives events promptly, but the page updates all at onceFrontend parsing and rendering, plus differences in the browser's request path
Events arrive normally until an idle disconnectRead timeouts, heartbeats, and maximum connection duration

This comparison narrows the investigation; it does not eliminate every variable. Multiple origins, regional routing, or different request headers may send the tests to different application instances. Record those differences too.

Disable Nginx response buffering only on streaming routes

Nginx's proxy_buffering controls buffering of proxied responses. When it is disabled, Nginx passes data to the client as it receives it from upstream. An upstream response can also influence this behavior with X-Accel-Buffering: no, unless the administrator disables that mechanism with proxy_ignore_headers. [4]

If /events/ is a dedicated streaming path, make a scoped change to its existing proxy configuration:

location /events/ {
    proxy_pass http://127.0.0.1:8080;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;

    proxy_buffering off;
    proxy_cache off;
    gzip off;
    proxy_read_timeout 75s;
}

Retain authentication, rate limits, logging, other required forwarded headers, and access restrictions. Do not replace a production location block wholesale with this example. Because proxy_pass has no URI component here, it preserves the request path. The minimal application above listens at /events-test; it needs a corresponding test route and cannot directly validate a business endpoint under /events/.

The 75s timeout is an example, not a universal recommendation. Set it for your workload and proxy chain. Nginx's proxy_read_timeout limits the interval between successive reads from upstream, rather than the total duration of a response. A stream lasting several minutes and a stream sending nothing for more than 75 seconds are different cases. [4]

proxy_request_buffering controls the incoming client request body. Changing it generally does not fix buffering of an outgoing SSE response. Test compression separately: temporarily disable gzip on the affected path, then check whether the edge still compresses or transforms the response. Avoid disabling optimizations across the entire site. Back up the configuration, run nginx -t, and reload only after validation through your normal change process.

Check the CDN configuration separately

An origin's X-Accel-Buffering: no header can affect proxies that support it. It does not establish that the CDN will disable buffering. Check the current CDN product's streaming behavior and the rules actually matching this route:

  • Bypass caching for dynamic event streams, especially responses containing user-specific data.

  • Avoid page rewriting or edge code that waits for the complete response body.

  • Check compression, response buffering, and origin connection settings independently.

  • Identify both idle timeouts and any maximum connection duration.

Returning Cache-Control: no-store from the origin is a recommended part of the setup, but also inspect CDN rules that explicitly override cache behavior. A cache-status header alone does not demonstrate that the entire path supports incremental delivery.

Change one variable at a time. Record the request path, response headers, actual client arrival times, and the rule version. Disabling caching site-wide can affect unrelated traffic while obscuring which rule caused the delay.

Use heartbeats for idle periods and event IDs for recovery

SSE allows comment lines starting with a colon. If the application may have long gaps between business events, it can periodically send a comment heartbeat such as : ping\n\n. The specification's authoring notes suggest sending a comment roughly every 15 seconds to work around some older proxies. Choose an interval below the shortest idle timeout in your path and validate its effect under load. [1]

Heartbeats must reach the next layer promptly. If they are buffered, sending them more frequently may still fail to keep the client-facing connection alive. They also do not remove a hard maximum connection duration.

Native EventSource supports reconnection and passing a recovery position through an event's id: field and the subsequent request's Last-Event-ID header. [1] The application must retain replayable records, define a replay window, and handle duplicates. Browser reconnection does not automatically recover missing business events. A custom fetch() client needs its own reconnect and recovery logic.

For user-specific streams, review authentication, cross-origin policy, and resource cleanup after disconnection. Do not disable authentication or enable shared caching merely to make messages appear.

Validate incremental delivery and recovery

Keep a baseline from before the change, then compare the same endpoint with the same client afterward. The minimal program's five messages should arrive separately over time. For a business stream, also test long idle periods, normal completion, client-initiated closure, and recovery after a network interruption.

Run tests through the origin proxy and through the CDN. Check time to the first event, intervals between subsequent events, and whether reconnecting produces duplicates or loses messages. Confirm that the targeted rules apply, then check that ordinary pages and download endpoints remain unaffected. If a change does not help, roll back to the saved configuration and continue tracing the previous layer instead of accumulating unexplained buffering and timeout settings.

References

[1] WHATWG HTML Living Standard: Server-sent events — event framing, comments, reconnection, and Last-Event-ID.

[2] Node.js HTTP documentation: response.flushHeaders() and response.write().

[3] curl man page: --no-buffer, --resolve, and --max-time.

[4] Nginx ngx_http_proxy_module: proxy_buffering, proxy_read_timeout, and proxy_request_buffering.

Source documents reviewed for the Chinese edition on October 8, 2026. This English edition retains the same technical scope and examples. No tests were performed on the user's production environment.