Serverless & Managed WebSockets #

Most of this site is about running WebSocket servers well: raising descriptor limits, balancing and draining long-lived connections, fanning out across nodes, surviving reconnect storms. All of that is real work, and for many teams it is not the work they want to be doing. A growing set of platforms takes it off your hands. Serverless gateways such as AWS API Gateway hold connections and invoke your functions per event. Edge compute such as Cloudflare Durable Objects gives each room its own single-threaded object that holds its sockets and sleeps when idle. Managed real-time services such as Pusher, Ably, PubNub and Azure Web PubSub provide channels, presence and delivery guarantees behind an SDK. Each removes a different slice of the operational burden and replaces it with a different set of limits and a different bill.

This area explains those models, their constraints, and how to decide between them and a self-hosted fleet. It complements the self-hosted scaling material in Scaling Real-Time Infrastructure, and the protocol-level concerns in Real-Time Protocol Selection & Architecture apply unchanged.

Who owns what, by hosting model Four hosting models from most to least ownership: a self-hosted fleet where you own everything, a serverless gateway where you own the registry and fan-out, edge objects where you own per-room logic, and a managed real-time service where you mostly just publish. Who owns what, by hosting model Self-hosted fleet you own sockets, fan-out, presence, scaling, OS tuning and on-call all yours Serverless gateway (API Gateway) platform owns sockets; you own registry, fan-out, presence shared Edge objects (Durable Objects) platform owns sockets and placement; you own per-room logic shared Managed real-time service vendor owns sockets, fan-out, presence, history; you publish mostly theirs Moving down the stack trades control and unit cost for less operational work
Each model draws the line between your code and the platform in a different place.

Prerequisites #

Whatever the hosting model, some foundations remain your responsibility:

  • An application protocol with message types and sequence numbers, so clients can resume after the disconnects every platform imposes — see WebSocket Message Protocol Design.
  • Client reconnection with jittered backoff and resume, because platforms close connections for their own reasons: lifetime caps, edge maintenance, idle limits. Auto-Reconnection Strategies covers it.
  • Authentication that works with the platform’s handshake hook — often only the initial request carries credentials, and browsers cannot set headers, so tickets or cookies are typical.
  • Measured workload numbers: peak concurrent connections, messages published, and average fan-out. Every cost comparison depends on them.

Core implementation: a platform-neutral client #

The single most valuable piece of code in this area is not platform code at all. It is a thin client interface your application uses everywhere, with one adapter per hosting model behind it. It keeps the choice of platform reversible and lets you run two platforms side by side during a migration.

// The interface features depend on — never a vendor SDK directly.
export interface RealtimeClient {
subscribe(channel: string, onMessage: (data: unknown, seq?: number) => void): () => void;
publish(channel: string, data: unknown): Promise<void>;
onStatus(cb: (s: 'connecting' | 'live' | 'offline') => void): () => void;
close(): void;
}

// Adapter for a raw WebSocket endpoint (self-hosted, API Gateway or a Durable Object).
export function websocketAdapter(url: string, getTicket: () => Promise<string>): RealtimeClient {
const handlers = new Map<string, Set<(d: unknown, seq?: number) => void>>();
const statusCbs = new Set<(s: 'connecting' | 'live' | 'offline') => void>();
const lastSeq = new Map<string, number>();
let ws: WebSocket | null = null;
let attempt = 0;

const setStatus = (s: 'connecting' | 'live' | 'offline') => statusCbs.forEach((cb) => cb(s));

async function open() {
setStatus('connecting');
ws = new WebSocket(`${url}?ticket=${encodeURIComponent(await getTicket())}`);
ws.onopen = () => {
attempt = 0;
setStatus('live');
for (const ch of handlers.keys()) ws!.send(JSON.stringify({ action: 'subscribe', channel: ch, afterSeq: lastSeq.get(ch) ?? 0 }));
};
ws.onmessage = (e) => {
const { channel, data, seq } = JSON.parse(e.data);
if (typeof seq === 'number') {
if (seq <= (lastSeq.get(channel) ?? 0)) return; // duplicate after resume
lastSeq.set(channel, seq);
}
handlers.get(channel)?.forEach((h) => h(data, seq));
};
ws.onclose = () => { // platform caps, maintenance, etc.
setStatus('offline');
const cap = Math.min(30_000, 500 * 2 ** attempt++);
setTimeout(open, Math.random() * cap);
};
}
void open();

return {
subscribe(channel, onMessage) {
if (!handlers.has(channel)) {
handlers.set(channel, new Set());
if (ws?.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ action: 'subscribe', channel, afterSeq: 0 }));
}
handlers.get(channel)!.add(onMessage);
return () => handlers.get(channel)?.delete(onMessage);
},
async publish(channel, data) { ws?.send(JSON.stringify({ action: 'publish', channel, data })); },
onStatus(cb) { statusCbs.add(cb); return () => statusCbs.delete(cb); },
close() { ws?.close(1000); },
};
}
// A managedServiceAdapter(...) would implement the same interface with the vendor's SDK.

