Standalone output internals, static export feature graveyard (what silently breaks), WebSocket authentication (HttpOnly cookie on upgrade handshake, token validation on reconnect, secret rotation with 40K live connections), Pusher/Ably vs Partykit vs self-hosted socket layer, and the cold start optimisation playbook.
A-14 — Self-Hosting vs Serverless, WebSockets, and Long-Lived Connections
Who this is for: Architects making infrastructure decisions for Next.js applications that have outgrown the "just deploy to Vercel" answer — teams self-hosting on Kubernetes, applications that need WebSockets or Server-Sent Events, and anyone who needs to understand the real constraints that serverless imposes on connection-oriented features.
The Fundamental Serverless Constraint
Serverless (Vercel Functions, AWS Lambda, Cloudflare Workers) is the dominant deployment model for Next.js because it matches the request-per-invocation model well. A request comes in, the function runs, the response goes out. For stateless HTTP, this is ideal.
The constraint: serverless functions don't persist between requests. Each invocation may run in a new container. There's no in-process state, no long-lived connections, no background threads.
This constraint rules out a specific category of features:
- WebSocket servers (require persistent TCP connections)
- Server-Sent Events that outlive a response (require persistent HTTP connections)
- In-memory job queues (state lost on restart)
- Background processing beyond the request duration
- In-process caches shared across requests (each instance has its own memory)
These are not framework limitations — they're infrastructure physics. The question is which of these your application actually needs, and what the alternatives are.
WebSockets in a Next.js Application
Next.js doesn't have built-in WebSocket support. The Route Handler model (request → response) doesn't map to WebSockets (persistent bidirectional connection). This is not a missing feature — it's an architectural mismatch.
The three architectural patterns for WebSockets alongside Next.js:
Pattern 1: Separate WebSocket server
The most common production pattern. A dedicated Node.js service (Socket.io, ws, uWebSockets.js) handles WebSocket connections. The Next.js application communicates with it via HTTP or a message broker.
Pattern 2: Partykit / Cloudflare Durable Objects
Cloudflare Durable Objects are stateful edge workers — they persist state and accept WebSocket connections. Partykit is a higher-level abstraction built on Durable Objects.
This runs entirely at the edge — WebSocket connections go to the nearest Cloudflare datacenter, not a central origin server. For real-time applications with global users, this is a compelling architecture.
Pattern 3: Serverless WebSockets (Pusher, Ably, Soketi)
Managed WebSocket infrastructure as a service. Your Next.js application publishes events to the service via HTTP. The service maintains WebSocket connections to clients and delivers events.
The trade-off: operational simplicity (no WebSocket server to manage) vs. cost (per-message pricing at scale) and latency (extra hop through the managed service).
Server-Sent Events
Server-Sent Events (SSE) are a lighter-weight alternative to WebSockets for one-directional server-to-client streaming. They use regular HTTP, work through most proxies, and automatically reconnect.
In Next.js Route Handlers, SSE works on self-hosted deployments but not on Vercel Functions — Vercel Functions have a maximum response duration, and SSE requires a persistent connection:
On Vercel, SSE responses are limited to Vercel's streaming response duration limit. For indefinite streams, you need self-hosted or a managed SSE service.
Self-Hosting on Kubernetes
For teams that need persistent connections, long-lived processes, or full infrastructure control, Kubernetes is the production target.
The canonical self-hosted architecture:
The key difference from serverless: pods persist between requests. In-process state (Prisma connection pools, warm Module singletons) survives across requests. This is why the Prisma globalThis singleton pattern (P-4) matters — on Kubernetes, the singleton is reused across thousands of requests per pod. On serverless, the singleton is created anew each cold start.
Kubernetes deployment manifest (simplified):
The livenessProbe and readinessProbe point to the health check Route Handler from P-14. Kubernetes uses these to determine which pods should receive traffic and whether to restart unhealthy pods.
ISR on Self-Hosted Kubernetes
ISR on Kubernetes has the distributed cache invalidation problem from A-3 — each pod has its own filesystem cache. revalidatePath('/products') on Pod A doesn't invalidate Pod B.
The solution: Redis-backed cache handler (also from A-3), or disable ISR's filesystem cache entirely and use use cache with a remote cache instead.
With a Redis cache handler, all pods share the same cache. Revalidation from any pod propagates to all pods via Redis.
Choosing the Right Deployment Model
| Requirement | Serverless (Vercel) | Self-hosted (K8s) |
|---|---|---|
| Zero infrastructure ops | ✅ | ❌ |
| WebSockets | ❌ (use separate service) | ✅ |
| Long-running background jobs | ❌ | ✅ |
| Per-request isolation | ✅ | ✅ (separate pods) |
| Global edge distribution | ✅ (built-in) | ✅ (needs CDN setup) |
| ISR with multiple instances | ✅ (Vercel handles it) | Requires Redis handler |
| Predictable cost at high volume | ❌ (per-request pricing) | ✅ (fixed pod cost) |
| Cold starts | Present (mitigatable) | None |
The practical decision: start with Vercel. Migrate specific services (WebSockets, background processing) to dedicated always-on infrastructure when you hit the serverless constraints. Only migrate the entire Next.js application to Kubernetes if you have specific reasons — usually cost at very high volume or regulatory requirements around where compute runs.
Where We Go From Here
A-15 — the final module — covers production observability: the tracing, metrics, logging, and alerting architecture that lets you understand what your application is doing in production, diagnose incidents quickly, and catch regressions before users report them.
WebSocket Authentication — The Part Nobody Documents
The deployment topology for WebSockets in production (covered earlier in this module) is clear. The authentication story is not — and it fails in ways that are hard to debug because they're browser-specific and often silent.
The HttpOnly Cookie Problem on Upgrade
Your application authenticates via HttpOnly cookies. The browser sends cookies on every HTTP request automatically. When the browser opens a WebSocket connection, it sends an HTTP GET with an Upgrade: websocket header. Cookies are included in this upgrade request — so far so good.
The problem: the HTTP 101 Switching Protocols response establishes the WebSocket connection. After that, there's no more HTTP. The WebSocket protocol has no built-in mechanism to re-send cookies on subsequent messages. The authentication happens once, at connection time.
What this means operationally:
-
If the session cookie expires while the socket is open, the server has no way to know until the client makes a new HTTP request. The WebSocket connection stays open with an expired session.
-
If the user logs out (session cookie is deleted), the WebSocket connection stays open. The client keeps receiving real-time updates for a user who has logged out.
-
Some environments (certain mobile browsers, proxy servers, load balancers) strip cookies from the WebSocket upgrade request. The connection goes through but arrives unauthenticated.
Authentication Pattern: Token in Initial Message
The most reliable approach: don't rely on the upgrade request for authentication. Complete the HTTP handshake, then require the client to send an auth message as the first message over the socket.
The client sends a short-lived access token (not the HttpOnly session cookie, which isn't accessible to JavaScript). Generate this token specifically for WebSocket authentication:
Token Validation on Reconnect
WebSocket connections drop. Networks change. Mobile devices sleep. Your client reconnects every time. Each reconnect goes through the same auth flow: fetch a new short-lived token, send it as the first message.
The short-lived token model (60s expiry) ensures that reconnections always use a fresh token — no stale token can be reused from a previous connection session.
JWT Secret Rotation with Live Connections
This is the scenario that causes the most pain: you need to rotate your JWT signing secret. On HTTP, it's straightforward — new tokens use the new secret, old tokens with the old secret expire naturally within the token TTL. No active user is affected.
On WebSockets, it's different. You have 40,000 active connections. Each was authenticated with a token signed with the old secret. If you delete the old secret, you cannot validate those tokens. All 40,000 connections become effectively unauthenticated on their next message.
The graceful rotation protocol:
Step 1: Add the new secret while keeping the old one. Token validation accepts either.
Step 2: Issue a system-wide re-authentication request over the WebSocket itself.
Step 3: Clients receive reauth_required, fetch a new short-lived token (signed with the new secret), and send it as an auth message. The connection continues uninterrupted for clients that re-authenticate.
Step 4: After the deadline, close connections that haven't re-authenticated.
Step 5: Remove the old secret from JWT_SECRETS. Deploy.
This protocol handles secret rotation with zero forced disconnections for clients that are online during the rotation window. Clients that are offline (mobile with bad network) reconnect normally after the rotation — they fetch a new token signed with the new secret.
Per-Connection Rate Limiting
One thing the WebSocket authentication story often misses: rate limiting at the connection and message level.
Without per-user connection limits, a single user (or a compromised account) can open thousands of connections and exhaust your server's file descriptor limit.
Why are serverless functions (like Vercel Functions or AWS Lambda) generally unsuitable for hosting WebSocket servers?
What is the primary issue with relying on the HTTP Upgrade request for authenticating WebSocket connections?
In a self-hosted Kubernetes deployment of Next.js, how does the behavior of the Prisma globalThis singleton pattern differ from a serverless deployment?
Test your knowledge with more question sets
Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.
Sign in & RegisterDiscussion
0Join the discussion