All posts

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 publish reaches the channel, which sends one copy to each shard with subscribers; each shard writes it to its sockets.Your backendpublishchannelseq 1042shardFrankfurtshardLondonshardTokyo1 message in1 copy per shardwritten to each socket
One publish in, one copy per shard, then each shard writes it to its own sockets.

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 shardDeliveredTime to reach every socket (median / p99)
1,00060,000 of 60,000250 ms / 962 ms
2,000119,165 of 119,182357 ms / 677 ms
5,000299,914 of 299,916539 ms / 1.1 s
9,000537,720 of 537,720959 ms / 2.9 s
17,000falls 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)
Load800 channels, 500 publishes a second, 20 min3,000 sockets in 100 rooms, 100 publishes a second, 10 min
Messages580,011~60,000 per run (~1.7M deliveries)
Gaps, duplicates0, 00, 0
Latency median / p9973 ms / 871 ms55 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 connected50,000 of 50,000
Edge connect time, by locationmedian 7–28 ms (São Paulo 127 ms)
Client's first connect, machines that weren't CPU-boundmedian 32–181 ms in every region
Capacity added during the stormwithin 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:

client.ts
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:

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.

WorkloadConnectionsDeliveries a secondLoad on each shardVerdict
Notifications: 1M users, one notification each every 5 minutes, on their own channel1,000,000~3,300~0.2% of a shard's budgetConnections decide. Traffic is negligible
Chat: 1M users in 30-person rooms, 550 messages a second in total1,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 second20,000100,000~11–13 shards' worthComfortable: traffic decides, and spreads automatically
Live event: 200,000 viewers on one channel, 2 updates a second200,000400,000~45–50 shards' worthInside what one channel fans out to directly today
Global final: 1,000,000 viewers on one channel, 1 update a second1,000,0001,000,000100+ shards' worthBeyond 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

Designing for it

A few habits make the most of this model, whatever your scale:

  1. 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.
  2. 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 when onGap fires.
  3. Treat reconnects as routine. They're short and handled for you; just don't build UI that assumes a socket lives forever.
  4. 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.