With features written against RealtimeClient, choosing API Gateway, a Durable Object, a managed vendor or a self-hosted fleet is a decision about one adapter and one server-side publisher. The platform-specific guides below fill in each side.

Comparing the models #

The models differ on four practical axes: connection lifetime (who closes connections, and when), fan-out mechanics (who loops over recipients, and what it costs per recipient), state (where room membership and history live), and cost shape (per server, per connection-minute, per message, or per duration).

Hosting models side by side Self-hosted fleets have no connection cap and fan out in memory; API Gateway caps connections at two hours with a ten-minute idle limit and needs an API call per recipient with state in DynamoDB; Durable Objects fan out in object memory with state in object storage; managed services handle fan-out and state and bill per delivered message. Hosting models side by side Self-hosted API Gateway Durable Objects Managed service Connection cap none 2 h, 10 min idle edge restarts vendor policy Fan-out in memory API call per recipient in object memory vendor Room state Redis / memory DynamoDB object storage vendor Cost driver nodes + people conn-min + msgs active duration msgs delivered Fan-out mechanics usually decide cost; connection caps usually decide client design
Four models, four answers to where sockets, fan-out and state live.

API Gateway WebSocket APIs suit event-driven backends already on Lambda, with modest room sizes and spiky traffic. Their constraints — the two-hour cap, the ten-minute idle limit, one management API call per recipient — are manageable with the patterns in AWS API Gateway WebSocket APIs and a registry designed as in storing WebSocket connection state in DynamoDB.

Durable Objects suit room-shaped workloads — chat rooms, documents, game lobbies — where one coordination point per room simplifies consistency, and where many rooms are idle most of the time, which the Hibernation API makes nearly free. Their limit is per-object throughput. See Cloudflare Durable Objects WebSocket hibernation.

Managed real-time services suit teams that want features — presence, history, delivery guarantees, SDKs for every platform — immediately, and whose fan-out volume keeps usage pricing reasonable. Their cost curve and lock-in are the trade-offs examined in managed vs self-hosted WebSocket services.

Self-hosting wins when fan-out volume is large, when you need semantics no platform offers, or when compliance requires full control — and when you have, or want, the operational skills this site describes.

Illustrative relative cost per delivered message For small chat rooms all four models cost about the same per delivered message; for five-hundred-member dashboards API Gateway and managed services cost two to three times more; for a fifty-thousand-member live event API Gateway costs about nine times more, managed six times and Durable Objects four times. Illustrative relative cost per delivered message normalized to self-hosted at each workload; excludes engineering time self-hosted API Gateway Durable Objects managed 2.5× 7.5× 10× Chat, rooms of 10 Dashboards, rooms of 500 Live event, room of 50k
Per-delivery economics diverge with room size — large fan-out favours in-memory delivery.

Security and tenancy on shared platforms #

Handing connections to a platform does not hand it your security model. Three responsibilities stay with you on every hosting option.

Authentication happens once, at the edge of the platform. API Gateway gives you the $connect request; a Durable Object sees whatever the routing Worker forwards; a managed service accepts whatever token your backend signs. After that moment, messages usually carry no credentials, so the identity established at connect time must be bound to the connection — in a registry row, a socket attachment, or the vendor’s client identity claims — and checked on every action. Short-lived tickets work well across all models, as described in authenticating WebSockets with short-lived tickets.

Authorization per channel is your code. A managed service’s channel is only as private as the token that grants access to it. Sign tokens that list exactly the channels a user may subscribe to and publish on, with short lifetimes, and never let clients choose arbitrary channel names that map to other tenants’ data. The rules in per-channel authorization for WebSocket subscriptions apply unchanged; only the enforcement point moves into token issuance.

Revocation needs a path to live connections. When a user is removed from a workspace, their open connection must lose access immediately, not at token expiry. On API Gateway, delete their registry rows and close the connection through the management API; on Durable Objects, close their sockets in the room object; on managed services, use the vendor’s API to revoke or disconnect the client.

Observability when you don’t own the servers #

