Managed vs self-hosted WebSocket services #
Your product needs real-time features and the team is split. One side wants a managed service — Pusher, Ably, PubNub, Azure Web PubSub, or a cloud provider’s WebSocket gateway — so nobody has to learn about file-descriptor limits, sticky sessions and reconnect storms. The other side points at the per-message price, the vendor’s SDK in every client, and the day the bill overtakes an engineer’s salary. Both are right about something. A managed service converts the operational work described across this site into a monthly invoice and an integration; self-hosting converts the invoice into engineering time and on-call load. Which is cheaper and safer depends on scale, message patterns, team skills and constraints that are easy to write down in advance.
Root cause #
Running WebSockets well at scale is a real body of work: connection limits and OS tuning, load balancing with draining and rebalancing, cross-node fan-out, presence, delivery guarantees, multi-region failover, observability, and security at the handshake. Managed services have already built all of it, operate it for many customers, and expose it through a pub/sub API: publish to a channel from your backend, subscribe from clients with the vendor’s SDK.
What you give up is control and a linear relationship between usage and cost. Pricing is typically per connection, per message delivered (each fan-out recipient counts), or both, so a broadcast to a 5,000-member channel costs 5,000 messages. Your clients speak the vendor’s protocol, so switching later means changing every client. Data flows through a third party, which matters for regulated industries and data residency. And features you need that the vendor lacks — a particular ordering guarantee, a custom auth flow, server-side filtering — are either impossible or require workarounds.
Resolution #
Decide with a written comparison against your own numbers rather than general opinion. Estimate three quantities from product analytics or a pilot: peak concurrent connections, messages published per month, and average fan-out per message (recipients per publish). Delivered messages — the unit most vendors bill — are published × fan-out. Then compare each option’s monthly cost and the engineering time each implies. A small calculator makes the assumptions explicit and reviewable:
interface Workload {
peakConnections: number;
publishesPerMonth: number;
avgFanout: number; // recipients per published message
}
interface ManagedPricing {
perMillionMessages: number; // $ per million delivered messages
perPeakConnection: number; // $ per peak concurrent connection per month
includedMessages?: number;
}
interface SelfHostedModel {
connectionsPerNode: number; // measured in load tests
nodeMonthly: number; // $ per node incl. LB/pub-sub share
engineerMonthly: number; // fully loaded $ per engineer-month
engineerFraction: number; // share of an engineer to operate it (on-call, upgrades)
minNodes: number; // redundancy floor (e.g. 3 across zones)
}
export function compare(w: Workload, m: ManagedPricing, s: SelfHostedModel) {
const delivered = w.publishesPerMonth * w.avgFanout;
const billable = Math.max(0, delivered - (m.includedMessages ?? 0));
const managed = (billable / 1e6) * m.perMillionMessages + w.peakConnections * m.perPeakConnection;
const nodes = Math.max(s.minNodes, Math.ceil(w.peakConnections / s.connectionsPerNode) * 1.3); // 30% headroom
const selfHosted = nodes * s.nodeMonthly + s.engineerMonthly * s.engineerFraction;
return { deliveredMessages: delivered, managed: Math.round(managed), selfHosted: Math.round(selfHosted), nodes: Math.ceil(nodes) };
}
// Example: a collaboration product with modest rooms.
console.table(compare(
{ peakConnections: 80_000, publishesPerMonth: 400e6, avgFanout: 6 },
{ perMillionMessages: 2.5, perPeakConnection: 0.015 },
{ connectionsPerNode: 40_000, nodeMonthly: 220, engineerMonthly: 18_000, engineerFraction: 0.25, minNodes: 3 },
));
The numbers above are placeholders — use your vendor’s actual price list and your measured per-node capacity from capacity planning for WebSocket fleets. The shape of the answer is what matters: fan-out multiplies managed costs but barely changes self-hosted costs, while the engineering fraction dominates self-hosting at small scale.
Cost is only one axis. Weigh the others explicitly:
Keeping the exit open #
Whichever you choose, protect the ability to change your mind. Wrap the real-time client in a thin interface of your own — subscribe(channel, handler), publish(channel, data), onStatus(cb) — and keep vendor SDK calls behind it, so switching is a change in one module plus a server-side publisher, not a rewrite of every feature. Keep your message envelope and sequence numbers in your own payloads rather than relying on vendor-specific metadata, following designing a WebSocket message envelope. And publish from your backend through a single outbound adapter, which becomes the natural place to dual-publish during a migration.
A common trajectory is hybrid: stay on a managed service for most features, and move one high-volume, high-fan-out stream (market data, game state, telemetry) to a self-hosted fleet once its bill justifies it. The abstraction layer makes that a per-channel decision.
Edge cases #
Hidden fan-out. Presence and typing indicators generate delivered messages quadratic in room size. On usage-based pricing, these ephemeral features can dominate the bill; throttle them as in typing indicators over WebSockets regardless of hosting model.
Connection limits per plan. Managed plans cap concurrent connections; exceeding them rejects new clients. Monitor your peak against the cap and understand the vendor’s behaviour at the limit.
Outage dependency. A managed service’s incident is your incident, and you cannot fix it. Check the vendor’s status history and SLA, and decide whether critical features need a fallback path (for example, polling your own API) during vendor outages.
Verification #
Validate the decision with a pilot rather than a spreadsheet alone. Run one real feature on the chosen option for a few weeks and record connections, delivered messages, latency percentiles, incident count and engineering hours spent. Compare the actual bill (or infrastructure cost plus time) against the calculator’s prediction; if they differ by more than a factor of two, the model’s fan-out or capacity assumptions were wrong and the decision deserves another look.
Operational checklist #
FAQ #
Is it cheaper to use Pusher or Ably than to run my own WebSocket servers? #
At small and moderate scale, usually yes once engineering time is counted. At large scale — especially with high fan-out — usage-based pricing often exceeds the cost of running your own fleet. Compute both with your own connection and delivery numbers.
What is the biggest risk of a managed WebSocket service? #
Lock-in: clients speak the vendor’s protocol and SDK, so leaving requires changing every client. An internal abstraction layer and your own message envelope keep that risk small.
Can I mix managed and self-hosted? #
Yes, and many teams do: most channels on a managed service, the highest-volume streams on self-hosted infrastructure, behind one client interface.
Where do API Gateway and Durable Objects fit? #
They are serverless infrastructure rather than full managed real-time products: you still write the fan-out, presence and delivery logic, but you do not run servers. See AWS API Gateway WebSocket APIs and Cloudflare Durable Objects WebSocket hibernation.
Related #
- Capacity Planning for WebSocket Fleets — the self-hosted side of the cost model.
- Socket.IO vs Raw WebSockets — the library decision if you self-host.
- AWS API Gateway WebSocket APIs — serverless infrastructure on AWS.
- Scaling WebSocket Broadcast with Redis Pub/Sub — what you build when self-hosting fan-out.
Back to Serverless & Managed WebSockets.