Benchmarks: Pylon vs soketi vs Laravel Reverb¶
2026-09-17
pylon held every connection step at both ramps up to 80,000 connections, at roughly a quarter of both soketi's and Reverb's bytes per connection at 10,000 (5,867.1 vs 23,201.4 vs 21,046.5 bytes per connection), and delivered the highest fan-out throughput within the 99%/100 ms budget at 79,629.7 deliveries/s. soketi (Aloware fork v2.0.0) is connect-rate bound, not connection-count bound: it seats 10,000 connections cleanly but its accept loop saturates a single core before 20,000 arrive at 500/s per feeder, only reaching 20,000 at a slowed 100/s ramp, and never seats 40,000 or 60,000 at either ramp. Reverb matches pylon's 80,000-connection ceiling at both ramps but at 3.6-5.7x the memory per connection and an S5 p99 of 10,428 µs against pylon's 4,440 µs.
At a glance¶
Table 7: Summary¶
| Metric | pylon | soketi | reverb | soketi-pm2 | Best | pylon/soketi | pylon/reverb |
|---|---|---|---|---|---|---|---|
| Bytes/Connection at 10k (every server seated it, lower better) | 5867.1 | 23201.4 | 21046.5 | n/a | pylon | 0.25 | 0.28 |
| Bytes/Connection, largest step each server seated (lower better) | 3592.3 (S2-2, 500/s per feeder) | 19606.5 (S1b, 100/s per feeder) | 20740.2 (S2-2, 500/s per feeder) | n/a | pylon | 0.18 | 0.17 |
| Max subscribed reached, best of both ramps (higher better) | 80000 (S2-2, 500/s per feeder) | 20000 (S1b, 100/s per feeder) | 80000 (S2-2, 500/s per feeder) | n/a | pylon | 4.0 | 1.0 |
| Best deliveries/s within budget (S3, higher better) | 79629.7 (S3-4, rate=500) | 23204.4 (S3-3, rate=250) | 11000.7 (S3-4, rate=500) | none within budget; first step S3-1 (rate=50): 6740.4/s | pylon | 3.43 | 7.24 |
| S5 p99 latency µs (lower better) | 4440 | 5959 | 10428 | n/a | pylon | 0.75 | 0.43 |
S1 — memory per connection (10k / 20k / 40k)¶
Table 1: Memory per connection (S1a/S1b/S1c)¶
| Server | Step | Ramp | Requested | Subscribed | Idle Baseline RSS (KB) | Peak RSS (KB) | Bytes/Connection | Time to Subscribed (s) | Failure Mode |
|---|---|---|---|---|---|---|---|---|---|
| pylon | idle memory 10k | 500/s per feeder | 10000 | 10000 | 20700.0 | 77996 | 5867.1 | 10.2 | completed |
| pylon | idle memory 20k | 500/s per feeder | 20000 | 20000 | 20692.0 | 105960 | 4365.7 | 20.4 | completed |
| pylon | idle memory 40k | 500/s per feeder | 40000 | 40000 | 20692.0 | 168328 | 3779.5 | 40.6 | completed |
| pylon | idle memory 20k | 100/s per feeder | 20000 | 20000 | 20532.0 | 107724 | 4464.2 | 101.4 | completed |
| pylon | idle memory 40k | 100/s per feeder | 40000 | 40000 | 20520.0 | 169372 | 3810.6 | 202.6 | completed |
| soketi | idle memory 10k | 500/s per feeder | 10000 | 10000 | 116340.0 | 342916 | 23201.4 | 15.6 | completed |
| soketi | idle memory 20k | 500/s per feeder | no summary (server did not accept connections) | no summary (server did not accept connections) | 118764.0 | 374136 | no summary (server did not accept connections) | no summary (server did not accept connections) | connect-rate bound: CPU saturated at ~96.1% while ramping, seated ~13337 before the watchdog (estimated from RSS against the slow-ramp 20,000-connection figure of 19606.5 bytes/conn) |
| soketi | idle memory 40k | 500/s per feeder | no summary (server did not accept connections) | no summary (server did not accept connections) | 119236.0 | 481816 | no summary (server did not accept connections) | no summary (server did not accept connections) | connect-rate bound: CPU saturated at ~98.3% while ramping, seated ~18937 before the watchdog (estimated from RSS against the slow-ramp 20,000-connection figure of 19606.5 bytes/conn) |
| soketi | idle memory 20k | 100/s per feeder | 20000 | 20000 | 118156.0 | 501096 | 19606.5 | 105.9 | completed |
| soketi | idle memory 40k | 100/s per feeder | no summary (server did not accept connections) | no summary (server did not accept connections) | 118984.0 | 619700 | no summary (server did not accept connections) | no summary (server did not accept connections) | no loader summary (server did not accept connections) |
| reverb | idle memory 10k | 500/s per feeder | 10000 | 10000 | 58392.0 | 263924 | 21046.5 | 10.2 | completed |
| reverb | idle memory 20k | 500/s per feeder | 20000 | 20000 | 58448.0 | 467660 | 20951.7 | 20.4 | completed |
| reverb | idle memory 40k | 500/s per feeder | 40000 | 40000 | 58368.0 | 888352 | 21247.6 | 42.9 | completed |
| reverb | idle memory 20k | 100/s per feeder | 20000 | 20000 | 57944.0 | 466792 | 20933.0 | 102.2 | completed |
| reverb | idle memory 40k | 100/s per feeder | 40000 | 40000 | 57976.0 | 904544 | 21672.1 | 202.6 | completed |
Table 1/2's "Peak RSS" is the peak sampled value, standing in for a plateau median: connect holds connections only for its ~2 s RSS sample plus one broadcast before draining, so there is no multi-second plateau to median over. pylon and Reverb seat every requested count at the standard ramp; soketi seats only 10,000, needs the slow ramp for 20,000, and still fails 40,000 at either — connect-rate exhaustion, not memory or a crash: the sampler shows its single core pinned near 96-98% CPU while the loader reports no summary at all. Reverb's memory per connection is 3.59x pylon's at 10k, rising to 4.80x at 20k and 5.62x at 40k (4.69x/5.69x at the slow-ramp points) — a growing gap, not a fixed ratio.
S2 — connection ceiling (60,000 then 80,000)¶
Table 2: Connection ceiling (S2)¶
| Server | Step | Ramp | Requested | Subscribed | Subscribed/Requested | Peak RSS (KB) | Failure Mode |
|---|---|---|---|---|---|---|---|
| pylon | S2-1 (60000) | 500/s per feeder | 60000 | 60000 | 100.0% | 247028 | completed (fourth attempt; see Reruns) |
| pylon | S2-2 (80000) | 500/s per feeder | 80000 | 80000 | 100.0% | 301344 | completed |
| pylon | S2-1 (60000) | 100/s per feeder | 60000 | 60000 | 100.0% | 246452 | completed |
| pylon | S2-2 (80000) | 100/s per feeder | 80000 | 80000 | 100.0% | 308908 | completed |
| soketi | S2-1 (60000) | 500/s per feeder | no summary (server did not accept connections) | no summary (server did not accept connections) | no summary (server did not accept connections) | 419396 | connect-rate bound: CPU saturated at ~99.0% while ramping, seated ~15700 before the watchdog (estimated from RSS against the slow-ramp 20,000-connection figure of 19606.5 bytes/conn) |
| soketi | S2-2 (80000) | 500/s per feeder | not run | not run | not run | not run | not run |
| soketi | S2-1 (60000) | 100/s per feeder | no summary (server did not accept connections) | no summary (server did not accept connections) | no summary (server did not accept connections) | 482116 | no loader summary (server did not accept connections) |
| soketi | S2-2 (80000) | 100/s per feeder | not run | not run | not run | not run | not run |
| reverb | S2-1 (60000) | 500/s per feeder | 60000 | 60000 | 100.0% | 1291996 | completed |
| reverb | S2-2 (80000) | 500/s per feeder | 80000 | 80000 | 100.0% | 1678256 | completed |
| reverb | S2-1 (60000) | 100/s per feeder | 60000 | 60000 | 100.0% | 1291408 | completed |
| reverb | S2-2 (80000) | 100/s per feeder | 80000 | 80000 | 100.0% | 1690908 | completed |
pylon and Reverb both hold the full 80,000-connection step at either ramp, with no ceiling found and the free-memory abort (below 300 MB) never triggering. pylon's standard-ramp S2-1 (60,000) logs four outcomes before the kept success: two with no loader summary at all (the feeder port exhaustion described in the Method notes, before the range was widened), a third that actually seated 60,000 but was misread as a failure by a defective pass/fail check in the run script (a false negative), and the fourth is the kept "subscribed=60000 OK" result; S2-2 needed only one pass. soketi never seats 60,000 at any ramp, confirming the ceiling already visible in S1: its true ceiling sits between the 20,000 it reaches at the slow ramp and the 40,000 it fails at either ramp. Its 80,000 step is not run, not a measured failure.
S3 — fan-out throughput, 10 subscribers per channel¶
Table 3: Fan-out throughput (S3)¶
Received (own) counts only events published by the reporting process itself, isolated per channel; Received (foreign) is shown for information and is expected to be 0 here (not recorded for older loader builds). Received/Expected and Deliveries/s use received (own) only. Budget = received(own)/expected ≥ 99% and worst p99 ≤ 100,000 µs. Each reported percentile is the worst value of that percentile across loader processes, so p50 and p99 in a row may come from different processes.
| Server | Step | Offered (pub/s) | Sent | Received (own) | Received (foreign) | Received/Expected | Deliveries/s | Budget | p50 (µs) | p99 (µs) | p99.9 (µs) | Mean CPU % | mpstat usr/sys % |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pylon | S3-1 (rate=50) | 800 | 16016 | 160300 | 0 | 100.09% | 8015.0 | within budget | 1311 | 3338 | 6651 | 18.68 | 2.68/4.41 |
| pylon | S3-2 (rate=100) | 1600 | 32016 | 320370 | 0 | 100.07% | 16018.5 | within budget | 1036 | 2455 | 5091 | 29.07 | 4.33/6.92 |
| pylon | S3-3 (rate=250) | 4000 | 80016 | 800540 | 0 | 100.05% | 40027.0 | within budget | 886 | 3590 | 6901 | 53.17 | 7.47/11.21 |
| pylon | S3-4 (rate=500) | 8000 | 159186 | 1592594 | 0 | 100.05% | 79629.7 | within budget | 3067 | 10764 | 15745 | 72.82 | 9.5/14.34 |
| soketi | S3-1 (rate=50) | 800 | 16016 | 160300 | 0 | 100.09% | 8015.0 | within budget | 4055 | 6475 | 10592 | 36.2 | 8.04/5.09 |
| soketi | S3-2 (rate=100) | 1600 | 32016 | 320370 | 0 | 100.07% | 16018.5 | within budget | 3217 | 10330 | 15253 | 47.17 | 10.25/6.08 |
| soketi | S3-3 (rate=250) | 4000 | 46387 | 464088 | 0 | 100.05% | 23204.4 | within budget | 6565 | 11927 | 16244 | 53.78 | 12.11/7.72 |
| soketi | S3-4 (rate=500) | 8000 | 46177 | 462015 | 0 | 100.05% | 23100.8 | within budget | 6606 | 11706 | 14737 | 55.73 | 11.9/7.75 |
| reverb | S3-1 (rate=50) | 800 | 16016 | 160300 | 0 | 100.09% | 8015.0 | within budget | 5140 | 15777 | 119406 | 59.44 | 15.69/5.12 |
| reverb | S3-2 (rate=100) | 1600 | 20558 | 205720 | 0 | 100.07% | 10286.0 | within budget | 15286 | 16809 | 23298 | 65.93 | 18.91/6.36 |
| reverb | S3-3 (rate=250) | 4000 | 18784 | 187938 | 0 | 100.05% | 9396.9 | within budget | 16769 | 18464 | 20840 | 64.98 | 17.65/5.46 |
| reverb | S3-4 (rate=500) | 8000 | 21985 | 220014 | 0 | 100.07% | 11000.7 | within budget | 14327 | 15491 | 18235 | 65.21 | 18.75/6.42 |
| soketi-pm2 | S3-1 (rate=50) | 800 | 16016 | 134809 | 0 | 84.17% | 6740.4 | over budget | 2365 | 7360 | 18759 | 59.88 | 13.28/6.55 |
| soketi-pm2 | S3-2 (rate=100) | 1600 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| soketi-pm2 | S3-3 (rate=250) | 4000 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| soketi-pm2 | S3-4 (rate=500) | 8000 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
All three single-process servers stay inside budget through all four steps: pylon's deliveries/s scale roughly linearly (8,015 to 79,630), while soketi's and Reverb's achieved sent totals plateau below the requested rate at steps 3-4 (soketi near 46k; Reverb 18-22k) — a throughput ceiling, not a dropped-message failure. soketi-pm2 fails the 99% bar on its first step at 84.17% received, well under single-process soketi's clean pass: the shortfall traces to the cluster adapter's UDP-broadcast IPC (SOKETI_CLUSTER_CHECK_INTERVAL=500 ms) not converging before messages are sent, even with the 20 s warm-up, so steps 2-4 were never attempted.
S4 — hot-channel fan-out¶
Table 4: Hot-channel fan-out (S4)¶
Received (own) counts only deliveries of a process's own publishes to its own subscribers; Received (foreign) counts deliveries of the other process's publishes to its own subscribers (not recorded for older loader builds, which makes Received/Expected and Deliveries/s unavailable for that row). Received/Expected = (received + received_foreign) summed over processes ÷ (sent summed × subscribed summed); Deliveries/s = (received + received_foreign) summed ÷ secs. Budget = that aggregate ratio ≥ 99% and worst p99 ≤ 100,000 µs (latency stays the process's own-event percentiles). Each reported percentile is the worst value of that percentile across loader processes, so p50 and p99 in a row may come from different processes.
| Server | Step | Offered (pub/s) | Sent | Received (own) | Received (foreign) | Received/Expected | Deliveries/s | Budget | p50 (µs) | p99 (µs) | p99.9 (µs) | Mean CPU % | mpstat usr/sys % |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pylon | S4-1 (rate=5) | 40 | 808 | 404000 | 404000 | 100.0% | 40400.0 | within budget | 8044 | 19021 | 45744 | 20.15 | 1.86/6.77 |
| pylon | S4-2 (rate=10) | 80 | 1608 | 804000 | 804000 | 100.0% | 80400.0 | within budget | 8126 | 16474 | 20987 | 39.56 | 2.5/12.02 |
| pylon | S4-3 (rate=25) | 200 | 3313 | 1656500 | 1656500 | 100.0% | 165650.0 | over budget | 36208 | 170655 | 263585 | 59.94 | 3.56/15.74 |
| pylon | S4-4 (rate=50) | 400 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| soketi | S4-1 (rate=5) | 40 | 808 | 404000 | 404000 | 100.0% | 40400.0 | over budget | 70909 | 148242 | 169738 | 48.0 | 6.91/8.55 |
| soketi | S4-2 (rate=10) | 80 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| soketi | S4-3 (rate=25) | 200 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| soketi | S4-4 (rate=50) | 400 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| reverb | S4-1 (rate=5) | 40 | 808 | 404000 | 403997 | 100.0% | 40399.8 | over budget | 88735 | 126550 | 175767 | 53.69 | 11.16/8.05 |
| reverb | S4-2 (rate=10) | 80 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| reverb | S4-3 (rate=25) | 200 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| reverb | S4-4 (rate=50) | 400 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| soketi-pm2 | S4-1 (rate=5) | 40 | 808 | 332589 | 332127 | 82.27% | 33235.8 | over budget | 14376 | 31883 | 47382 | 48.33 | 7.42/8.44 |
| soketi-pm2 | S4-2 (rate=10) | 80 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| soketi-pm2 | S4-3 (rate=25) | 200 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
| soketi-pm2 | S4-4 (rate=50) | 400 | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run | not run |
pylon is the only server to advance past the first hot-channel step: it passes S4-1/S4-2 within budget and stops at S4-3 on the 100 ms p99 cap (170,655 µs), while soketi and Reverb both stop after S4-1 on the same cap (148,242 µs and 126,550 µs) despite full delivery — pylon holds the budget twice as long on this single 1,000-connection channel. soketi-pm2's S4-1 fails outright on delivery (82.27%, under 99%) for the same cluster-convergence reason as S3 — a delivery failure, not a latency one.
S5 — latency at fixed load¶
Table 5: Latency at fixed load (S5)¶
Received/Expected is computed on received (own-process events only); Received (foreign) is the cross-process delivery count when the loader build records it (not recorded for older loader builds). Each reported percentile is the worst value of that percentile across loader processes, so p50 and p99 in a row may come from different processes.
| Server | p50 (µs) | p99 (µs) | p99.9 (µs) | max (µs) | Received (foreign) | Received/Expected | Mean CPU % |
|---|---|---|---|---|---|---|---|
| pylon | 1504 | 4440 | 8216 | 10846 | 0 | 100.07% | 13.79 |
| soketi | 2723 | 5959 | 10559 | 13246 | 0 | 100.07% | 25.39 |
| reverb | 6668 | 10428 | 110166 | 125763 | 0 | 100.07% | 47.96 |
At the fixed load of 10,000 connections across 1,000 channels publishing at 25/process, all three servers deliver 100% of expected events with zero cross-process contamination. pylon's p50 is 55% of soketi's and 23% of Reverb's; its p99 is 75% of soketi's and 43% of Reverb's (1,504/4,440 µs vs 2,723/5,959 µs vs 6,668/10,428 µs), with the lowest mean CPU (13.79% vs 25.39%/47.96%). Reverb's p99.9 (110,166 µs) is an order of magnitude above its own p50/p99 — a long tail. This is one fixed-load snapshot, not a latency trend; S3's per-step p99 is the source for how latency moves with throughput.
S6 — graceful stop under load¶
Table 6: Graceful stop (S6)¶
| Server | Stop Duration | Result | ExecMainStatus | Subscribed | Sent | Received |
|---|---|---|---|---|---|---|
| pylon | 0m2.235s | success | 0 | 10000 | 24 | 240 |
| soketi | 0m3.632s | success | 0 | 10000 | 28 | 200 |
| reverb | 0m2.110s | success | 0 | 10000 | 20 | 200 |
S6 ran as channels at a 1/process publish rate rather than the connect scenario, because connect only samples RSS for ~2 s and fires one broadcast before draining, so a systemctl stop timed against a secs-long hold would land after the loader had already disconnected; channels at rate 1 keeps publishing so the mid-flight stop lands on a genuinely active connection. pylon's S6 was rerun with the fixed (86d8a96) loader; the discarded first pass is not used here. At the tested 10,000-connection load, all three stop cleanly within TimeoutStopSec=20 (2.1-3.6 s, ExecMainStatus=0) — not representative of every load: at the 20,000-60,000-connection steps where it had seated an estimated 13,000-19,000 connections, soketi needed the full 20 s SIGTERM timeout and a SIGKILL in four runs (real 0m20.297s to 0m20.389s, Result=timeout, ExecMainStatus=9), a pattern this table misses since S6 only ran at 10,000.
Environment¶
| Component | Version / setting |
|---|---|
| pylon | v0.5.1, aarch64-unknown-linux-musl tarball, PYLON_WORKERS=0 (auto, both cores) |
| soketi | Aloware fork @aloware/soketi@2.0.0 (aloware-soketi-2.0.0.tgz), Node.js 24.21.0 / npm 11.19.0 (NodeSource setup_24.x), pm2 7.0.4 (PM2 row only) |
| Laravel Reverb | v1.11.1, Laravel framework 13.32.0, PHP 8.4.24 (NTS), Composer 2.10.3, ext-uv 0.3.0 (beta channel); cache, session and queue drivers file / file / sync |
| pylon-load | built from pylon master 4ae755ce1c07fd2df379383f7d748638ab9e83b8 in rust:1.98.1-alpine3.24; fixed for per-process channel isolation and own-event latency at commit 86d8a96 (see Method notes) |
| Payload | loader's default {"seq":N,"t":"<nanos>"} event body, 34-45 bytes; full published frame adds ~30-60 bytes for the channel-name wrapper |
| Kernel / hardware | 6.12.107+deb13-arm64, 2 vCPU / 4 GB target (arm64) |
| Sysctls | fs.nr_open=20000500, net.core.somaxconn=65535, net.ipv4.tcp_max_syn_backlog=65535 on target and feeders; feeders' net.ipv4.ip_local_port_range widened from the default 32768-60999 to 1024 65535 with tcp_tw_reuse=1 for the run, restored after |
| Unit settings | every server unit: LimitNOFILE=2000000 (pylon's shipped default, applied identically to soketi/soketi-pm2/reverb), TimeoutStopSec=20, KillSignal=SIGTERM, Restart=no; soketi-pm2 adds SOKETI_ADAPTER_DRIVER=cluster and -i 2 (2 cluster instances). Heap/memory flags: soketi single-process node --max-old-space-size=3072, Reverb php -d memory_limit=3G, soketi-pm2 has no heap flag at all; none of these bound anything measured — soketi's highest observed peak was ~620 MB and Reverb's ~1.68 GB, both well under their configured ceilings |
| Ramp rates | standard 500 connections/s per feeder; slow-ramp addendum 100 connections/s per feeder for S1b/S1c/S2, added mid-run once soketi's single-core connect-rate ceiling was identified |
| Fairness settings | TLS off; one shared app id/key/secret (rotated once); client events off; no webhooks; debug off; metrics endpoints off; identical scenario order, payload, connection counts, channel shapes and durations per server |
Method¶
One server at a time on a 2 vCPU / 4 GB arm64 host, load generated from two 4 vCPU / 8 GB hosts (the feeders) over a private network, one shared app, TLS off, per-second RSS and CPU sampling on the server host.
| Scenario | Definition | Stop rule |
|---|---|---|
| S1a/b/c | Connect 10k/20k/40k, hold for the sampler window, one post-ramp broadcast | none (idle-memory measurement) |
| S2 | Connect 60k then 80k | stop below 99% subscribed, on server exit, or target free memory < 300 MB |
| S3 | channels scenario, 10,000 conns / 1,000 channels / 8 procs per feeder, rate steps 50/100/250/500 per process |
stop when received < 99% of sent x 10 recipients, or worst p99 > 100 ms |
| S4 | fanout scenario, 1,000 conns / 4 publishers per feeder on one hot channel, rate steps 5/10/25/50 per publisher |
stop when aggregate (received+received_foreign) < 99% of sent x subscribed, or worst p99 > 100 ms |
| S5 | channels scenario, 10,000 conns / 1,000 channels / 8 procs per feeder, fixed rate 25/process, 60 s |
none (latency snapshot at fixed load) |
| S6 | channels scenario, 10,000 conns held, mid-flight systemctl stop after ramp+10s |
none (records stop duration and unit result) |
Method notes¶
The feeders' default ephemeral port range (32768-60999) capped one feeder near
28,000 connections to a single address and port, so it was widened to 1024-65535
with tcp_tw_reuse=1 for the runs and restored afterwards.
The first pylon pass on S3-S6 was discarded: the loader named its channels identically
in every process, so the collisions inflated received counts 16x, and it stamped
latency with a process-local clock. The loader was fixed at commit 86d8a96 (PR #154)
— a per-process run id in channel names and payloads, receives classified as own or
foreign, latency measured only on own events — and pylon's S3-S6 numbers above come
from the fixed loader.
The S4 budget gate is the aggregate
(received + received_foreign) ≥ 0.99 × (sent × subscribed), because own-only
counts are structurally ~50% in a symmetric two-process run.
Reruns: pylon's S2-1 at the standard ramp logged four attempts — two feeder port-range failures, one gate false-negative on an actual success, and the kept success at 60,000 — and S2-2 one pass. pylon's S4-1 ran three times, the kept pass being the one after the gate fix (p50 7,098/8,044 µs and p99 18,628/19,021 µs across its two processes). soketi's S1b/S1c/S2 failures were kept as single passes because they are server failures, not tooling. soketi-pm2 had no reruns.
What this does not show¶
pylon runs as a single process using both cores (PYLON_WORKERS=0); soketi and Reverb run single-process on one core in every main-table row, and soketi-pm2 is soketi's own two-core variant, not comparable core-for-core. Reverb has no outbound webhook feature at all — a missing capability, not a disabled setting. Keepalive cadence was left at each shipped default and never aligned: soketi hardcodes a 120 s WS idle timeout, Reverb defaults ping_interval=60s/activity_timeout=30s, pylon's default PYLON_ACTIVITY_TIMEOUT is 120 s (configuration reference). Payload limits (Reverb 10,000 bytes, soketi 100 KB) never bound anything, since every event is 34-45 bytes. TLS was off throughout; the load client is pylon-load, not a browser SDK; soketi here is the Aloware fork v2.0.0, not the stale upstream v1.6.1. Every p50/p99/p99.9 in Tables 3-5 is the worst value across the loader's own processes for that step, not a merged distribution, so a row's p50 and p99 can come from different processes. Table 7's "largest step each server seated" row compares different connection counts (80k for pylon/Reverb, 20k for soketi), not like-for-like; the "at 10k" row above it is. soketi's own published benchmark (500 idle connections, AWS t3.small, 6 ms internal latency) is the closest external reference for this hardware class but used a different scenario entirely — a sanity check only.
Reproducing¶
The load generator is pylon-load in load/ (cargo run -p pylon-load -- --help),
with the connect, channels and fanout scenarios used here. The raw outputs and
the analysis script are not part of this repository. Run each server alone on the
same host and keep the scenario order, payload, connection counts, channel shapes and
durations identical across servers.
Trademark notice
"Pusher" is a trademark of its respective owner. Pylon is an independent, clean-room implementation and is not affiliated with or endorsed by Pusher.