Why SSE Fails in Next.js Production (And How to Fix It)

Introduction

Everything worked perfectly in local development. The stream was flowing, events were firing, and the real-time updates looked great on screen.

Then it hit production.

No stream. No events. Just a hanging request that eventually timed out — or worse, silently failed with zero error output.

Server-Sent Events (SSE) in Next.js production is one of the most confusing debugging experiences in modern web development. The code is correct. The API route works locally. But the moment it's deployed to a Linux server behind NGINX, SSE breaks in ways that aren't immediately obvious.

This is a complete breakdown of why it happens and exactly how to fix it.

Next.js Server-Sent Events stream failing in production showing /api/user/stream error and debugging in browser DevTools

What Is SSE and Why Use It in Next.js?

Before jumping into fixes, a quick framing. Server-Sent Events (SSE) is a browser-native protocol that lets a server push a continuous stream of data to the client over a single long-lived HTTP connection. Unlike WebSockets, SSE is one-directional — server to client only — which makes it perfect for:

  • Live notifications and real-time dashboards
  • Progress updates for long-running background jobs
  • Live log streaming in developer tools

In Next.js, SSE is typically implemented inside an API Route (Pages Router) or a Route Handler (App Router) using a ReadableStream or TransformStream. It works flawlessly in local development because nothing gets in the way of the raw HTTP stream.

Production on a Linux server is a completely different story.


Why SSE Breaks in Next.js Production on Linux — The Real Reasons

There's no single cause. SSE failure in production is almost always caused by one or more layers in your Linux deployment stack interrupting the stream before it reaches the browser. The most common culprits are:

  • NGINX proxy buffering — NGINX buffers the upstream response and waits for it to complete before forwarding it to the client, which destroys streaming entirely
  • Missing response headers — Without the correct Content-Type and Cache-Control headers, NGINX treats SSE like a normal buffered HTTP response
  • Next.js App Router runtime mismatch — App Router Route Handlers behave differently depending on whether they run on Edge Runtime or Node.js Runtime
  • NGINX connection timeout — Default NGINX timeout settings terminate long-lived SSE connections after a short idle period
  • Incorrect ReadableStream implementation — Subtle API differences between Node.js runtimes break streaming in production even when the code looks correct locally

How I Debugged and Fixed SSE in Next.js on Linux — Step by Step

Step 1: Confirm SSE Works Locally vs Breaks in Production

First, establish exactly where the failure is. SSH into your Linux server and run this curl command directly to bypass the browser entirely:

curl -N https://yourdomain.com/api/user/stream

The -N flag disables output buffering in curl, so you can see if events are actually arriving from the server. If curl receives events but the browser doesn't — the problem is in your response headers. If curl also hangs with no output — the problem is NGINX blocking the stream before it ever leaves the server.


Step 2: Set the Correct SSE Response Headers in Next.js

This is the most commonly missed fix in Linux deployments. Without the right headers, NGINX will treat your SSE response as a regular buffered HTTP response and hold it until the connection closes — which means the client receives nothing in real time.

Here is the correct Route Handler implementation for SSE in Next.js App Router on Linux:

// app/api/stream/route.ts

export const runtime = 'nodejs'; // Important — see Step 4

export async function GET() {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    start(controller) {
      const send = (data: string) => {
        controller.enqueue(encoder.encode(`data: ${data}\n\n`));
      };

      send(JSON.stringify({ message: 'Stream started' }));

      const interval = setInterval(() => {
        send(JSON.stringify({ time: new Date().toISOString() }));
      }, 1000);

      return () => clearInterval(interval);
    },
  });

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache, no-transform',
      'Connection': 'keep-alive',
      'X-Accel-Buffering': 'no',
    },
  });
}

Every one of these headers matters in a Linux NGINX deployment:

  • Content-Type: text/event-stream — Tells NGINX and the browser this is an SSE stream, not a regular response
  • Cache-Control: no-cache, no-transform — Prevents NGINX from caching or transforming the stream body
  • Connection: keep-alive — Keeps the HTTP connection open for continuous event delivery
  • X-Accel-Buffering: no — Disables NGINX response buffering at the application level without touching your NGINX server config

Step 3: Fix NGINX Proxy Buffering for SSE on Linux

If you're self-hosting Next.js behind NGINX on Ubuntu or any Linux server, proxy buffering is almost certainly the root cause of your broken SSE stream. NGINX's default behavior is to buffer the entire upstream response before sending it to the client — the exact opposite of what SSE requires.

Add these directives to your NGINX location block specifically for the SSE endpoint:

