How far does Thunderclap scale? What our load tests measured
Measurements taken between 17 October and 1 November 2026.
"Scales to millions" is easy to write on a landing page. Before you put a product on a realtime service, you want to know what was actually measured, how, and where the limits are. This article is that: the load tests we ran on Thunderclap, the numbers they produced, and how to turn them into an estimate for your own workload.
Everything below was run against a separate deployment of the production engine, with real thunderclapJS clients and @thunderclapjs/server publishing, from machines we rented for the purpose in ten regions. Where a number is an extrapolation rather than a measurement, we say so.
The 30-second model
To read the numbers, you need one picture of how Thunderclap moves a message:
- A shard holds a few thousand WebSockets in the edge location nearest to those users. It sleeps when they're quiet.
- A channel knows which shards have subscribers, never the individual sockets. It numbers each event and sends it once to each of those shards.
- Each shard then writes the event to its local subscribers.
So there are two capacities that matter: how many deliveries a shard can write per second, and how fast new shards come into play when users arrive. Everything else follows from those.
What we measured
1. A shard writes about 9,500 deliveries a second
We pointed every socket of a test app at a single shard, subscribed them all to one channel, and broadcast once a second for a minute at growing sizes.
| Sockets on one shard | Delivered | Time to reach every socket (median / p99) |
|---|---|---|
| 1,000 | 60,000 of 60,000 | 250 ms / 962 ms |
| 2,000 | 119,165 of 119,182 | 357 ms / 677 ms |
| 5,000 | 299,914 of 299,916 | 539 ms / 1.1 s |
| 9,000 | 537,720 of 537,720 | 959 ms / 2.9 s |
| 17,000 | falls behind | — |
Time to reach every socket on one shard
One broadcast a second for a minute, to every socket on a single shard.
Median99th percentile
Measured 26 October 2026 from a machine in Frankfurt. Nothing was lost up to ~9,000 sockets.
Time to reach everyone grows linearly, by about 0.1 ms per socket: a shard writes about 9,500 deliveries a second. Push it to 17,000 a second and it can't keep up: a backlog builds and latency climbs into seconds.
The interesting part is what doesn't load a shard: idle sockets. The same 17,000 sockets that broke under one broadcast a second took a single broadcast, after a quiet spell, in 2.4 s with nothing lost. A shard's limit is traffic, not connections. That's why Thunderclap places sockets by how much traffic each shard is carrying, and keeps every shard comfortably below that ceiling.
Waking a sleeping shard is cheap too: after hibernation, the first delivery arrived 0.2–1.4 s after publishing, against 0.2–0.8 s when awake.
2. Delivery is ordered, complete, and fast
Two shapes of traffic, each run for 10–20 minutes, with the publisher and the subscribers on the same machine so latency is measured on one clock, from the start of your backend's publish request to the event arriving in the client.
| One subscriber per channel (notifications) | 30-person rooms (chat) | |
|---|---|---|
| Load | 800 channels, 500 publishes a second, 20 min | 3,000 sockets in 100 rooms, 100 publishes a second, 10 min |
| Messages | 580,011 | ~60,000 per run (~1.7M deliveries) |
| Gaps, duplicates | 0, 0 | 0, 0 |
| Latency median / p99 | 73 ms / 871 ms | 55 ms / 367 ms |
| Sockets dropped during the run | — | 0 |
The rooms column comes from two runs of the same shape, before and after we changed how sockets are packed into shards. Delivery counts are from the first, latency and drops from the second, which is how the engine runs today.
The median includes the HTTP request from the backend to Thunderclap, a hop through the channel and one through the shard. For chat, notifications and live dashboards that's comfortably below what a person perceives as instant.
We also checked billing while we were at it: the usage Thunderclap metered was within 0.01–0.23% of what the API had accepted. You're billed for what you sent.
3. 50,000 clients connecting at once, from ten regions
Connect storms are where realtime systems usually hurt: a deploy of your own app, a push notification, a match kicking off. To test one honestly you need many source IPs (edge networks, sensibly, limit how many WebSockets a single address can open), so we started 20 machines across Frankfurt, Amsterdam, London, Stockholm, Virginia, Chicago, San Jose, São Paulo, Tokyo and Sydney, and had them open 50,000 thunderclapJS clients in 10 seconds, starting in the same second, against a cold app.
| Result | |
|---|---|
| Clients connected | 50,000 of 50,000 |
| Edge connect time, by location | median 7–28 ms (São Paulo 127 ms) |
| Client's first connect, machines that weren't CPU-bound | median 32–181 ms in every region |
| Capacity added during the storm | within 2 seconds of it starting |
50,000 clients connecting from ten regions
Clients connected, second by second, as 20 machines opened 2,500 clients each over 10 seconds.
Measured 1 November 2026 with thunderclapJS, against an app with no connections before the storm.
Two properties made this easy. First, every edge location takes its own share: the storm arrived as ten storms of about 500 connects a second, each absorbed locally. Second, shards don't need provisioning: a shard exists the moment it's first addressed, so the engine widens the set of shards a location uses within seconds of seeing the rate climb, and narrows it again afterwards.
In the same run, each machine also broadcast once a second for five minutes. Every machine received 99.9% of the deliveries it expected. The rest fell while clients were reconnecting, which is exactly what gap events are for.
4. Idle connections are free
We held 500 connections open for an hour, sending keepalives every 10 seconds, twelve times more often than in production. About 80,000 keepalives were sent and all answered. The engine handled none of them: they're answered at the edge while the shard sleeps, and the only work the shard did in that hour was 9 requests, for sockets that closed.
A tab left open overnight, a dashboard on a wall, a mobile app in the background: none of it costs you anything until a message actually moves.
5. Deploys don't drop connections, and resets recover in seconds
We redeployed the engine in the middle of two storm runs, once with every shard asleep and once with shards busy delivering broadcasts. No socket was dropped, and deliveries continued at the same rate through the deploy.
Deliveries per second through a deploy
50,000 connected clients receiving 20 broadcasts a second, while a new version of the engine was deployed.
Measured 1 November 2026, averaged over 2-second windows: the load machines sometimes counted a batch a second late.
What does happen, on any edge network, is the platform restarting servers underneath you. In one run a shard holding 2,033 sockets was reset by the platform in a single moment; the clients were back within a median of about 5 seconds, most of them on the same shard. In another run, a reset that dropped 450 sockets mid-broadcast was recovered in a median of about 1 second, with the missed events reported as gaps. thunderclapJS reconnects with backoff and random jitter from the first attempt, so a burst of drops doesn't come back as a burst of reconnects.
The honest summary: a WebSocket on Thunderclap typically lives for hours, not weeks. Your app doesn't need to care, because every event has a sequence number and thunderclapJS tells you exactly which range was missed:
room.onGap((gap) => {
if (gap.reset) reloadRecentHistory();
else fetchMissed({ from: gap.from, to: gap.to });
});6. Overload is handled, not suffered
We also tested what happens when a room gets busier than its shards should carry. We set an artificially low per-shard budget and put 3,000 clients on one channel at two broadcasts a second, well over it. Overloaded shards handed sockets off to fresh ones until each was back within budget, without oscillating. Moved clients were back in a median of 150 ms, 99.7% of deliveries arrived (1.71 million), every missing one was reported as a gap, and no publish failed.
From measurements to your workload
A rule of thumb
Two numbers size a workload:
- Connections: a shard holds a few thousand sockets, so 1 million concurrent users is a few hundred shards, spread over the locations your users come from.
- Deliveries per second: for each channel, publishes per second × subscribers. A shard writes about 9,500 a second, and Thunderclap keeps each one below that, with headroom.
Whichever needs more shards decides. You don't size anything yourself (shards appear and disappear on their own), but the arithmetic tells you whether your workload is comfortably inside what's been measured.
Worked examples
These are extrapolations from the measurements above, not runs at this scale.
| Workload | Connections | Deliveries a second | Load on each shard | Verdict |
|---|---|---|---|---|
| Notifications: 1M users, one notification each every 5 minutes, on their own channel | 1,000,000 | ~3,300 | ~0.2% of a shard's budget | Connections decide. Traffic is negligible |
| Chat: 1M users in 30-person rooms, 550 messages a second in total | 1,000,000 | ~16,500 | ~1% | Connections decide. Each room is its own channel, so there's no shared bottleneck |
| Live dashboard: 20,000 viewers, 5 updates a second | 20,000 | 100,000 | ~11–13 shards' worth | Comfortable: traffic decides, and spreads automatically |
| Live event: 200,000 viewers on one channel, 2 updates a second | 200,000 | 400,000 | ~45–50 shards' worth | Inside what one channel fans out to directly today |
| Global final: 1,000,000 viewers on one channel, 1 update a second | 1,000,000 | 1,000,000 | 100+ shards' worth | Beyond direct fan-out today: needs regional relays (below) |
Two things stand out. For most products (chat, notifications, collaboration, presence) traffic per shard is tiny, and the only number that grows is connections, which are the cheap part. And the workloads that really stress a realtime system are single channels with huge audiences and frequent updates, so that's where the limits are.
Where the limits are today
- One channel, one writer. Each channel is ordered by a single actor, which is what makes sequence numbers and gap detection possible. It's not a bottleneck for rooms, feeds or dashboards, but if you need thousands of publishes a second on the same channel, split it (per room, per region, per entity) rather than funnelling everything through one name.
- Direct fan-out up to roughly half a million deliveries a second per channel. Today a channel sends each event straight to every shard that has subscribers. For larger audiences we're building regional relays, so a channel sends one copy per region and each region fans out locally.
- Presence channels hold up to 100 members, the same as Pusher. Larger presence is on the roadmap.
- We haven't run a storm larger than 50,000 clients at ~5,000 connects a second yet. The design aims an order of magnitude higher, and storms split naturally by location, but we'll publish that number when we've measured it, not before.
Designing for it
A few habits make the most of this model, whatever your scale:
- Use many small channels. One per room, per document, per user. Channels are independent actors, so a thousand quiet channels cost less than one busy one, and they never contend with each other.
- Store your own history, and use gaps. Thunderclap is a transport, not a database. Save each event's sequence number (returned by
publish) with the message, and backfill from your store whenonGapfires. - Treat reconnects as routine. They're short and handled for you; just don't build UI that assumes a socket lives forever.
- Don't worry about idle. Keep connections open. A connection that isn't receiving anything costs nothing.
Try it
The free plan, Spark, includes 200 concurrent connections and 2 million messages a month. Paid plans start at $29 for 5,000 connections, and are never capped: usage above the plan is billed, not refused. Every message counts once, however many people receive it.
Start with the quickstart, or read how Thunderclap compares with Pusher and Soketi.
Methodology. Tests ran on a dedicated deployment of the production engine. Load came from a dedicated server in Helsinki (delivery tests), a cloud machine in Frankfurt (single-shard tests) and 20 cloud machines in 10 regions (connect storms). Clients were thunderclapJS; publishes went through @thunderclapjs/server with 200-byte payloads. Latency was measured on the load machine's clock, from the publish request to the client receiving the event. Client timings from machines whose CPU was saturated by TLS handshakes are excluded, since they measure the load generator rather than Thunderclap.