Context & Pain Points
Live events produce the hardest traffic shape for a connection-oriented system: almost no subscribers for hours, then a flood the moment a session opens. A fleet of stateful WebSocket servers sized for the peak sits idle most of the week, and one sized for the average falls over at the start of every event. Connection state made it harder. Every socket needs a record of who owns it and what it subscribed to, that record has to be readable by whichever compute instance handles the next message, and it has to disappear promptly when the client drops. A sub-10ms routing budget left no room to fetch that from a relational database on the hot path.
What We Had To Solve
- Absorbing connection bursts without cold starts showing up as user-visible latency. A live event trigger opens a large share of its connections within seconds, which is exactly when a heavy runtime pays the most for initialization.
- Keeping client state consistent across regions. A subscriber connected in one region has to receive a message published in another, which means the connection registry cannot be a single-region write that everything else waits on. Regional failover has the same problem: a client that reconnects somewhere else has to be recognised without replaying its whole session.
- Holding down propagation delay between publisher and subscriber. Each hop added between ingestion and delivery spends part of a budget measured in milliseconds, so anything not on the delivery path had to move off it.
- Removing idle cost from a workload that is quiet most of the time. Anything provisioned by the hour would have been paid for during the long gaps between events.
How We Built It
- Stored WebSocket connection state in DynamoDB, keyed for single-item lookups on the routing path and given a TTL so disconnected sessions expire without a sweeper process.
- Used ElastiCache Redis as the pub/sub broker between publishers and subscribers, so fan-out happens in memory and the database is never in the middle of a broadcast.
- Kept the Lambda handlers narrow, with cold-path work such as persistence and analytics moved off the delivery route, so a burst scales out on functions that are cheap to start. Handlers do one job each, which also keeps their memory footprint and package size small enough that scale-out is fast rather than staged.
- Exposed the read and subscription surface through AWS AppSync and GraphQL, so clients ask for the fields they need over one managed subscription instead of a custom protocol per feature.
Outcomes That Mattered
Massive Session Scaling
Sustained over 500,000 active, simultaneous WebSocket connections under live conditions, with API Gateway holding the sockets rather than an application fleet.
Ultra-Low Latency
Achieved sub-15ms message distribution from ingestion to end-user display by keeping fan-out in Redis and lookups on a single DynamoDB key.
Zero Infrastructure Idle Costs
Nothing is provisioned by the hour, so the quiet stretches between events cost nothing to keep ready.
Outcome
The platform runs as a serverless WebSocket architecture on Amazon API Gateway, AWS Lambda, DynamoDB and ElastiCache Redis, and scaled past 500,000 concurrent client sessions. API Gateway terminates the sockets and holds them open, so no application server carries the connections. DynamoDB stores the connection registry with a TTL that reaps dead entries without a cleanup job, Redis handles pub/sub fan-out between publishers and subscribers, and Lambda functions stay small enough to spin up inside the latency budget. AppSync sits alongside it for GraphQL reads and subscriptions, so a client asks for the fields it wants rather than parsing a bespoke message envelope.