location /api/user/stream {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;

    # Disable buffering — critical for SSE on Linux
    proxy_buffering off;
    proxy_cache off;
    proxy_set_header Connection '';
    chunked_transfer_encoding on;

    # Extend timeouts for long-lived SSE connections
    proxy_read_timeout 86400s;
    proxy_send_timeout 86400s;
    keepalive_timeout 86400s;

    # Forward headers correctly
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

After editing the NGINX config, always validate syntax and reload gracefully:

sudo nginx -t
sudo systemctl reload nginx

Important: Apply proxy_buffering off only inside the specific SSE location block — not globally across your entire NGINX configuration. Disabling buffering globally can degrade performance for all other routes.


Step 4: Set the Correct Runtime in Next.js App Router

Next.js App Router Route Handlers can run on Edge Runtime or Node.js Runtime. Edge Runtime has strict limitations on long-lived connections and streaming behavior — it is designed for short-lived responses, not persistent SSE streams running on a Linux server.

Explicitly declare Node.js runtime at the top of your Route Handler file:

export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';

force-dynamic is equally critical. Without it, Next.js may statically optimize your route during the build and serve a cached response — which completely breaks SSE for every request after the first one.


Step 5: Check NGINX Error Logs for SSE Connection Failures

NGINX error logs are the fastest diagnostic tool when SSE is silently failing on Linux. Always check them before and after applying fixes:

tail -f /var/log/nginx/error.log

Common error messages that confirm NGINX is the problem:

# Buffering timeout — stream held too long
upstream timed out (110: Connection timed out) while reading response header

# Connection closed before stream completed
upstream prematurely closed connection while reading response header

After applying proxy_buffering off and the extended timeout values, these errors disappear and SSE events flow through to the client in real time.


Step 6: Handle Client Reconnection Properly

Even after fixing NGINX and the server configuration, SSE connections will occasionally drop — servers restart, networks blip, Linux processes get recycled. The browser's native EventSource API handles reconnection automatically, but send the correct SSE event ID so the client can resume cleanly:

controller.enqueue(encoder.encode(`id: ${eventId}\n`));
controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`));
const eventSource = new EventSource('/api/user/stream');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log(data);
};

eventSource.onerror = () => {
  // EventSource reconnects automatically
  console.log('Connection lost — retrying...');
};

Root Cause Summary — Why SSE Fails in Next.js on Linux

  • Missing X-Accel-Buffering: no header — NGINX buffers and holds the entire stream
  • No force-dynamic export — Next.js caches the SSE route statically on build
  • Wrong runtime (Edge instead of Node.js) — Long-lived streams fail silently
  • NGINX proxy_buffering enabled — Events never reach the client in real time
  • Default NGINX timeout too short — SSE connection killed after idle period

Common Mistakes When Deploying SSE in Next.js on Linux

  • Testing only in local development — Local has no NGINX, no proxy, no timeout constraints; always test SSE against a staging server that mirrors your Linux production setup
  • Using Pages Router API Routes for streaming — Pages Router has limited streaming support; App Router Route Handlers with Node.js runtime are significantly more reliable on Linux
  • Applying proxy_buffering off globally — This degrades performance across all NGINX routes; always scope it to the SSE location block only
  • Not handling stream cleanup — Failing to clear intervals or release resources when the client disconnects causes memory leaks in the Node.js process on your Linux server

Best Practices for SSE in Next.js Linux Production

  • Always test with curl -N from the server itself — it confirms whether NGINX or the application is the failure point before you touch any code
  • Add a heartbeat event every 15–30 seconds — keeps the connection alive through NGINX idle timeout thresholds
  • Monitor active SSE connections using ss -tulnp on Linux — connections that never close signal a stream cleanup bug in your application
  • Scope NGINX buffering rules to the SSE path only — never apply proxy_buffering off to your entire server block
  • Run sudo nginx -t before every reload — never apply an unvalidated NGINX config change in production

Final Result — SSE Streaming Working in Next.js on Linux

Server-Sent Events stream working correctly in Next.js production after fixing Nginx buffering and API configuration issue

After applying all the fixes above — correct SSE headers, NGINX proxy buffering disabled, extended connection timeouts, and Node.js runtime declared — SSE streams reliably in Next.js production on Linux with zero silent failures.


If your Server-Sent Events work locally but break in Next.js production on Linux, the problem is almost never your application code. It is your NGINX configuration — proxy buffering enabled, missing headers, or timeout values too short for a long-lived stream. Fix the infrastructure layer first, and your SSE stream will work exactly as it did in local development.

One NGINX directive. That is often all that stands between a completely broken stream and a working real-time feature in production.

f X W