Skip to content

[docs?] nginx buffers query.live responses; needs X-Accel-Buffering: no on the stream #15790

@ollema

Description

@ollema

Describe the bug

Not sure if this should be classified as a bug or just docs improvement, sorry for that!

query.live returns application/x-ndjson, an open response whose body is written incrementally as the server-side async generator yields. SvelteKit sets Cache-Control: private, no-store on the response, which Cloudflare and some other proxies respect for both caching and buffering decisions.

nginx, however, ignores Cache-Control when deciding whether to buffer proxy_pass responses (docs). With its default proxy_buffering on;, nginx holds the entire upstream response until either the body finishes or proxy_read_timeout expires (default 60s). For an open ndjson stream, neither happens cleanly: nginx times out, closes the upstream, and flushes its (typically empty) buffer. The client sees a 200 response complete after ~60s with net::ERR_HTTP2_PROTOCOL_ERROR.

This means any nginx-fronted Node deploy of a SvelteKit app using query.live is broken without the framework or the runtime surfacing an error: live.connected just sits at false and the request hangs in the network panel.

The standard upstream opt-out is the X-Accel-Buffering: no response header. nginx honors it and strips it from the client-facing response. Setting it from the framework on streaming remote-function responses would fix this entire class of deploys without requiring user changes, and is a no-op on proxies that don't recognize the header.

I think it would be nice if this was fixed (by adding the header automatically) or at least maybe better documented!

Reproduction

Minimal repro repo with a live demo: https://github.com/ollema/query-live-repro

Live demo: https://query-live-repro.server.ollema.xyz

Two routes share the same watchCounter (query.live) and bump (command) remote functions and the same in-memory counter:

  • /broken: no proxy hint, hangs forever behind nginx.
  • /fixed: X-Accel-Buffering: no set in hooks.server.ts for this route, works as expected.

Steps:

  1. Open /broken with devtools open.
  2. Observe live.connected === false from first render.
  3. In the network panel, the /_app/remote/<hash>/watchCounter request sits pending for ~60s, then "completes" with 200 OK and is immediately logged as net::ERR_HTTP2_PROTOCOL_ERROR 200 (OK).
  4. Response headers:
Status:         200 OK
cache-control:  private, no-store
content-type:   application/x-ndjson
server:         nginx
  1. Open /fixed. Same code, same remote functions, but live.connected === true immediately and bump ticks the counter live. Two /fixed tabs stay in sync.

pnpm dev works in both cases. The bug only appears once nginx (or any other default-buffering reverse proxy) is in front of the Node server. The included Dockerfile and GitHub Actions workflow deploy via CapRover, but any nginx-fronted Node setup reproduces identically.

Logs

System Info

System:
    OS: macOS 26.4.1
    CPU: (10) arm64 Apple M1 Pro
    Memory: 111.22 MB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 25.9.0 - /opt/homebrew/bin/node
    npm: 11.12.1 - /opt/homebrew/bin/npm
    pnpm: 10.33.2 - /opt/homebrew/bin/pnpm
  Browsers:
    Chrome: 146.0.7680.178
    Firefox: 125.0.1
    Safari: 26.4
  npmPackages:
    @sveltejs/adapter-node: ^5.5.4 => 5.5.4 
    @sveltejs/kit: ^2.59.0 => 2.59.0 
    @sveltejs/vite-plugin-svelte: ^7.0.0 => 7.0.0 
    svelte: ^5.55.5 => 5.55.5 
    vite: ^8.0.10 => 8.0.10

Severity

serious, but I can work around it

Additional Information

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions