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.
- Introduction
- What Is SSE and Why Use It in Next.js?
- Why SSE Breaks in Next.js Production on Linux — The Real Reasons
- How I Debugged and Fixed SSE in Next.js on Linux — Step by Step
- Root Cause Summary — Why SSE Fails in Next.js on Linux
- Common Mistakes When Deploying SSE in Next.js on Linux
- Best Practices for SSE in Next.js Linux Production
- Final Result — SSE Streaming Working in Next.js on Linux
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-TypeandCache-Controlheaders, 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 responseCache-Control: no-cache, no-transform— Prevents NGINX from caching or transforming the stream bodyConnection: keep-alive— Keeps the HTTP connection open for continuous event deliveryX-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: noheader — NGINX buffers and holds the entire stream - No
force-dynamicexport — 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 -Nfrom 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 -tulnpon 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 offto your entire server block - Run
sudo nginx -tbefore every reload — never apply an unvalidated NGINX config change in production
Final Result — SSE Streaming Working in Next.js on Linux
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.