Platforms expose fewer internals than your own servers, so decide early what you will measure from the outside. Instrument the client adapter to report connection lifetimes, close codes, reconnect counts, resume outcomes and end-to-end delivery latency (publish timestamp to render). Instrument the server-side publisher to record what it sent and when. Combine them with the platform’s own metrics — connection counts, message counts, throttling and error counters — in one dashboard. The ratio alerts in alerting on WebSocket connection drops work on client-reported data just as well as on server metrics, and they are often the only way to see platform-caused disconnects at all.

Configuration reference #

Concern API Gateway Durable Objects Managed service
Authentication $connect authorizer or ticket Worker before routing to object Vendor token endpoint you sign
Heartbeat app ping < 10 min auto-response pair SDK built in
Lifetime cap handling reconnect before 2 h with jitter reconnect on close SDK reconnects
Room membership DynamoDB, room-keyed + GSIs ctx.getWebSockets() vendor channels
Message size 32 KB frames / 128 KB messages platform limit plan limit
Resume your seq + replay store object storage history vendor history / rewind
Scaling unit account quotas one object per room (shard hot rooms) plan limits

Edge cases & gotchas #

Platform disconnects are routine. Every model closes connections for its own reasons. Clients that treat any close as an error, or reconnect without jitter, create storms. Build resume in from the start, and show users a subtle reconnecting state rather than an error banner, since most of these closes last well under a second.

Presence and typing multiply costs. Ephemeral signals generate deliveries quadratic in room size. On per-message pricing they can dominate the bill; throttle and aggregate them as in typing indicators over WebSockets.

Cold paths. Functions and objects that wake on demand add latency to the first message after idle periods. For latency-critical routes, measure p99 including cold starts, and consider provisioned concurrency or a keep-warm schedule only for the routes where users actually wait on the reply.

Local development and testing. Serverless and managed platforms are harder to run locally than a Node process. Keep the platform adapter thin and test feature logic against the in-memory implementation of the client interface; test the adapters themselves against the real platform in a dedicated staging environment, including its lifetime caps and idle limits, which local emulators often do not reproduce.

Hidden limits. Account-level quotas (API Gateway management API rates, connection limits per plan) are easy to miss until launch day. Load-test to at least twice your expected peak on the actual platform, with the tools in Load Testing & Capacity Planning.

Verification #

Evaluate a platform with a representative pilot: one real feature, real clients, and the metrics that matter — time to first message, delivery latency percentiles, disconnects per connection-hour by cause, resume success rate, and cost per thousand delivered messages. Compare against the same metrics from your current system or a self-hosted baseline.

// Per-platform pilot metrics, reported by the client adapter.
interface PilotMetrics {
platform: 'self-hosted' | 'api-gateway' | 'durable-objects' | 'managed';
ttfmMs: number; // time to first message after connect
deliveryP50Ms: number;
deliveryP99Ms: number; // include cold starts
disconnectsPerConnHour: number;
resumeSuccessRate: number; // resumed from seq vs needed a snapshot
}

A platform is a good fit when its p99 latency and disconnect rate meet your product’s needs, resume succeeds for nearly all disconnects, and the projected monthly cost at expected scale — computed from measured fan-out — is acceptable.

Guides in this area #

FAQ #

Can I run WebSockets on serverless platforms like Lambda? #

Not by holding sockets in a function — functions are short-lived. Use a gateway that holds connections and invokes functions per event (API Gateway WebSocket APIs), or an edge platform with stateful objects (Durable Objects).

Which option is cheapest? #

It depends on fan-out. For small rooms and spiky traffic, serverless and managed options are often cheapest once engineering time is counted. For large rooms or heavy broadcast volume, per-delivery pricing adds up and self-hosted in-memory fan-out usually wins.

Do managed services guarantee message delivery? #

Many offer message history, rewind or at-least-once delivery on reconnect, with specifics varying by vendor and plan. Check the exact guarantees and keep your own sequence numbers so you can verify them.

Can I combine models? #

Yes. A common pattern keeps most features on a managed service or serverless gateway and moves the highest-volume stream to self-hosted servers, all behind one client interface.

How do I migrate between platforms without downtime? #

Run both behind the client interface, publish every event to both from a single server-side adapter, and move clients over in cohorts using a feature flag in the adapter factory. Because each adapter resumes from the same sequence numbers, a client switching platforms mid-session resumes cleanly on the new one. Retire the old platform once its connection count reaches zero for a sustained period.

What about data residency on managed platforms? #

Serverless gateways and edge objects run in regions or locations you can usually constrain; managed services vary by vendor and plan. If messages contain regulated data, confirm where messages are processed and stored — including history and logs — before committing, or keep regulated streams on infrastructure you control.

Back to Scaling Real-Time Infrastructure.