WebSockets on Vercel Functions: Real-Time Without a Separate Server
Table of Contents
About the Author

Ahmed Mahmoud
Author & Developer
Software engineer passionate about web development and user experience design.
Founder of Devya · eng-ahmed.com ↗Key takeaways
Vercel Functions support WebSockets natively through experimental_upgradeWebSocket() from @vercel/functions, running on Fluid Compute — no standalone WebSocket server or third-party realtime service required.
Fluid Compute is Vercel's function runtime that reuses a warm instance across concurrent requests instead of spinning up one instance per request; that instance-reuse model is what makes holding a long-lived socket open practical on a platform historically built around short request/response cycles.
Standard Node.js libraries like ws work inside a Vercel Function unchanged — our team didn't rewrite client or server socket logic to move this off a dedicated server.
WebSockets still win over Server-Sent Events for anything bidirectional: chat input, collaborative cursors, live multiplayer state. SSE stays the simpler choice for one-way token or event streaming.
Managed services like Pusher or Ably still earn their place at large fan-out scale or when you need built-in presence and room primitives out of the box; for a single app's realtime feature, self-hosting on Vercel Functions is now a realistic default instead of a workaround.
Can a Vercel Function actually keep a WebSocket connection open?
Yes, as long as it runs on Fluid Compute, which is the default runtime for Vercel Functions. The old serverless mental model — one isolated container per invocation, torn down the moment the response finishes — never had a slot for a connection that stays open for minutes or hours.
Fluid Compute changes the unit of work: instances are reused across concurrent requests, and a function can keep a socket, a stream, or a background timer alive across that reused instance instead of dying the second it returns a response. That's also why the platform supports graceful shutdown and request cancellation on the same runtime — a function that's mid-stream when a deploy rolls out gets a chance to close connections cleanly instead of being killed mid-write.
For a chat feature or a live dashboard we build for a client, that's the difference between a socket that silently drops on every deploy and one that reconnects predictably.
How do we upgrade a Vercel Function to a WebSocket connection?
We call experimental_upgradeWebSocket() from @vercel/functions inside a route handler, the same way you'd upgrade an HTTP connection on any Node.js server. Next.js exposes this through the App Router's route handlers, so the upgrade lives next to normal API code instead of in a separate service.
On the client it's a plain WebSocket object — nothing Vercel-specific to install. If a project already uses ws for connection pooling, rooms, or heartbeat handling, that code carries over unchanged; the upgrade point is the only part that's Vercel-specific.
When should we reach for WebSockets instead of SSE or polling?
The deciding question is direction: does the client need to send frequent messages back, not just receive them? If yes, WebSockets are the right tool. If the client only ever listens, Server-Sent Events are simpler to operate and debug.
Comparison: WebSocket vs SSE vs polling
WebSocket — bidirectional, its own protocol upgrade (ws:// or wss://), reconnection is manual, best for chat, collaborative editing, and multiplayer cursors, needs experimental_upgradeWebSocket() plus Fluid Compute on Vercel.
Server-Sent Events (SSE) — server-to-client only, plain HTTP with text/event-stream, reconnection is built into the browser's EventSource API, best for token streaming, notifications, and live logs, runs on any Node.js runtime with no special API.
Polling — client-initiated repeated requests, plain HTTP, no persistent connection at all, best for low-frequency status checks, needs no special infrastructure.
We'd already covered SSE for AI token streaming in an earlier post — that's still the right call for one-way model output. WebSockets are for the features where the browser talks back: a cursor position, a keystroke, a typing indicator.
Do we still need Pusher, Ably, or a standalone WebSocket server?
Sometimes, but not by default anymore. Managed realtime platforms like Pusher and Ably handle two things that are genuinely hard to build yourself: fan-out across thousands of concurrent connections, and presence or room primitives — who's online, who's in this document — as an out-of-the-box API. If a feature is at that scale, or needs presence semantics today without building them, a managed service is still the pragmatic choice.
What changed is the baseline. Before, any WebSocket on Vercel meant running a separate always-on server — a small Node process elsewhere just to hold the socket, talking back to the Vercel-hosted app over HTTP. Now a single-feature realtime need — a live cursor in a small collaborative tool, a status channel for a background job, a two-way chat widget — can live entirely inside the same Vercel Function that serves the rest of the API. One deploy target, one auth model, one set of environment variables.
What connection limits and reconnection patterns do we need to handle ourselves?
WebSockets don't get automatic reconnection the way EventSource does for SSE — that logic is on us. A few patterns we build into every WebSocket client now.
Exponential backoff on reconnect: on close, retry with increasing delay — 1 second, 2 seconds, 4 seconds, capped around 30 seconds — instead of hammering the endpoint immediately.
Heartbeat pings: send a small ping message on an interval and expect a pong back; if none arrives within a timeout, treat the connection as dead and reconnect rather than waiting for the OS to notice a half-open socket.
Idle timeouts are real: a connection with no traffic for too long can be dropped by intermediate proxies or the platform itself — the heartbeat above doubles as a keep-alive.
State on reconnect, not just the socket: when a client reconnects, it needs to resync whatever state it missed — last message ID, current document version. Design the protocol so the server can answer 'catch me up from X' rather than assuming the client saw every message.
None of this is specific to Vercel — it's the same discipline any WebSocket client needs — but it matters more here because there's no managed service hiding it from you.
FAQ
Q: Do we need the Edge runtime to use WebSockets on Vercel?
A: No. WebSocket support runs on Fluid Compute with the standard Node.js runtime — runtime = 'edge' isn't needed, and Edge Functions are generally not the recommended default on Vercel anymore.
Q: Can we use the ws npm package directly instead of experimental_upgradeWebSocket()?
A: experimental_upgradeWebSocket() is the entry point that performs the HTTP-to-WebSocket upgrade inside a Vercel Function; once the connection is open, existing socket-handling code — including logic built around ws — carries over largely unchanged.
Q: Is this the same thing as Server-Sent Events for AI streaming?
A: No. SSE is one-way, server to client, and remains the right choice for streaming AI tokens. WebSockets are bidirectional and fit features where the client also sends frequent messages back.
Q: Do WebSocket connections survive a new deployment?
A: Fluid Compute supports graceful shutdown and request cancellation, giving in-flight connections a chance to close cleanly during a deploy rather than being hard-killed — client code should still implement reconnect logic for the brief window during rollover.
Q: Do we still need a database or Redis for multi-instance fan-out?
A: If an app can run on a single reused Fluid Compute instance, in-memory broadcast is enough. Once traffic spans multiple instances, a shared layer — Redis pub/sub or a managed realtime service — is still needed to fan messages out across them.
Further Reading
Frontend Engineering
Next.js Middleware in 2026: Auth Guards, A/B Tests, and What Belongs at the Edge
We have debugged the same mistake across multiple projects: database calls in middleware.ts that make every matched request pay a round-trip cost before the route runs. Field notes on the patterns that belong at the edge — auth guards, A/B cookie bucketing, locale rewrites — and what does not.
Frontend Engineering
Shipping Bilingual Next.js Apps in English and Arabic: Routing, RTL, and What Crawlers Actually See
We build almost every product in English and Arabic. Field notes from our team: why cookie-based language switching hides half your site from crawlers, URL-per-locale routing in the App Router, RTL layouts with CSS logical properties, and hreflang done right.