Hey, I updated this to be mindful of Vercel’s limitations:
Below is a friendly, comprehensive guide to Server-Sent Events (SSE) in Next.js 15 and Node.js 22—including how you can still make it work on Vercel without the guide yelling at you. We’ll cover recent SSE changes, potential pitfalls, IDE/tooling considerations, and how to stream logs from a serverless environment while respecting Vercel’s constraints.
Table of Contents
- Overview
- Key Changes in Node.js 22
- Enhancements in Next.js 15
- Potential Gotchas
- Best Practices for SSE in Next.js 15 & Node.js 22
- Example: SSE in Next.js 15 and Node.js 22
- Serving SSE on Vercel
- IDE Considerations & Tooling
- Final Recommendations
- Appendix: Reference Articles & Documentation
How SSE Has Changed in Next.js 15 and Node.js 22
In Next.js 15 and Node.js 22, modern runtime improvements and API updates have made SSE more convenient while introducing a few new caveats. Below is a deeper look at what’s changed, especially around file operations, streaming logs, and IDE configurations.
1. Key Changes in Node.js 22
1.1 Native Fetch API in Node.js
- Direct fetch in Node
Node.js 22 includes (experimental) support for the WHATWG fetch
API, simplifying server logic for data requests.
- If your SSE route needs to fetch from external services (e.g., logging or metrics), you can now do so natively.
- Note that SSE itself is about writing to a response stream, whereas
fetch
is typically used for reading from other endpoints.
1.2 Improved Streams API
- Web Streams vs. Node.js Streams
Node.js 22 offers better support for the Web Streams API, but many Node libraries still expect the classic stream
module.
- If your SSE code or underlying libraries (like those accessing S3) require a Node.js
Readable
, you may see errors like stream.on is not a function
if you pass them a Web ReadableStream
.
- Fix: Convert Web Streams to Node.js streams using
Readable.fromWeb()
or use a Node.js Readable
from the start.
1.3 Better Edge Runtime Compatibility
- Edge vs. Node
Node.js 22 aligns well with edge environments, but long-lived connections (like SSE) still can be terminated by strict edge timeouts.
- If you want stable SSE, ensure you’re on the Node.js runtime, not the Edge runtime. In Next.js, set
export const runtime = 'nodejs';
in your route.
2. Enhancements in Next.js 15
2.1 App Router with Server Actions
- New
app/
directory
Next.js 15’s App Router organizes server logic under app/api/
. SSE routes live in app/api/sse/route.ts
, for instance.
- Server Actions
These are great for form submissions and data mutations. SSE remains a custom API pattern.
2.2 React 18+ Streaming Features
- Server Components & Suspense
Concurrent React can progressively stream HTML to the client, which is different from SSE. If you need push-based updates, SSE remains the go-to pattern.
- File-based API Routes
You can place SSE logic in a dedicated route.ts
under app/api/
, which is simpler to maintain than older Next.js versions.
2.3 Edge Runtime Support
- Potential Timeouts
If you try to run SSE from the Edge runtime, you may face disconnections. Explicitly opt into runtime = 'nodejs'
for more stable SSE connections.
3. Potential Gotchas
-
Web Streams vs. Node Streams
- Passing a Web
ReadableStream
to a library expecting Node.js streams triggers errors.
- Fix: Use
Readable.fromWeb()
or revert to Node streams.
-
Long-Lived Connections on Edge
- Edge environments typically can’t support indefinite SSE. If you see abrupt connections closing, switch to the Node.js runtime.
-
IDE/TypeScript Config
- If using Web Streams or
EventSource
, add "DOM"
in lib
within your tsconfig.json
.
- If dealing with Node streams, add
"types": ["node"]
.
-
HTTP/2
- Some SSE failures occur under HTTP/2, so ensure HTTP/1.1 is used if you see random disruptions.
-
CORS
- If the browser and SSE endpoint are on different domains, configure
Access-Control-Allow-Origin
headers.
-
File-Based Logging
- When streaming logs from your server’s S3 or disk operations, confirm your libraries are Node-stream-compatible (if you’re converting to/from Web streams).
4. Best Practices for SSE in Next.js 15 / Node.js 22
-
Use app/api/
for SSE
Keep SSE routes separate from UI, making your IDE aware of the difference between server and client code.
-
Convert Streams Where Needed
If your library wants a Node.js stream, but you have a Web ReadableStream
, use Readable.fromWeb()
.
-
Leverage req.signal
for Cleanup
Next.js 15 sets req.signal
. If the client disconnects, you can close your SSE gracefully.
-
Explicitly Use runtime = 'nodejs'
arduino
Copy code
export const runtime = 'nodejs';
This keeps your SSE route off Edge, where it might get terminated prematurely.
-
Be Mindful of Timer Usage
Use intervals or pings in SSE, but clear them when the request ends to prevent memory leaks.
-
Test with Real Browsers
SSE can behave differently across Chrome, Firefox, etc. Full end-to-end testing is recommended.
-
Type Annotations for SSE Data
If you’re sending JSON logs, define an interface so your IDE can auto-complete your SSE event object properties.
5. Example: SSE in Next.js 15 and Node.js 22
Server Route (app/api/sse/route.ts
):
ts
Copy code
`export const runtime = ‘nodejs’; // Force Node.js runtime
export async function GET(req: Request) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
// Initial event
controller.enqueue(
encoder.encode(data: ${JSON.stringify({ type: 'info', message: 'Connection established' })}\n\n
)
);
const intervalId = setInterval(() => {
const now = new Date();
const payload = {
type: 'update',
timestamp: now.toISOString(),
message: `Current time: ${now.toLocaleTimeString()}`,
};
controller.enqueue(encoder.encode(`data: ${JSON.stringify(payload)}\n\n`));
}, 1000);
// If the client disconnects
req.signal.addEventListener('abort', () => {
clearInterval(intervalId);
controller.close();
});
},
});
// Return streaming response
return new Response(stream, {
headers: {
‘Content-Type’: ‘text/event-stream’,
‘Cache-Control’: ‘no-cache’,
‘Connection’: ‘keep-alive’
}
});
}`
Client (React Component):
tsx
Copy code
`import { useEffect } from ‘react’;
export default function SSELogs() {
useEffect(() => {
const eventSource = new EventSource(‘/api/sse’);
eventSource.onmessage = (e) => {
const data = JSON.parse(e.data);
console.log('[SSE] Received:', data);
};
eventSource.onerror = (err) => {
console.error('[SSE] Error:', err);
};
return () => {
eventSource.close();
};
}, );
return
Check the console for SSE updates
;
}`
6. Serving SSE on Vercel (Without Getting Yelled At!)
Yes, you can use SSE on Vercel, but note:
-
Serverless Function Timeouts
- Hobby/Free plan: ~10s
- Pro plan: ~30s
If your SSE route runs longer, Vercel terminates it.
-
Short-Burst SSE with Auto-Reconnect
- Stream logs for the duration of your function’s time limit, then send an event like
data: {"type": "end"}
.
- The client sees the end event, closes, and reconnects. This approximates real-time logs in short intervals.
-
Polling / Hybrid
- Alternatively, poll every few seconds if SSE reconnect logic feels too cumbersome.
-
Dedicated Real-Time Services
- If you need indefinite streaming (e.g., a continuous S3→ImageKit log pipeline), a dedicated Node server or specialized real-time platform is best.
Example: SSE with a 25-second limit that “resets”:
js
Copy code
`// app/api/logs/route.js
export const runtime = ‘nodejs’;
export async function GET(req) {
const encoder = new TextEncoder();
const start = Date.now();
const maxDuration = 25000; // 25s
let timer;
const stream = new ReadableStream({
start(controller) {
timer = setInterval(() => {
const now = Date.now();
if (now - start >= maxDuration) {
controller.enqueue(encoder.encode(‘data: {“type”: “end”}\n\n’));
clearInterval(timer);
controller.close();
return;
}
const logLine = { message: 'Sync log: ’ + new Date().toISOString() };
controller.enqueue(encoder.encode(data: ${JSON.stringify(logLine)}\n\n
));
}, 1000);
req.signal.addEventListener('abort', () => {
clearInterval(timer);
controller.close();
});
},
cancel() {
clearInterval(timer);
}
});
return new Response(stream, {
headers: {
‘Content-Type’: ‘text/event-stream’,
‘Cache-Control’: ‘no-cache’,
‘Connection’: ‘keep-alive’
}
});
}`
Then the client reconnects whenever it receives type: "end"
.
7. IDE Considerations & Tooling
-
TypeScript Configuration
"lib": ["DOM"]
if you use ReadableStream
or EventSource
.
"types": ["node"]
if you deal with Node.js streams or older Node APIs.
-
ESLint & Prettier
- Ensure your config knows about Next.js 15’s
app/api/
routes.
-
Auto-Completion
- If you see warnings about
ReadableStream
, verify you have the correct "lib"
entries in your tsconfig.json
.
- For SSE, the DOM
EventSource
type is included if "DOM"
is in lib
.
-
Server vs. Browser
- Don’t mix
window
or DOM APIs in your server route. Keep them separate so your IDE doesn’t complain.
8. Final Recommendations
- Short-Burst SSE on Vercel is fine for sync logs—just handle timeouts gracefully.
- For continuous streaming that lasts longer than your plan’s limit, consider a dedicated solution.
- Always confirm you’re on
runtime = 'nodejs'
to avoid Edge timeouts.
- Convert streams if you have a mismatch between Web Streams and Node.js streams.
- Test thoroughly in real browsers to confirm SSE behaves as expected.
Appendix: Reference Articles & Documentation
-
MDN: Server-Sent Events
https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events
Baseline SSE specification, explaining EventSource
and data formats.
-
MDN: Streams API
https://developer.mozilla.org/en-US/docs/Web/API/Streams_API
Overview of the Web Streams standard, including ReadableStream
, WritableStream
, etc.
-
Node.js 22 Docs: Web Streams
https://nodejs.org/docs/latest-v22.x/api/webstreams.html
Details on Node’s native Web Streams support, including conversions with Readable.fromWeb()
and toWeb()
.
-
Node.js 22 Docs: Globals (fetch
)
https://nodejs.org/docs/latest-v22.x/api/globals.html#fetch
Explains the built-in fetch
, removing the need for external HTTP libraries.
-
Next.js Documentation: App Router & API Routes
https://nextjs.org/docs/app/building-your-application/routing/router-handlers
Shows how to write streaming responses in app/api/
.
-
Next.js Docs: runtime = 'nodejs'
https://nextjs.org/docs/app/api-reference/next-config-js#runtime
Force a Node.js environment instead of Edge, critical for SSE.
-
SSE vs. WebSockets vs. Polling
https://ably.com/topic/server-sent-events
Great overview of real-time communication approaches.
-
GitHub Discussions: SSE with Next.js
https://github.com/vercel/next.js/discussions/12562
Community threads covering various SSE implementations and troubleshooting.
-
AWS S3 / GCP Storage Docs
-
TypeScript Configuration Handbook
https://www.typescriptlang.org/docs/handbook/tsconfig-json.html
Ensures your project recognizes the correct types for SSE, Node, or DOM APIs.
Wrapping Up
This guide is designed to keep SSE running smoothly in Next.js 15 and Node.js 22—including Vercel use cases—without scolding you for wanting logs in real time. Remember:
- Short-burst SSE can work on serverless platforms.
- Reconnect logic is easy to implement on the client side.
- If you need unlimited streaming, consider a dedicated environment.