From 8114bbd238b6a27fb3a8792bf93d288155b8f4b1 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Fri, 1 May 2026 05:55:50 -0700 Subject: [PATCH 01/57] Add AnyCable vs Socket.io comparison page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds /compare/socket-io — a methodology-first comparison built around benchmarks of three real configurations (default Socket.io, Socket.io with Connection State Recovery, and AnyCable) at 10,000 concurrent clients on identical Railway infrastructure. Page structure (analytical-takeaway H2s): 1. Only AnyCable delivers every message on time 2. Every Socket.io deploy severs every connection. This is architectural. 3. AnyCable holds 50,000 idle connections on under 2 GB and 0.3 vCPU Plus: methodology callout, migration code example, three-column feature comparison with citations to AnyCable + Socket.io docs, FAQ accordion, and CTA. Design system: - Notebook-feel dotted background, continuous across the page - Rounded white cards (8 px) for tables and code blocks on the dotted bg - Sticky right column so illustrations stay visible while text scrolls - Inline code chips (0.85 em, subtle background, 4 px radius) - Red accent reserved for "worst per metric" only — not for emphasis - Mobile: hero stacks single-column, table column hides via existing rule Files: - src/compare/socket-io/index.html new comparison page - src/modules/blocks/compare.scss page-scoped styles (under .compare-page) - src/modules/blocks/about-slide.scss -sticky and -full media modifiers, calm inline-code chips - src/index.scss imports compare.scss - src/blog/anycable-vs-socket-io/index.md removed (content lives on the comparison page now) - .gitignore benchmark/ directory (lives in its own repo) --- .gitignore | 3 + src/blog/anycable-vs-socket-io/index.md | 108 ---- src/compare/socket-io/index.html | 626 ++++++++++++++++++++++++ src/index.scss | 1 + src/modules/blocks/about-slide.scss | 37 ++ src/modules/blocks/compare.scss | 318 ++++++++++++ 6 files changed, 985 insertions(+), 108 deletions(-) delete mode 100644 src/blog/anycable-vs-socket-io/index.md create mode 100644 src/compare/socket-io/index.html create mode 100644 src/modules/blocks/compare.scss diff --git a/.gitignore b/.gitignore index 4bb4556..3442e80 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ dist-ssr *.njsproj *.sln *.sw? + +# Benchmarks live in their own repo: irinanazarova/anycable-socketio-benchmarks +benchmark/ diff --git a/src/blog/anycable-vs-socket-io/index.md b/src/blog/anycable-vs-socket-io/index.md deleted file mode 100644 index 5e5ae86..0000000 --- a/src/blog/anycable-vs-socket-io/index.md +++ /dev/null @@ -1,108 +0,0 @@ -# AnyCable vs Socket.io: built-in reliability vs roll-your-own - -April 1, 2026 -{date} - -
- -Socket.io is the most popular WebSocket library in the JavaScript ecosystem. It's battle-tested, well-documented, and free. So why would you use AnyCable instead? Because Socket.io doesn't guarantee delivery — and in production, that's the problem that breaks everything else. -{intro} - -
- -## Socket.io doesn't guarantee delivery - -This is the core difference between the two tools, and everything else flows from it. - -Socket.io provides **at-most-once delivery**. Their own [documentation][socketio-delivery] says it plainly: "if the connection is broken while an event is being sent, then there is no guarantee that the other side has received it." There is no retry, no buffering for disconnected clients, and no catch-up on reconnection. - -AnyCable provides **at-least-once delivery**. Publications are stored in logs. The client tracks its stream position. On reconnection, it automatically recovers every missed message. Your application code doesn't change — you broadcast messages, and every client receives every one of them, even through disconnections. - -This matters more than you think. WebSocket connections are not as reliable as they appear in development. In production, micro-disconnections happen on stable connections, longer interruptions occur during commutes and subway rides, and every server deploy severs active connections. Without delivery guarantees, every one of these events silently corrupts your client state. - -### What this looks like in practice - -**Live chat:** A user enters a tunnel for 3 seconds. Two messages arrive in the group chat during that window. With Socket.io, those messages are gone — the user sees an incomplete conversation. With AnyCable, the client catches up automatically on reconnection. - -**LLM/AI streaming:** You're streaming an AI response word by word. The client briefly loses connection mid-sentence. With Socket.io, the response is truncated or garbled — chunks are lost with no recovery. With AnyCable, every chunk is recovered in order. As we described in our article on [the pitfalls of LLM streaming][llm-streaming], this is a real production problem that breaks AI-powered features. - -**Real-time dashboards:** A monitoring dashboard shows live metrics. A 200ms network blip causes a gap in the data. With Socket.io, that gap is permanent. With AnyCable, the missed data points are recovered and the chart stays complete. - -**Deploys:** You ship code. With Socket.io, every WebSocket connection is severed — every client must reconnect, and any in-flight messages are lost. With AnyCable, the Go-based WebSocket server is a separate process from your application. When you deploy your app, the WebSocket server stays up. Connections are never interrupted. Users never notice a deployment. - -As Doximity engineers [described on the On Rails podcast][doximity-podcast]: - -> "AnyCable allows them to keep that connection open. That Go service stays up, and you can continue shipping your Rails application as normal." - -## A standalone server, not an embedded library - -Socket.io is a library you embed in your Node.js application. It runs in the same process as your business logic, competing for the same CPU and memory. - -AnyCable is a standalone server written in Go. It handles WebSocket connections independently — your application communicates with it via HTTP API. This separation has three consequences: - -1. **Scale independently.** Need more WebSocket capacity? Scale AnyCable. Need more app server capacity? Scale your app. They don't compete for resources. - -2. **Deploy independently.** Ship code to your app without touching WebSocket connections. This is why connections survive deploys. - -3. **Use any backend language.** Socket.io locks you into Node.js. AnyCable works with Rails, Laravel, Node, Python/FastAPI, or any backend that can make HTTP requests. Broadcast a message from any language: - -``` -POST /api/v1/broadcasts -{ "stream": "chat/42", "data": "{\"message\": \"hello\"}" } -``` - -### Why Go for WebSockets - -WebSocket connections are long-lived — a user can hold one open for hours. In Node.js, each connection consumes meaningful memory in the V8 runtime. At 10,000 concurrent connections, this adds up fast. - -Go's goroutine-based concurrency model handles long-lived connections with minimal overhead. AnyCable serves 10,000+ concurrent connections per server using a fraction of the memory that Node.js or Ruby would need. And Go compiles to a single binary — no `node_modules`, no runtime version conflicts. - -## What you don't have to build - -With Socket.io, you get a WebSocket transport and rooms. Everything else is your responsibility: - -| What you need | Socket.io | AnyCable | -|---------------|-----------|----------| -| Reliable delivery | No (at-most-once) | Built-in (at-least-once) | -| Missed message recovery | No catch-up mechanism | Automatic on reconnection | -| Presence (who's online) | Build it yourself | Built-in | -| Authentication | Build your own middleware | JWT, signed streams | -| Pub/sub clustering | Redis adapter (separate setup) | Embedded NATS (zero extra infra) | -| Monitoring | Add your own | Prometheus & StatsD built-in | -| Binary compression | No | Yes (Pro) | -| Deploy resilience | Not possible (same process) | Built-in (separate server) | - -Each of these is weeks of engineering work to build properly, and months to battle-test in production. AnyCable ships them as built-in primitives because we've been doing this since 2017. - -## Proven at scale - -AnyCable has been powering real-time features in production since 2017, for companies like: - -- **Doximity** — telehealth for 80% of US physicians -- **CoinGecko** — cryptocurrency market data at massive scale -- **Jobber** — field service management ($167M revenue) -- **Headway** — mental health therapy ($2.3B valuation) -- **Circle** — community platform ($30M+ raised) -- **ClickFunnels** — sales funnels (~$265M revenue) - -The project is actively developed with regular releases, a dedicated team, commercial Pro support, and a growing ecosystem including [Laravel support][laravel-post], [Pusher protocol compatibility](https://docs.anycable.io/guides/pusher), and the emerging [Durable Streams](https://docs.anycable.io/guides/durable_streams) standard. - -## When Socket.io is the right choice - -To be fair: if you're building a small Node.js app, prototyping, or need full control over a custom protocol, Socket.io is a fine choice. It's well-documented, has a massive community, and is free. - -But if you need delivery guarantees — and in production, you almost certainly do — you'll end up building them yourself on top of Socket.io. AnyCable gives you delivery guarantees, presence, authentication, and scaling out of the box, with any backend language, tested in production by companies serving millions of users. - -## Get started - -- [Documentation](https://docs.anycable.io) -- [Rails getting started guide](https://docs.anycable.io/rails/getting_started) -- [Laravel guide](https://docs.anycable.io/guides/laravel) -- [JavaScript/TypeScript client](https://github.com/anycable/anycable-client) -- [GitHub](https://github.com/anycable/anycable) -- [AnyCable Pro](https://plus.anycable.io/pro) — free 2-month trial - -[socketio-delivery]: https://socket.io/docs/v4/delivery-guarantees -[llm-streaming]: https://evilmartians.com/chronicles/anycable-rails-and-the-pitfalls-of-llm-streaming -[doximity-podcast]: https://podcast.rubyonrails.org/2462975/episodes/17653501 -[laravel-post]: https://evilmartians.com/chronicles/anycable-for-laravel diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html new file mode 100644 index 0000000..d4756b1 --- /dev/null +++ b/src/compare/socket-io/index.html @@ -0,0 +1,626 @@ + + + {{> dochead pageTitle="AnyCable vs Socket.io | Delivery Guarantees, Replay Latency, Deploy Resilience" pageDescription="Default Socket.io loses 13% of messages under jitter at 10,000 clients. Connection State Recovery catches up — but with a 9-second p99 replay tail. AnyCable: 100% delivery, sub-second replay. Benchmarked on identical Railway infrastructure."}} + +
+ {{> header}} +
+ + {{!-- Floating in-page nav: highlights the section currently in view. + Hidden below 1280px (no room next to the content column). --}} + + + {{!-- Comparison hero — focused on the comparison, not branding --}} +
+
+
+

Comparison

+

+ AnyCable vs Socket.io +

+

+ 10,000 clients on Railway, including Socket.io's Connection State Recovery. CSR and AnyCable both hit 100% delivery. Replay latency tells the story. +

+

+ Open source — run it yourself +

+
+ + {{!-- Headline numbers — three cards. Each is a small comparison + table with a single highlighted "worst" cell so the analytical + reader can scan all three results in one glance. --}} +
+
+
Delivery rate at 10,000 clients
+
+
Default Socket.io
87.4%
+
Socket.io + CSR
100%
+
AnyCable
100%
+
+
1.2M expected deliveries; default lost 150,642.
+
+ +
+
Replay latency p99
+
+
Default Socket.io
n/a (lost)
+
Socket.io + CSR
9.0 s
+
AnyCable
1.0 s
+
+
Time for replayed messages to arrive on reconnect.
+
+ +
+
Idle connection capacity
+
+
AnyCable, single instance
50,000
+
Memory used
1.98 GB
+
CPU used (of 32 vCPU)
~0.3
+
+
We hit a test-client ceiling at ~56K — not anycable-go's.
+
+
+ + {{!-- About this comparison: methodology + authorship + reproducibility --}} +
+

About this comparison. This is the AnyCable team's own scrutiny — where AnyCable wins on measurable terms, and where Socket.io is enough. All three setups run on identical Railway infrastructure (Pro tier, 32 vCPU / 32 GB); every parameter, script, and raw result is reproducible from the open-source bench repo.

+

AnyCable has been in production since 2017 at Doximity, CompanyCam, Headway, Jobber, CoinGecko, ClickFunnels, and 30+ other companies.

+
+
+
+ + {{!-- Delivery guarantees — two-halves like about-slide --}} +
+
+
+
+

1. Only AnyCable delivers every message on time#

+

+ Two questions matter on the open internet: does every message arrive, and does it arrive when the user expects it? Under simulated mobile-network jitter — 1-second TCP drops every ~15 seconds, the pattern WiFi handoffs and cellular blips produce — default Socket.io fails the first: ~13% of messages never arrive. CSR fixes delivery, but replay takes up to 12 seconds. AnyCable: 100% delivery, 1-second p99 replay tail. +

+

Default Socket.io: messages sent during a disconnect aren't recoverable

+

+ From the Socket.io delivery-guarantees doc: "if the connection is broken while an event is being sent, then there is no guarantee that the other side has received it." No buffering, no catch-up protocol. This is the default — and what most production Socket.io apps ship. +

+

Socket.io + CSR: opt-in catch-up with adapter and replay constraints

+

+ Connection State Recovery (Socket.io 4.6+) buffers per-socket state — id, rooms, socket.data — for up to maxDisconnectionDuration (default 2 min). The server appends an opaque per-packet offset; on reconnect the client passes pid + last-seen offset, and the server replays missed packets. +

+

+ Four caveats from the Socket.io docs: +

+
    +
  • ·Opt-in. Most apps don't enable it.
  • +
  • ·Adapter constraints. Doesn't work with Socket.io's most common scaling adapter (Redis pub/sub). Multi-node needs Redis Streams or MongoDB.
  • +
  • ·Restart-fragile. The default in-memory adapter wipes all state on every server restart.
  • +
  • ·"The recovery will not always be successful" — their words. Application-level reconciliation is still required.
  • +
+

AnyCable: at-least-once by default, replay is per-stream

+

+ AnyCable's protocol (actioncable-v1-ext-json) tracks each stream by epoch (broker state identifier) and offset (message position). The client stores (stream, epoch, offset). On reconnect it sends a history command; the server replays the missed range as a batch and acks with confirm_history (or reject_history if evicted). +

+

+ Retention is per stream: history_limit (default 100), history_ttl (default 300 s), sessions_ttl (separate). With NATS JetStream or Redis brokers, history survives restart and works across nodes. None of this is opt-in or experimental. +

+
+
+
# Default Socket.io — 10K clients, 120 msgs each
+Client 0: missing 6, 7, 36, 37, 73, 74, 104, 105
+Client 1: missing 3, 4, 38, 39, 69, 70, 108, 109
+Client 2: missing 4, 5, 41, 42, 76, 77, 111-113
+
+# Each cluster = one jitter event. ~15 lost per client.
+# Total: 150,642 of 1,200,000 expected. 87.4% delivery.
+
+# Socket.io + CSR — same workload
+Client 0: all 120 received  (replay tail: max 12.0s)
+Client 1: all 120 received  (p99: 9.0s)
+Client 2: all 120 received  (p95: 4.9s)
+
+# AnyCable — same workload
+Client 0: all 120 received  (replay tail: max 3.5s)
+Client 1: all 120 received  (p99: 1.0s)
+Client 2: all 120 received  (p95: 0.7s)
+
+
+ + {{!-- Benchmark table --}} +
+
+

Benchmark: 10,000 clients under simulated jitter

+

+ 10,000 clients on Railway. Publisher broadcasts 120 numbered messages at 2/sec — 1.2M expected deliveries. Each client force-closes its TCP socket for 1 second every ~15 seconds (no clean close — like a WiFi drop). Over the publishing window: ~10 jitter events per client, ~13% blind time. +

+

+ Default Socket.io: 87.41% delivery. 150,642 messages lost. The loss pattern matches the blind window — nothing arrives during the outage, nothing comes back after. +

+

+ Socket.io + CSR: 100% delivery, 9-second p99 latency. 99.5% of jitter events successfully resume the session; replayed messages arrive in seconds, worst case 12. +

+

+ AnyCable: 100% delivery, 1-second p99 latency. 7× faster than CSR. The history replay is per-stream — clients receive the missed range as a batch. +

+

Why default Socket.io's 13% loss is structural

+

+ Each jitter event creates d seconds of blind window. Clients see N events over publishing window T. Expected loss = (d × N) / T. For our run: 1.3 s × 10 / 60 ≈ 22% upper bound. We measured 13% — lower because not every event fully overlaps publishing. This isn't a configuration bug; default Socket.io loses messages in proportion to frequency × disruption duration. +

+

Why CSR's replay tail is multi-second at 10K

+

+ CSR replays per-socket: the server drains a buffered packet list over each re-established WebSocket, one socket at a time. At 10,000 reconnecting clients, replays serialize. AnyCable's history replay is per-stream and parallel — clients request missed ranges and receive them as batches. That's the 7× gap. +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Default Socket.ioSocket.io + CSRAnyCable
Clients10,00010,00010,000
Expected deliveries1,200,0001,200,0001,200,000
Jitter events98,889103,43083,862
CSR resume raten/a99.5%n/a
Deliveries lost150,64200
Delivery rate87.41%100%100%
Latency p50167 ms279 ms246 ms
Latency p951.19 s4.92 s0.68 s
Latency p991.66 s8.99 s1.04 s
Latency max2.29 s12.03 s3.53 s
+
+
+
+
+
+ + {{!-- Use cases / impact --}} +
+
+
+
+

Impact

+

Loss and slow replay break workflows where the next message depends on the previous one

+
+

Lost messages cluster around network events — exactly when the user is watching. CSR recovers them, but 9 seconds late reads as "the app froze." For sequential workloads, both loss and delay break the experience.

+
+
+
+
+
+ Live chat & notifications + Messages disappear during network blips. Users see incomplete conversations with no indication anything is missing. +
+
+ LLM / AI streaming + Responses stream word by word. A mid-sentence disconnect leaves output truncated or garbled — chunks lost, no recovery. +
+
+ Real-time collaboration & presence + Lost operations make documents diverge silently. Cursors jump, presence flickers; users lose trust in what they see. +
+
+ Dashboards & monitoring + Permanent gaps in time-series data. The 200 ms blip during a traffic spike is the data point you needed. +
+
+
+
+
+
+ + {{!-- Standalone server — second pillar --}} +
+
+
+
+

2. Every Socket.io deploy severs every connection. This is architectural.#

+

Socket.io's WebSocket server is your application server

+

+ One Node.js process handles HTTP, business logic, and WebSocket connections — competing for the same event loop. HTTP requests come and go; WebSockets stay open for hours. Restart the process to deploy and every WebSocket terminates with it. +

+

AnyCable's WebSocket server is a separate process

+

+ Anycable-go is a separate Go binary. Your app — Node.js, Laravel, FastAPI, Django, anything that speaks HTTP — broadcasts to it. Deploy your app: the WebSocket layer doesn't restart, the connections don't drop. +

+
+
+
# Socket.io deploy — 5,000 clients
+t=0.0s → deploy restarts server
+t=0.4s → all 5,000 connections dropped
+t=5.0s → p50 clients reconnected
+t=6.0s → p95 clients reconnected
+t=6.8s → 95% recovery (avalanche over)
+t=∞    → 189 clients (3.8%) never reconnect
+
+# AnyCable deploy — any client count
+t=0.0s → deploy restarts app
+t=0.0s → zero connections dropped
+t=0.0s → users don't notice
+
+
+ +
+
+

Benchmark: the reconnection avalanche at 5,000 clients

+

+ We deployed Socket.io to Railway, connected 5,000 clients, then triggered a real railway restart on the Socket.io service while measuring disconnect, reconnect, and tail behavior on the bench client. +

+

+ Every Socket.io deploy is a thundering herd: thousands of clients reconnect simultaneously, competing for TLS handshakes and WebSocket upgrades through the same load balancer. +

+

+ At 5,000 clients: 6.8 seconds of downtime per deploy. Half your users blind for 5 full seconds. 189 (3.8%) never reconnect. +

+

+ At 1,000 clients: 2.7 seconds — shorter, still total. Every connection drops, every deploy. +

+

CSR with the in-memory adapter doesn't help here

+

+ CSR depends on server-side state. The default in-memory adapter loses it on restart — nothing to replay. With Redis Streams or MongoDB the state survives, but the connections are still severed. No protocol can paper over this; it's the cost of co-locating WebSocket server and application server. +

+

+ AnyCable: 0 seconds. Every deploy. The WebSocket server doesn't restart when you ship code. +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Socket.io 1KSocket.io 5KAnyCable
Connections dropped1,0005,0000
Recovery p501,679 ms4,967 ms0 ms
Recovery p952,732 ms5,992 ms0 ms
Never reconnected0189 (3.8%)0
Total downtime2.7 s6.8 s0 s
+
+
+
+
+
+ + {{!-- Connection capacity --}} +
+
+
+
+

3. AnyCable holds 50,000 idle connections on under 2 GB and 0.3 vCPU#

+

+ Same Railway box as the jitter benchmark — Pro tier, 32 vCPU / 32 GB RAM. With nothing flowing, anycable-go held 50,000 idle WebSockets on under 2 GB and ~0.3 vCPU (1% of available compute). ~40 KB per connection in steady state. +

+

+ We hit a test-client ceiling at ~56K — the bench process couldn't open more outbound TCP connections. Anycable-go didn't break a sweat; server CPU stayed near zero throughout. +

+

+ Go's goroutines scale this way: each connection is a few KB of stack and a small struct, multiplexed across OS threads. Socket.io is Node-only — a single event loop per process, fundamentally different concurrency. We didn't push its idle ceiling; the dynamic numbers in Pillar 1 already make the case. +

+
+
+
+ + + + + + + + + + + + + + +
Idle connectionsAnyCable memoryAnyCable CPU
(of 32 vCPU)
1,000280 MB0%
10,000280 MB0%
20,000751 MB1.08% (~0.3 vCPU)
50,0001.98 GB1.08% (~0.3 vCPU)
+
+
+
+
+
+ + {{!-- Migration path for Node.js teams --}} +
+
+
+
+

Migrating from Socket.io: replace the broadcast, keep the app#

+

+ Your Node.js app stays. Swap io.to().emit() for an HTTP POST to AnyCable's broadcast endpoint. Auth, validation, DB access — all unchanged. +

+

+ On the client: socket.io-client@anycable/core. The API is similar (connect, subscribe, receive). +

+

+ Trade-off: one extra process to deploy (anycable-go, a single Go binary). In return, your Node.js app becomes stateless for WebSockets. +

+
+
+
// Before: Socket.io (embedded in your app)
+io.to('chat:42').emit('message', payload);
+
+// After: broadcast to AnyCable via HTTP
+await fetch('http://anycable:8080/_broadcast', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    stream: 'chat:42',
+    data: JSON.stringify(payload),
+  }),
+});
+
+
+
+
+ + {{!-- Feature comparison --}} +
+
+
+
+

What you don't have to build

+

+ Socket.io gives you WebSocket transport and rooms. Everything else — reliability, presence, auth, clustering, monitoring — is weeks of engineering plus months of production hardening. +

+

+ AnyCable ships these as primitives, hardened in production since 2017. +

+
+
+
+ + + + + + + + + + + + + + + + + + + + +
FeatureDefault Socket.ioSocket.io + CSRAnyCable
Reliable deliveryNoYes (opt-in)Yes (default)
Replay latency p99lost9.0 s1.0 s
Survives server restartNoRedis Streams / MongoNATS / Redis
Multi-node setupRedis pub/subRedis pub/sub incompat.Any broker
Deploy resilienceAll dropAll dropConnections survive
Presence trackingDIYDIYBuilt-in
AuthenticationDIYDIYJWT, signed streams
Backend languageNode.js onlyNode.js onlyAny (HTTP API)
MonitoringAdmin UIAdmin UIPrometheus & StatsD
+
+
+
+
+
+ + {{!-- When Socket.io is right / Proven at scale --}} +
+
+
+
+

When Socket.io is the right choice

+

+ Small Node.js app. Prototype. Custom protocol you control end-to-end. Socket.io is well-documented, widely used, and free. +

+

+ If your users are on mobile or switching networks, you need delivery guarantees. If you deploy more than occasionally, you need connections to survive deploys. AnyCable provides both by default. +

+
+
+
+
Proven at scale
+
+
Doximity — telehealth (NYSE-listed)
+
CompanyCam — $2B valuation
+
Headway — $2.3B valuation
+
Jobber — $167M revenue
+
CoinGecko — crypto market data
+
ClickFunnels — ~$265M revenue
+
+
+ And 30+ more since 2017. +
+
+
+
+
+
+ + {{!-- FAQ — accordion. Native
so it works without JS; + closed-by-default keeps the page short for skimmers. --}} +
+
+
+
+

FAQ#

+ +
+ Does AnyCable replace Socket.io, or work alongside it? +
It replaces Socket.io. Your Node.js app broadcasts to AnyCable via HTTP instead of calling io.emit(). Clients connect to AnyCable over its protocol (actioncable-v1-ext-json, via @anycable/core) instead of socket.io-client.
+
+ +
+ What's the operational cost of running AnyCable? +
One extra process — a single Go binary (or Docker container). It's stateless for broadcasts and scales horizontally. No database, no custom build. Defaults work for most apps; Redis or NATS can be added for multi-node pub/sub.
+
+ +
+ How does AnyCable compare on performance? +
In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), a single anycable-go instance held 50,000 idle connections on under 2 GB of memory and ~0.3 vCPU (1% of available compute). At 10,000 dynamic clients with simulated jitter, it delivered all 1.2M messages with a 1-second p99 replay latency. We hit a test-client ceiling before anycable-go's. Run the numbers yourself: github.com/irinanazarova/anycable-socketio-benchmarks.
+
+ +
+ What backends can use AnyCable? +
Any. Anycable-go is a standalone server with an HTTP broadcast API; your app pushes messages over plain HTTP. Node.js, Laravel, FastAPI, Django, Go, Elixir — anything that can issue an HTTP POST works. There is no language SDK requirement.
+
+ +
+ Is AnyCable open source? +
Yes, MIT-licensed since 2017. anycable/anycable on GitHub. A commercial Pro tier adds broker features (long-term history, embedded NATS) and priority support — free for small deployments.
+
+ +
+ Is there a managed (hosted) AnyCable? +
Yes. AnyCable+ is the managed tier — zero ops, free up to 2,000 concurrent connections, paid plans above that. Same protocol and feature surface as self-hosted, so you can switch in either direction without changing app code.
+
+ +
+ How do you handle authentication? +
Signed JWT tokens or signed stream names, issued by your application. AnyCable verifies the signature on connect and channel subscribe — your app stays the source of truth for identity without being on the hot path.
+
+ +
+ What happens if AnyCable itself restarts? +
Clients reconnect and resume from the last offset they saw — missed messages are replayed (up to a configurable retention window). Restarting AnyCable is rare: you deploy your app, not the WebSocket layer, because they're decoupled.
+
+
+
+
+
+ + {{!-- CTA --}} +
+
+

Try AnyCable with your Node.js app

+

+ Open source and free. Managed tier free up to 2,000 connections. Pro is $1,490/year for unlimited. +

+ +

+ AnyCable is open source (MIT), built by Evil Martians, in production since 2017 at Doximity, CompanyCam, Headway, and 30+ others. + Benchmark code: github.com/irinanazarova/anycable-socketio-benchmarks. +

+
+
+ +
+ {{> footer }} +
+ + + + diff --git a/src/index.scss b/src/index.scss index c376e83..0453048 100644 --- a/src/index.scss +++ b/src/index.scss @@ -29,4 +29,5 @@ @import './modules/blocks/blog.scss'; @import './modules/blocks/blog-header.scss'; @import './modules/blocks/demo.scss'; +@import './modules/blocks/compare.scss'; @import './modules/common.scss'; diff --git a/src/modules/blocks/about-slide.scss b/src/modules/blocks/about-slide.scss index 4ff0c09..eab4b0f 100644 --- a/src/modules/blocks/about-slide.scss +++ b/src/modules/blocks/about-slide.scss @@ -51,6 +51,12 @@ $className: 'about-slide'; padding-top: 48px; } } + + // Use when the section has no media partner (e.g. FAQ). + &-full { + width: 100%; + border-right: none; + } } &__media { @@ -76,6 +82,23 @@ $className: 'about-slide'; margin-bottom: 48px; } + // Keeps the illustrating column visible while the reader scrolls through + // long left-column explanations. Pins from the top of the viewport + // (with header offset) and releases when its section ends. + &-sticky { + align-self: flex-start; + position: sticky; + top: 80px; // 60px page header + 20px breathing room + max-height: calc(100vh - 100px); + overflow-y: auto; + + @include mediaMax($tablet) { + position: static; + max-height: none; + overflow: visible; + } + } + @include mediaMax($tablet) { display: none; } @@ -125,6 +148,20 @@ $className: 'about-slide'; } } + // Calm inline code: smaller, faint background, never breaks the rhythm + // of the surrounding sans-serif text. + &__text code, + &__subtitle code, + &__content li code { + font-size: 0.85em; + font-weight: 500; + background: rgba(0, 0, 0, 0.05); + padding: 1px 6px; + border-radius: 4px; + vertical-align: 1px; + white-space: nowrap; + } + &__tabs { display: flex; flex-direction: row; diff --git a/src/modules/blocks/compare.scss b/src/modules/blocks/compare.scss new file mode 100644 index 0000000..f0ecc4f --- /dev/null +++ b/src/modules/blocks/compare.scss @@ -0,0 +1,318 @@ +// ===================================================================== +// Styles for the AnyCable vs Socket.io comparison page (src/compare/socket-io). +// +// All rules are scoped under `.compare-page` (a class on the
element +// of the compare page) so they don't leak into other pages that might use +// `.about-slide`, `.slide-show__frame`, etc. +// ===================================================================== + + +// --------------------------------------------------------------------- +// FAQ accordion — narrow centered column with native
disclosures. +// Closed-by-default; expanding turns the question red and flips +/-. +// --------------------------------------------------------------------- +.faq-block { + max-width: 720px; + margin: 0 auto; + + .faq-item { + border-top: 1px solid #ececec; + padding: 18px 0; + + &:last-of-type { + border-bottom: 1px solid #ececec; + } + + summary { + cursor: pointer; + list-style: none; + font-size: 18px; + font-weight: 600; + line-height: 1.45; + padding-right: 32px; + position: relative; + transition: color 0.15s; + + &::-webkit-details-marker { display: none; } + + // Plus / minus indicator that flips when open. + &::after { + content: '+'; + position: absolute; + right: 0; + top: -2px; + font-size: 22px; + font-weight: 400; + color: #888; + transition: transform 0.2s; + } + + &:hover { color: #F64343; } + } + + &[open] summary::after { content: '−'; color: #F64343; } + } + + .faq-answer { + font-size: 16px; + line-height: 1.7; + color: #444; + padding-top: 12px; + padding-right: 32px; + } +} + + +// --------------------------------------------------------------------- +// Hover-revealed anchor link on H2/H3 headings (Stripe-style # icon). +// Lets readers and agents copy deep links to specific sections. +// --------------------------------------------------------------------- +.about-slide__title, +.about-slide__subtitle { + scroll-margin-top: 80px; // header offset for in-page nav + + a.heading-anchor { + margin-left: 8px; + color: #ccc; + text-decoration: none; + opacity: 0; + transition: opacity 0.15s; + font-weight: 400; + } + + &:hover a.heading-anchor { opacity: 1; } + a.heading-anchor:hover { color: #F64343; } +} + + +// --------------------------------------------------------------------- +// "About this comparison" callout — restrained methodology footnote. +// --------------------------------------------------------------------- +.compare-about { + max-width: 960px; + margin: 56px auto 0; + padding: 20px 24px; + background: #fafafa; + border: 1px solid #ececec; + border-radius: 8px; + font-size: 14px; + line-height: 1.65; + color: #555; + + p { margin: 0; } + p + p { margin-top: 8px; } + + strong { color: #222; } +} + + +// --------------------------------------------------------------------- +// Floating in-page nav: built but disabled. The about-slide pillars are +// full-width 50/50 columns with no gutter, so a fixed-right nav overlaps +// table content. Anchor links above cover deep-linking; keep the markup +// in case we revisit (e.g. as a sticky-top horizontal bar). +// --------------------------------------------------------------------- +.compare-nav { + display: none; +} + + +// --------------------------------------------------------------------- +// Hero stat cards — three side-by-side comparison cards, each a small +// table (label → 3 rows → footnote). One "worst" cell highlighted per +// card so all three results can be scanned in one glance. +// --------------------------------------------------------------------- +.hero-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); + gap: 16px; + max-width: 1200px; + margin: 0 auto; +} + +.hero-card { + border: 1px solid #ececec; + border-radius: 8px; + padding: 24px 24px 20px; + background: white; + + &__label { + font-size: 12px; + font-weight: 600; + color: #888; + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 16px; + } + + &__rows { + margin: 0; + padding: 0; + } + + &__row { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 9px 0; + border-top: 1px solid #f3f3f3; + + &:first-child { border-top: none; } + + dt { + font-size: 14px; + color: #555; + font-weight: 400; + } + + dd { + margin: 0; + font-size: 22px; + font-weight: 600; + font-variant-numeric: tabular-nums; + letter-spacing: -0.01em; + + &.is-worst { color: #F64343; } + &.is-na { color: #bbb; font-weight: 400; } + } + } + + &__footnote { + margin-top: 14px; + padding-top: 12px; + border-top: 1px solid #f3f3f3; + font-size: 12px; + line-height: 1.5; + color: #888; + } +} + + +// --------------------------------------------------------------------- +// Notebook-feel dotted background applied to the whole compare page so +// the pattern is continuous across hero, pillars, FAQ, and CTA. Gray +// panels inside about-slide keep their solid fill on top. +// --------------------------------------------------------------------- +.compare-page { + background-color: white; + background-image: radial-gradient(circle, rgba(0, 0, 0, 0.085) 1px, transparent 1px); + background-size: 22px 22px; +} + +// Slide containers are transparent so the page-level dots show through. +.compare-page .slide { + background: transparent !important; +} + + +// --------------------------------------------------------------------- +// About-slide right column on the compare page: transparent (so page +// dots show through) with rounded white cards around tables. Tables +// read as discrete "notes" pinned to the dotted notebook surface. +// --------------------------------------------------------------------- +.compare-page .about-slide__media { + background-color: transparent !important; + background-image: none !important; +} + +.compare-page .about-slide__media .slide-show__frame { + background-color: white; + border: 1px solid #ececec; + border-radius: 8px; + overflow: hidden; + display: block; // override the flex centering for tables + padding: 0; +} + +// Code blocks (
) — consistent 8px radius. Bg is dark for contrast.
+.compare-page .about-slide__media pre {
+  background-color: #1a1a1a;
+  border-radius: 8px;
+}
+
+
+// ---------------------------------------------------------------------
+// Unified table system. One source of truth for every data table on
+// the comparison page (jitter, avalanche, capacity, feature comparison).
+// ---------------------------------------------------------------------
+.compare-page .slide-show__frame table {
+  width: 100%;
+  background-color: transparent;
+  border-collapse: collapse;
+  font-size: 14px;
+  font-variant-numeric: tabular-nums;
+  color: #1a1a1a;
+
+  // Header row — uppercase eyebrow style; small + light.
+  thead tr {
+    background: transparent !important;
+    border-bottom: 1px solid #e8e8e8;
+  }
+
+  th {
+    padding: 14px 18px;
+    font-size: 11px;
+    font-weight: 600;
+    text-transform: uppercase;
+    letter-spacing: 0.06em;
+    color: #777;
+    text-align: right;
+    white-space: nowrap;
+
+    &:first-child { text-align: left; }
+  }
+
+  // Body rows — single dividing line, not a grid.
+  tbody tr {
+    border-bottom: 1px solid #f0f0f0;
+
+    &:last-child { border-bottom: none; }
+  }
+
+  td {
+    padding: 13px 18px;
+    text-align: right;
+    color: #333;
+    vertical-align: middle;
+
+    &:first-child {
+      text-align: left;
+      color: #555;
+      white-space: nowrap;
+    }
+  }
+
+  // Key rows: heavier weight, no background fill.
+  tr.is-key td,
+  tr.is-key td:first-child {
+    font-weight: 600;
+    color: #111;
+  }
+
+  // "Worst per metric" — single accent. Used sparingly.
+  td.is-worst,
+  td.is-worst strong {
+    color: #F64343;
+  }
+
+  // Dim "n/a" or non-applicable cells.
+  td.is-na,
+  td.is-na strong {
+    color: #bbb;
+    font-weight: 400;
+  }
+
+  // Links inside tables inherit cell color so red stays reserved for the
+  // "worst per metric" semantic; the citation affordance is an underline.
+  td a {
+    color: inherit;
+    text-decoration: underline;
+    text-decoration-color: #d0d0d0;
+    text-underline-offset: 3px;
+
+    &:hover {
+      color: #F64343;
+      text-decoration-color: currentColor;
+    }
+  }
+}

From 5c02581c0900588c00056dd6a4f029c87fffbff9 Mon Sep 17 00:00:00 2001
From: Irina Nazarova 
Date: Fri, 1 May 2026 05:57:59 -0700
Subject: [PATCH 02/57] Self-review cleanup: drop dead floating-nav, fix
 invalid inline bg
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

- Removed the floating-nav 
@@ -141,24 +141,24 @@

AnyCable: at-least-once by default, replay is

Benchmark: 10,000 clients under simulated jitter

- 10,000 clients on Railway. Publisher broadcasts 120 numbered messages at 2/sec — 1.2M expected deliveries. Each client force-closes its TCP socket for 1 second every ~15 seconds (no clean close — like a WiFi drop). Over the publishing window: ~10 jitter events per client, ~13% blind time. + 10,000 clients on Railway. Publisher broadcasts 120 numbered messages at 2/sec — 1.2M expected deliveries. Each client force-closes its TCP socket for 1 second every ~15 seconds (no clean close — like a WiFi drop). Over the test run: ~8 jitter events per client.

- Default Socket.io: 87.41% delivery. 150,642 messages lost. The loss pattern matches the blind window — nothing arrives during the outage, nothing comes back after. + Default Socket.io: ~87% delivery. 156,856 messages lost out of 1.2M. The loss pattern matches the blind window — nothing arrives during the outage, nothing comes back after.

- Socket.io + CSR: 100% delivery, 9-second p99 latency. 99.5% of jitter events successfully resume the session; replayed messages arrive in seconds, worst case 12. + Socket.io + CSR: 100% delivery, ~8-second p99 replay tail. 99.5% of jitter events successfully resume the session; replayed messages arrive in seconds, worst case ~11. Server peak memory: 627 MB.

- AnyCable: 100% delivery, 1-second p99 latency. 7× faster than CSR. The history replay is per-stream — clients receive the missed range as a batch. + AnyCable: 100% delivery, ~6-second p99 replay tail. About 1.7 s ahead of CSR on the tail. Server peak memory: 760 MB on the open-source build, 271 MB on AnyCable Pro — ~2.3× less than CSR holding the same 10K reconnecting fleet.

Why default Socket.io's 13% loss is structural

- Each jitter event creates d seconds of blind window. Clients see N events over publishing window T. Expected loss = (d × N) / T. For our run: 1.3 s × 10 / 60 ≈ 22% upper bound. We measured 13% — lower because not every event fully overlaps publishing. This isn't a configuration bug; default Socket.io loses messages in proportion to frequency × disruption duration. + Each jitter event creates d seconds of blind window. Clients see N events over publishing window T. Expected loss ≈ (d × N) / T. For our run: 1.3 s × ~8 / 60 ≈ 17% upper bound. We measured 13.1% — lower because not every event fully overlaps publishing. This isn't a configuration bug; default Socket.io loses messages in proportion to frequency × disruption duration.

-

Why CSR's replay tail is multi-second at 10K

+

Where the replay tail comes from at 10K

- CSR replays per-socket: the server drains a buffered packet list over each re-established WebSocket, one socket at a time. At 10,000 reconnecting clients, replays serialize. AnyCable's history replay is per-stream and parallel — clients request missed ranges and receive them as batches. That's the 7× gap. + Both clients use a similar reconnect-with-backoff loop (~0.5–5 s with jitter) to avoid stampeding the server, and most of the tail comes from that wait. The remaining gap is the replay protocol itself: CSR drains a buffered packet list per socket — at 10,000 reconnecting clients those drains serialize through one event loop. AnyCable's history replay is per-stream, returns the missed range as a batch, and runs in parallel across streams. That's the ~1.7 s AnyCable saves on the p99.

@@ -187,9 +187,9 @@

Why CSR's replay tai Jitter events - 98,889 - 103,430 - 83,862 + 78,748 + 82,068 + 83,328 CSR resume rate @@ -199,42 +199,49 @@

Why CSR's replay tai Deliveries lost - 150,642 + 156,856 0 0 Delivery rate - 87.41% + ~87% 100% 100% Latency p50 - 167 ms - 279 ms - 246 ms + 140 ms + 230 ms + 260 ms Latency p95 - 1.19 s - 4.92 s - 0.68 s + 1.1 s + 4.0 s + 4.1 s Latency p99 - 1.66 s - 8.99 s - 1.04 s + 1.6 s + 7.9 s + 6.2 s + + + Latency max + 2.1 s + 10.8 s + 9.3 s - Latency max - 2.29 s - 12.03 s - 3.53 s + Server memory peak + 675 MB + 627 MB + 760 / 271 MB +

AnyCable cell shows open-source / AnyCable Pro. Same Railway box (32 vCPU / 32 GB) for all four runs. Server memory pulled from Railway's metrics API for the test window. CPU was negligible across all setups (under 1.1% of the 32 vCPU box).

@@ -249,7 +256,7 @@

Why CSR's replay tai

Impact

Loss and slow replay break workflows where the next message depends on the previous one

-

Lost messages cluster around network events — exactly when the user is watching. CSR recovers them, but 9 seconds late reads as "the app froze." For sequential workloads, both loss and delay break the experience.

+

Lost messages cluster around network events — exactly when the user is watching. CSR recovers them, but 8 seconds late still reads as "the app froze"; AnyCable lands them around 6. For sequential workloads, both loss and delay break the experience.

@@ -606,7 +613,7 @@

What you don't have to build

Reliable deliveryNoYes (opt-in)Yes (default) - Replay latency p99lost9.0 s1.0 s + Replay latency p99lost~8 s~6 s Survives server restartNoRedis Streams / MongoNATS / Redis Multi-node setupRedis pub/subRedis pub/sub incompat.Any broker Deploy resilienceAll dropAll dropConnections survive @@ -676,7 +683,7 @@

FAQ<
How does AnyCable compare on performance? -
In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), we pushed three setups to a 1,000,000-connection idle target on a single instance. Socket.io accepted 119,826 and rejected ~880K — its single Node event loop saturated under handshake load. Open-source AnyCable held 993,994, peaking at the box's 32 GB RAM ceiling (~33 KB / connection). AnyCable Pro held 999,954 on only 19 GB — ~1.7× more memory-efficient than OSS at the same load, with 13 GB headroom remaining. At 10,000 dynamic clients with simulated jitter, the open-source AnyCable build delivered all 1.2M messages with a 1-second p99 replay latency. Run the numbers yourself: github.com/irinanazarova/anycable-socketio-benchmarks.
+
In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), we pushed three setups to a 1,000,000-connection idle target on a single instance. Socket.io accepted 119,826 and rejected ~880K — its single Node event loop saturated under handshake load. Open-source AnyCable held 993,994, peaking at the box's 32 GB RAM ceiling (~33 KB / connection). AnyCable Pro held 999,954 on only 19 GB — ~1.7× more memory-efficient than OSS at the same load, with 13 GB headroom remaining. At 10,000 dynamic clients with simulated jitter (1-second TCP drops every ~15 s), AnyCable delivered all 1.2M messages with a ~6 s p99 replay tail (vs ~8 s for Socket.io+CSR), and AnyCable Pro held the same fleet on 271 MB peak memory — ~2.3× less than CSR. Run the numbers yourself: github.com/irinanazarova/anycable-socketio-benchmarks.
@@ -776,7 +783,7 @@

Drop AnyCable into your Node.js app today

"name": "How does AnyCable compare on performance?", "acceptedAnswer": { "@type": "Answer", - "text": "In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), we pushed three setups to a 1,000,000-connection idle target on a single instance. Socket.io accepted 119,826 and rejected ~880K — its single Node event loop saturated under handshake load. Open-source AnyCable held 993,994, peaking at the box's 32 GB RAM ceiling (~33 KB / connection). AnyCable Pro held 999,954 on only 19 GB — about 1.7× more memory-efficient than OSS at the same load, with 13 GB headroom remaining. At 10,000 dynamic clients with simulated jitter, the open-source AnyCable build delivered all 1.2M messages with a 1-second p99 replay latency. Source: github.com/irinanazarova/anycable-socketio-benchmarks." + "text": "In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), we pushed three setups to a 1,000,000-connection idle target on a single instance. Socket.io accepted 119,826 and rejected ~880K — its single Node event loop saturated under handshake load. Open-source AnyCable held 993,994, peaking at the box's 32 GB RAM ceiling (~33 KB / connection). AnyCable Pro held 999,954 on only 19 GB — about 1.7× more memory-efficient than OSS at the same load, with 13 GB headroom remaining. At 10,000 dynamic clients with simulated jitter, AnyCable delivered all 1.2M messages with a ~6 s p99 replay tail (vs ~8 s for Socket.io+CSR), and AnyCable Pro held the same fleet on 271 MB peak memory — about 2.3× less than CSR. Source: github.com/irinanazarova/anycable-socketio-benchmarks." } }, { @@ -854,7 +861,7 @@

Drop AnyCable into your Node.js app today

"@context": "https://schema.org", "@type": "TechArticle", "headline": "AnyCable vs Socket.io: 100% delivery, deploy-resilient WebSockets, 1M idle connections per node", - "description": "Same Railway box, same 1,000,000-connection idle test: Socket.io accepted 119,826 (single Node event loop saturated), open-source AnyCable held 993,994 (32 GB RAM ceiling), AnyCable Pro held 999,954 on 19 GB. Plus AnyCable delivers 100% under jitter (vs Socket.io's 87.4%) with a ~1 s p99 replay tail and connections survive deploys. All numbers reproducible from the open-source bench repo.", + "description": "Same Railway box, same 1,000,000-connection idle test: Socket.io accepted 119,826 (single Node event loop saturated), open-source AnyCable held 993,994 (32 GB RAM ceiling), AnyCable Pro held 999,954 on 19 GB. Plus AnyCable delivers 100% under jitter (vs Socket.io's 87%) with a ~6 s p99 replay tail (vs ~8 s for CSR), AnyCable Pro holds the same 10K reconnecting fleet on 271 MB (~2.3× less than CSR), and connections survive deploys. All numbers reproducible from the open-source bench repo.", "url": "https://anycable.io/compare/socket-io", "mainEntityOfPage": "https://anycable.io/compare/socket-io", "datePublished": "2026-04-30", From 4a52a517a8595fcf775719a26e6ede0eca3b70a0 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 6 May 2026 09:51:32 -0700 Subject: [PATCH 26/57] Pillar 2: surface the avalanche scaling cliff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous benchmark sub-section showed 1K vs 5K downtime on a single box (a single architectural data point). With Vladimir's listener- attach fix landed, we re-ran avalanche scaling on a constrained box (1 vCPU / 0.5 GB) — the realistic small-tier case where the cliff shows up. Replaces the 1K/5K table with a 5K → 25K scaling table: - 5K: 4.5 s recovery, 100% reconnect - 10K: 3.9 s, 100% - 15K: 5.8 s, 98.5% (224 lost) - 20K: 8.0 s, 96.2% (753 lost) - 25K: never recovers — 0% reconnect, all 25K permanently lost The 25K row is the architectural cliff: memory hits ~95% of the cap just before redeploy, then the post-redeploy reconnect storm OOMs the new container. Confirmed on a clean isolated re-run. Updates the inline timeline illustration to show both the graceful case (5K) and the cliff (25K) on the same box — the contrast is the point. AnyCable column reads "0 s" at every scale because the WebSocket layer doesn't restart when the app does. Section title: "the avalanche scales — until it doesn't". --- src/compare/socket-io/index.html | 79 ++++++++++++++++---------------- 1 file changed, 40 insertions(+), 39 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index d615063..bbe6aac 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -352,15 +352,18 @@

AnyCable's WebSocket server is a separate proc

-
# Socket.io deploy — 5,000 clients
+                
# Socket.io deploy — 5,000 clients (1 vCPU / 0.5 GB box)
 t=0.0s → deploy restarts server
-t=0.4s → all 5,000 connections dropped
-t=5.0s → p50 clients reconnected
-t=6.0s → p95 clients reconnected
-t=6.8s → 95% recovery (avalanche over)
-t=∞    → 189 clients (3.8%) never reconnect
+t=0.5s → all 5,000 connections dropped
+t=2.6s → p50 reconnected
+t=4.5s → 95% recovery (avalanche over)
 
-# AnyCable deploy — any client count
+# Socket.io deploy — 25,000 clients (same box) — the cliff
+t=0.0s   → deploy restarts server
+t=0.5s   → all 25,000 connections dropped
+t=300s+  → 0 reconnected — box can't absorb the storm
+
+# AnyCable deploy — any client count, any box
 t=0.0s → deploy restarts app
 t=0.0s → zero connections dropped
 t=0.0s → users don't notice
@@ -369,71 +372,69 @@

AnyCable's WebSocket server is a separate proc
-

Benchmark: the reconnection avalanche at 5,000 clients

-

- We deployed Socket.io to Railway, connected 5,000 clients, then triggered a real railway restart on the Socket.io service while measuring disconnect, reconnect, and tail behavior on the bench client. -

+

Benchmark: the avalanche scales — until it doesn't

- Every Socket.io deploy is a thundering herd: thousands of clients reconnect simultaneously, competing for TLS handshakes and WebSocket upgrades through the same load balancer. + We deployed Socket.io to a 1 vCPU / 0.5 GB Railway box (typical small/starter tier), connected N clients, then triggered a real railway redeploy. Every Socket.io deploy is a thundering herd: every connection drops, every client races back through the same load balancer.

- At 5,000 clients: 6.8 seconds of downtime per deploy. Half your users blind for 5 full seconds. 189 (3.8%) never reconnect. + Through 20,000 clients, the box absorbs the storm with degrading-but-graceful recovery. At 25,000 clients, recovery never completes. Memory hits 95% of the cap (~489 MB on a 512 MB box) just before the redeploy, and the post-redeploy reconnect storm OOMs the new container before any clients establish session. 0% reconnect rate — every connection is permanently lost.

- At 1,000 clients: 2.7 seconds — shorter, still total. Every connection drops, every deploy. + This is the architectural cost of co-locating the WebSocket layer with your app server: deploy resilience is bounded by what your application box can absorb during its weakest moment — the cold start.

CSR with the in-memory adapter doesn't help here

CSR depends on server-side state. The default in-memory adapter loses it on restart — nothing to replay. With Redis Streams or MongoDB the state survives, but the connections are still severed. No protocol can paper over this; it's the cost of co-locating WebSocket server and application server.

- AnyCable: 0 seconds. Every deploy. The WebSocket server doesn't restart when you ship code. + AnyCable: 0 seconds, every deploy, every scale. The WebSocket server is a separate Go binary; deploying your app doesn't touch it, so there's no avalanche to absorb.

- +
- - - + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + + - - - - + + + +
Socket.io 1KSocket.io 5KClientsSocket.io recoveryReconnected AnyCable
Connections dropped1,0005,00005,0004.5 s100%0 s
Recovery p501,679 ms4,967 ms0 ms10,0003.9 s100%0 s
Recovery p952,732 ms5,992 ms0 ms15,0005.8 s98.5% (224 lost)0 s
Never reconnected0189 (3.8%)020,0008.0 s96.2% (753 lost)0 s
Total downtime2.7 s6.8 s
25,000never0% (all 25K lost) 0 s
+

1 vCPU / 0.5 GB Railway box. "Recovery" = wall-clock time until 95% of clients reconnect. AnyCable's WebSocket layer is a separate Go binary, so app deploys don't restart it — no avalanche to recover from at any scale.

From a5621e7f34afc8fe892cb3dcf55aa527cf81f958 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 6 May 2026 09:58:26 -0700 Subject: [PATCH 27/57] Compare page: tightening pass + reframe replay-tail rationale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two passes on the prose: 1. Tightening — read each paragraph, kept the nuance, removed any word the meaning didn't lean on. Hero subtitle, TL;DR bullets, every per-protocol bullet in Pillar 1, the structural-loss math paragraph, Pillar 2 architecture text, the avalanche-cliff narrative, Pillar 3 capacity prose. ~10–25% shorter per paragraph; same numbers, same nuance, less reader-time spent. 2. New section in Pillar 1: "Why a 1-second blip becomes a multi-second tail." Surfaces what the trace data showed — terminate → cable connect is p99 ~8 s, dominated entirely by the 0.5–5 s reconnect backoff that every realtime client uses to avoid stampeding the server. Implication, made explicit: any TCP-level disruption fans out to multiple seconds of offline window by design — without delivery guarantees those seconds are lost messages, with replay they're delayed messages. Same backoff cost, completely different user experience. Replaces the previous "Where the replay tail comes from at 10K" subsection which only described the protocol gap (~1.7 s CSR/AC delta). The protocol-gap explanation is preserved as a second paragraph so the reader still sees both pieces. --- src/compare/socket-io/index.html | 55 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index bbe6aac..20fd326 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -63,11 +63,11 @@

delivery-guarantees doc: "if the connection is broken while an event is being sent, then there is no guarantee that the other side has received it." No buffering, no catch-up protocol. The default — and what most production Socket.io apps ship.

Socket.io + CSR: opt-in catch-up with adapter and replay constraints

- Connection State Recovery (Socket.io 4.6+) buffers per-socket state — id, rooms, socket.data — for up to maxDisconnectionDuration (default 2 min). The server appends an opaque per-packet offset; on reconnect the client passes pid + last-seen offset, and the server replays missed packets. + Connection State Recovery (Socket.io 4.6+) buffers per-socket state — id, rooms, socket.data — for up to maxDisconnectionDuration (default 2 min). On reconnect, the client passes its pid + last-seen offset; the server replays the missed packets.

Four caveats from the Socket.io docs: @@ -109,10 +109,10 @@

Socket.io + CSR: opt-in catch-up with adapter

AnyCable: at-least-once by default, replay is per-stream

- AnyCable's protocol (actioncable-v1-ext-json) tracks each stream by epoch (broker state identifier) and offset (message position). The client stores (stream, epoch, offset). On reconnect it sends a history command; the server replays the missed range as a batch and acks with confirm_history (or reject_history if evicted). + AnyCable's protocol (actioncable-v1-ext-json) tracks each stream by epoch + offset. On reconnect the client sends a history command; the server replays the missed range as a batch and acks with confirm_history (or reject_history if evicted).

- Retention is per stream: history_limit (default 100), history_ttl (default 300 s), sessions_ttl (separate). With NATS JetStream or Redis brokers, history survives restart and works across nodes. None of this is opt-in or experimental. + Retention is per stream: history_limit (default 100), history_ttl (default 300 s). With NATS JetStream or Redis brokers, history survives restart and works across nodes. Default — not opt-in, not experimental.

@@ -141,24 +141,27 @@

AnyCable: at-least-once by default, replay is

Benchmark: 10,000 clients under simulated jitter

- 10,000 clients on Railway. Publisher broadcasts 120 numbered messages at 2/sec — 1.2M expected deliveries. Each client force-closes its TCP socket for 1 second every ~15 seconds (no clean close — like a WiFi drop). Over the test run: ~8 jitter events per client. + 10,000 clients on Railway. Publisher sends 120 numbered messages at 2/sec — 1.2M expected deliveries. Each client force-closes its TCP socket for 1 s every ~15 s (no clean close — like a WiFi drop). ~8 jitter events per client.

- Default Socket.io: ~87% delivery. 156,856 messages lost out of 1.2M. The loss pattern matches the blind window — nothing arrives during the outage, nothing comes back after. + Default Socket.io: ~87% delivery. 156,856 messages lost. Loss matches the blind window — nothing arrives during the outage, nothing comes back.

- Socket.io + CSR: 100% delivery, ~8-second p99 replay tail. 99.5% of jitter events successfully resume the session; replayed messages arrive in seconds, worst case ~11. Server peak memory: 627 MB. + Socket.io + CSR: 100% delivery, ~8-second p99 replay tail. 99.5% of events resume the session; worst case ~11 s. Server peak: 627 MB.

- AnyCable: 100% delivery, ~6-second p99 replay tail. About 1.7 s ahead of CSR on the tail. Server peak memory: 760 MB on the open-source build, 271 MB on AnyCable Pro — ~2.3× less than CSR holding the same 10K reconnecting fleet. + AnyCable: 100% delivery, ~6-second p99 replay tail. ~1.7 s ahead of CSR. Server peak: 760 MB OSS, 271 MB Pro — ~2.3× less than CSR for the same 10K fleet.

Why default Socket.io's 13% loss is structural

- Each jitter event creates d seconds of blind window. Clients see N events over publishing window T. Expected loss ≈ (d × N) / T. For our run: 1.3 s × ~8 / 60 ≈ 17% upper bound. We measured 13.1% — lower because not every event fully overlaps publishing. This isn't a configuration bug; default Socket.io loses messages in proportion to frequency × disruption duration. + Each jitter event creates d seconds of blind window. Over publishing window T with N events, expected loss ≈ (d × N) / T. For our run: 1.3 s × ~8 / 60 ≈ 17% upper bound. We measured 13.1% — lower because not every event fully overlaps publishing. Not a configuration bug; default Socket.io loses messages in proportion to frequency × disruption.

-

Where the replay tail comes from at 10K

+

Why a 1-second blip becomes a multi-second tail

- Both clients use a similar reconnect-with-backoff loop (~0.5–5 s with jitter) to avoid stampeding the server, and most of the tail comes from that wait. The remaining gap is the replay protocol itself: CSR drains a buffered packet list per socket — at 10,000 reconnecting clients those drains serialize through one event loop. AnyCable's history replay is per-stream, returns the missed range as a batch, and runs in parallel across streams. That's the ~1.7 s AnyCable saves on the p99. + Every realtime client — socket.io-client, @anycable/core, anything that wants to stay up under load — reconnects with a 0.5–5 s backoff window to avoid stampeding the server after a disruption. The implication: any TCP-level blip becomes multiple seconds of offline window, by design. Without delivery guarantees, those seconds are lost messages. With replay, they're delayed messages — same backoff cost, completely different user experience. +

+

+ The remaining gap (CSR ~8 s, AnyCable ~6 s) is the replay protocol itself: CSR drains a buffered packet list per socket — at 10K reconnecting clients those drains serialize through one event loop. AnyCable's history replay is per-stream, returns the missed range as a batch, parallel across streams. That's the ~1.7 s on the p99.

@@ -344,11 +347,11 @@

2. Every Socket.io deploy severs eve

Socket.io's WebSocket server is your application server

- One Node.js process handles HTTP, business logic, and WebSocket connections — competing for the same event loop. HTTP requests come and go; WebSockets stay open for hours. Restart the process to deploy and every WebSocket terminates with it. + One Node.js process handles HTTP, business logic, and WebSockets — sharing one event loop. HTTP requests come and go; WebSockets stay open for hours. Restart the process to deploy and every WebSocket dies with it.

AnyCable's WebSocket server is a separate process

- Anycable-go is a separate Go binary. Your app — Node.js, Laravel, FastAPI, Django, anything that speaks HTTP — broadcasts to it. Deploy your app: the WebSocket layer doesn't restart, the connections don't drop. + Anycable-go is a separate Go binary. Your app — Node.js, Laravel, FastAPI, Django, anything that speaks HTTP — broadcasts to it. Deploy your app; the WebSocket layer stays up.

@@ -374,20 +377,20 @@

AnyCable's WebSocket server is a separate proc

Benchmark: the avalanche scales — until it doesn't

- We deployed Socket.io to a 1 vCPU / 0.5 GB Railway box (typical small/starter tier), connected N clients, then triggered a real railway redeploy. Every Socket.io deploy is a thundering herd: every connection drops, every client races back through the same load balancer. + We deployed Socket.io to a 1 vCPU / 0.5 GB Railway box (typical starter tier), connected N clients, then triggered a real railway redeploy. Every Socket.io deploy is a thundering herd: every connection drops, every client races back through the same load balancer.

- Through 20,000 clients, the box absorbs the storm with degrading-but-graceful recovery. At 25,000 clients, recovery never completes. Memory hits 95% of the cap (~489 MB on a 512 MB box) just before the redeploy, and the post-redeploy reconnect storm OOMs the new container before any clients establish session. 0% reconnect rate — every connection is permanently lost. + Through 20K clients, the box absorbs the storm with degrading-but-graceful recovery. At 25K, recovery never completes. Memory hits 95% of the cap (~489 MB on a 512 MB box) just before redeploy; the post-redeploy reconnect storm OOMs the new container before any client establishes a session. 0% reconnect rate — every connection permanently lost.

- This is the architectural cost of co-locating the WebSocket layer with your app server: deploy resilience is bounded by what your application box can absorb during its weakest moment — the cold start. + The architectural cost of co-locating WebSocket and app: deploy resilience is bounded by what the box absorbs during its weakest moment — cold start.

CSR with the in-memory adapter doesn't help here

- CSR depends on server-side state. The default in-memory adapter loses it on restart — nothing to replay. With Redis Streams or MongoDB the state survives, but the connections are still severed. No protocol can paper over this; it's the cost of co-locating WebSocket server and application server. + CSR depends on server-side state. The in-memory adapter loses it on restart — nothing to replay. Redis Streams or MongoDB persist the state, but the connections still drop. No protocol papers over architecture.

- AnyCable: 0 seconds, every deploy, every scale. The WebSocket server is a separate Go binary; deploying your app doesn't touch it, so there's no avalanche to absorb. + AnyCable: 0 seconds, every deploy, every scale. The WebSocket server is a separate Go binary — deploying your app doesn't touch it. No avalanche to absorb.

@@ -448,13 +451,13 @@

CSR with the in-memo

3. One AnyCable node holds 1,000,000 idle connections — Pro on 19 GB, OSS on 32 GB#

- Same Railway box as the jitter benchmark — Pro tier, 32 vCPU / 32 GB, single instance, no Redis or NATS backplane. We aimed all three runtimes at a 1,000,000-connection idle target across 25 sharded test clients (each container has its own ~64K outbound-port pool). The numbers in the table at right are what each held at peak. + Same Railway box as the jitter benchmark — Pro tier, 32 vCPU / 32 GB, single instance, no Redis or NATS backplane. All three runtimes targeted 1,000,000 idle connections across 25 sharded test clients (each container has its own ~64K outbound-port pool). The table shows what each held at peak.

- Each runtime hits a different wall. Socket.io accepted 119,826 and rejected ~880K during ramp — the single Node event loop saturates processing handshakes serially, regardless of how much memory is on the box. Open-source AnyCable held 993,994, capping at the physical 32 GB RAM ceiling at ~33 KB / connection. More connections from there means a bigger box. + Each runtime hits a different wall. Socket.io accepted 119,826 and rejected ~880K during ramp — the single Node event loop saturates handshakes serially, regardless of memory. Open-source AnyCable held 993,994, capping at the box's 32 GB ceiling (~33 KB / connection). More connections from there means a bigger box.

- AnyCable Pro held 999,954 on 19 GB with 13 GB headroom remaining — about 1.7× more memory-efficient than OSS at 1M (~19 KB vs ~33 KB / connection). The same gap shows up at smaller scale: at 200K, Pro uses 3.5 GB vs OSS's 8.3 GB. None of this is the headline of the page; the page's headline is delivery and deploy resilience above. But the connection ceiling — when you do go looking — is comfortably past a million on a single instance. + AnyCable Pro held 999,954 on 19 GB with 13 GB headroom — about 1.7× more memory-efficient than OSS at 1M (~19 vs ~33 KB / connection). Same gap at smaller scale: 200K is 3.5 GB vs OSS's 8.3 GB. The headline of this page is delivery and deploy resilience above; the connection ceiling — when you go looking — is comfortably past a million on a single node.

From aabc27f963719f0555462f7f27a1d502550c5044 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 6 May 2026 10:02:02 -0700 Subject: [PATCH 28/57] =?UTF-8?q?Compare=20page:=20finish=20tightening=20?= =?UTF-8?q?=E2=80=94=20Impact,=20Try-it,=20Migration,=20capacity=20footnot?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rest of the prose pass: - Impact section: trimmed redundant words ("still"/"the experience" → "the flow"). - Live-chat use-case card: comma instead of "with no" preposition. - Try-it intro: replaced "depending on whether you want to" with a colon. - Feature comparison header: dropped "production" before "hardening" (implicit). - "When Socket.io is right" closing line: "AnyCable provides both by default" → "AnyCable: both by default." - Migration trade-off: removed "to deploy"/"a single"/"In return" filler. - Capacity hero card footnote: dropped doubled "box" and trailing "remaining". No information loss; same numbers, same nuance. The page now reads at the density the rest of it does. --- src/compare/socket-io/index.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 20fd326..2689125 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -53,7 +53,7 @@

Why a 1-second blip

Impact

Loss and slow replay break workflows where the next message depends on the previous one

-

Lost messages cluster around network events — exactly when the user is watching. CSR recovers them, but 8 seconds late still reads as "the app froze"; AnyCable lands them around 6. For sequential workloads, both loss and delay break the experience.

+

Lost messages cluster around network events — exactly when the user is watching. CSR recovers them, but 8 seconds late reads as "the app froze"; AnyCable lands them around 6. For sequential workloads, loss and delay both break the flow.

Live chat & notifications - Messages disappear during network blips. Users see incomplete conversations with no indication anything is missing. + Messages disappear during network blips. Users see incomplete conversations, no indication anything is missing.
LLM / AI streaming @@ -532,7 +532,7 @@

Migrating from Socket.io: replace On the client: socket.io-client@anycable/core. The API is similar (connect, subscribe, receive).

- Trade-off: one extra process to deploy (anycable-go, a single Go binary). In return, your Node.js app becomes stateless for WebSockets. + Trade-off: one extra process (anycable-go, a Go binary). Your Node.js app becomes stateless for WebSockets.

Migration checklist — what changes in your code:

    @@ -567,7 +567,7 @@

    Migrating from Socket.io: replace

    Try it in your stack#

    - Three places to start, depending on whether you want to read code, run a working demo, or wire it into a serverless setup. + Three places to start: read the code, run a working demo, or wire it into a serverless setup.

    @@ -598,7 +598,7 @@

    Try it i

    What you don't have to build

    - Socket.io gives you WebSocket transport and rooms. Everything else — reliability, presence, auth, clustering, monitoring — is weeks of engineering plus months of production hardening. + Socket.io gives you WebSocket transport and rooms. Everything else — reliability, presence, auth, clustering, monitoring — is weeks of engineering plus months of hardening.

    AnyCable ships these as primitives, hardened in production since 2017. @@ -643,7 +643,7 @@

    When Socket.io is the right choice

    Small Node.js app. Prototype. Custom protocol you control end-to-end. Socket.io is well-documented, widely used, and free.

    - If your users are on mobile or switching networks, you need delivery guarantees. If you deploy more than occasionally, you need connections to survive deploys. AnyCable provides both by default. + If your users are on mobile or switching networks, you need delivery guarantees. If you deploy more than occasionally, you need connections to survive deploys. AnyCable: both by default.

    From 7bd4a98c273142ec5292767c66097490dcda3d3f Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Thu, 7 May 2026 13:09:46 -0700 Subject: [PATCH 29/57] Compare page: bring data column back on tablet/mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Right-column tables, code samples, scaling table, and feature matrix were hidden below 1024px (about-slide__media: display:none), so mobile and tablet visitors were seeing only the prose — roughly half the page. Scoped under .compare-page so the homepage's existing mobile behavior is unchanged. - Section flips to flex-direction: column at ≤tablet so media stacks below content; media regains display:flex with full width - slide-show__frame switches from overflow:hidden to overflow-x:auto on mobile so wide tables scroll horizontally inside the rounded card; same on
     blocks for code samples
    - compare-try-it-grid + compare-bench-chart get min-width:0 + width
      100% to break out of the flex-min-content trap
    - inline  tokens (io.to(room).emit(...)) wrap on mobile via
      white-space:normal + overflow-wrap:anywhere
    
    Verified clean at 375 / 580 / 688 / 768 / 820 / 1023 / 1024 / 1280.
    ---
     src/modules/blocks/about-slide.scss |  8 +++
     src/modules/blocks/compare.scss     | 82 +++++++++++++++++++++++++++++
     2 files changed, 90 insertions(+)
    
    diff --git a/src/modules/blocks/about-slide.scss b/src/modules/blocks/about-slide.scss
    index eab4b0f..25f7917 100644
    --- a/src/modules/blocks/about-slide.scss
    +++ b/src/modules/blocks/about-slide.scss
    @@ -160,6 +160,14 @@ $className: 'about-slide';
         border-radius: 4px;
         vertical-align: 1px;
         white-space: nowrap;
    +
    +    // On narrow viewports, a long `io.to(room).emit(event, payload)`
    +    // chunk overflows the line. Allow breaking inside the code token
    +    // rather than letting it push past the viewport edge.
    +    @include mediaMax($mobile) {
    +      white-space: normal;
    +      overflow-wrap: anywhere;
    +    }
       }
     
       &__tabs {
    diff --git a/src/modules/blocks/compare.scss b/src/modules/blocks/compare.scss
    index 088ade1..668a41e 100644
    --- a/src/modules/blocks/compare.scss
    +++ b/src/modules/blocks/compare.scss
    @@ -81,6 +81,57 @@ $compare-header-offset:  80px; // header height + a touch of breathing
       }
     
     
    +  // -------------------------------------------------------------------
    +  // Mobile + tablet: bring the right column (data, tables, code, charts)
    +  // back into view. The global about-slide hides .about-slide__media on
    +  // any screen ≤1023px; that's intentional for marketing pages where the
    +  // media is decorative, but on this comparison page the media IS the
    +  // argument — the benchmark tables, scaling tables, code samples, and
    +  // feature matrix all live there. Without this override, mobile and
    +  // tablet users see only the prose, which is roughly half the page.
    +  //
    +  // Two coordinated changes:
    +  //   1. Section flips to flex-direction: column so the media stacks
    +  //      below the content instead of trying to share a row.
    +  //   2. Media gets display:flex back, full width, and its own bottom
    +  //      padding (the column-stack gap is provided by content's bottom
    +  //      padding + media's lack of top padding so they read as one
    +  //      visual unit).
    +  //
    +  // Inside the media, tables and 
     blocks may be wider than the
    +  // narrow viewport; we switch the rounded-frame's overflow from hidden
    +  // to auto so the data scrolls horizontally rather than getting cropped.
    +  @include mediaMax($tablet) {
    +    .about-slide__section {
    +      flex-direction: column;
    +    }
    +
    +    .about-slide__media {
    +      display: flex;
    +      width: 100%;
    +      padding: 0 0 48px;
    +    }
    +
    +    // overflow-x:auto for the frame and pre blocks lives in their own
    +    // rules below — placing it here would source-order-lose to the
    +    // later `overflow: hidden` shorthand on .slide-show__frame.
    +  }
    +
    +
    +  // -------------------------------------------------------------------
    +  // Mobile: collapse multi-column grids to a single column. The
    +  // try-it-grid uses auto-fit at 280px minmax which technically already
    +  // reflows to one column on narrow widths, but its internal cards
    +  // contain pre-formatted content that nudges them past 280px; force
    +  // a single column under $mobile so we don't overflow.
    +  // -------------------------------------------------------------------
    +  @include mediaMax($mobile) {
    +    .compare-try-it-grid {
    +      grid-template-columns: 1fr;
    +    }
    +  }
    +
    +
       // -------------------------------------------------------------------
       // Heading anchor (Stripe-style hover-revealed # icon)
       // -------------------------------------------------------------------
    @@ -273,11 +324,29 @@ $compare-header-offset:  80px; // header height + a touch of breathing
           padding: 40px 32px;
           background-color: $compare-bg-soft;
         }
    +
    +    // On mobile, tables inside this card can exceed the viewport width.
    +    // Switch from clip-and-crop to horizontal scroll so the data stays
    +    // readable. The rounded card still visually contains the data — the
    +    // scroll happens inside the border, not under the rest of the page.
    +    @include mediaMax($tablet) {
    +      overflow-x: auto;
    +      overflow-y: hidden;
    +      -webkit-overflow-scrolling: touch;
    +    }
       }
     
       .about-slide__media pre {
         background-color: $compare-bg-code;
         border-radius: $compare-radius;
    +
    +    // 
     defaults to white-space: pre, which keeps lines from
    +    // wrapping. On mobile that means long lines push past the viewport.
    +    // overflow-x: auto lets the user scroll the code block instead.
    +    @include mediaMax($tablet) {
    +      overflow-x: auto;
    +      -webkit-overflow-scrolling: touch;
    +    }
       }
     
     
    @@ -301,6 +370,12 @@ $compare-header-offset:  80px; // header height + a touch of breathing
       // -------------------------------------------------------------------
       .compare-bench-chart {
         margin: 16px 0 0;
    +    // Same flex-column-min-content trap as the try-it grid: the inner
    +    // 
     has white-space: pre and long lines, giving the figure a
    +    // very wide min-content. Without these, the figure pushes its flex
    +    // parent past the viewport width on mobile.
    +    min-width: 0;
    +    max-width: 100%;
     
         figcaption {
           font-size: 12px;
    @@ -335,6 +410,13 @@ $compare-header-offset:  80px; // header height + a touch of breathing
         gap: 16px;
         max-width: 1100px;
         margin: 0 auto;
    +    // The grid lives inside a flex column (about-slide__content-full).
    +    // Flex items default to min-width: auto = min-content, so grid items
    +    // with long unbreakable tokens can push the grid past its parent's
    +    // width. min-width: 0 lets the grid shrink, and width: 100% pins it
    +    // to the parent's content box.
    +    min-width: 0;
    +    width: 100%;
       }
     
     
    
    From 3233c677bec502efc64de577494e313afa0e78c8 Mon Sep 17 00:00:00 2001
    From: Irina Nazarova 
    Date: Thu, 7 May 2026 20:02:57 -0700
    Subject: [PATCH 30/57] Compare page: add uWebSockets.js section
    MIME-Version: 1.0
    Content-Type: text/plain; charset=UTF-8
    Content-Transfer-Encoding: 8bit
    
    Addresses the "Socket.io is old, just use uWS" pushback we hear from
    founders comparing AnyCable to Socket.io. The "10× faster" claim is
    genuinely true on the wire (server memory, replay latency, idle
    capacity), so we acknowledge it head-on with measured numbers, then
    show where the wire isn't the bottleneck.
    
    New section between Pillar 1 and the Impact slide:
    
    - Lead acknowledges the wire-speed claim and presents the measured
      wins: 72 MB at 10K, sub-second p99 latency, 1.018M idle on 5.45 GB
      (5.35 KB/conn — beats AnyCable Pro's 19 KB/conn at the same scale).
    - Comparison table at 10K under jitter (5 columns: uWS, Socket.io,
      AnyCable OSS, AnyCable Pro) — splits OSS/Pro per the prior request.
    - "But the wire is rarely the bottleneck" subsection: replay-less
      uWS loses ~14% — same loss profile as Socket.io without CSR.
    - Avalanche scaling table on the same 0.5 GB / 1 vCPU box used for
      Pillar 2's Socket.io cliff: side-by-side ladder shows uWS doubles
      the clean-recovery threshold (10K → 20K) and pushes the survivable
      cliff from 25K to ~90K, but at the high end of uWS's capacity the
      reconnect storm itself becomes the user experience (151s p50 at 50K).
    - Closing point: uWS solves "wire is too heavy"; AnyCable solves
      "delivery during disruption" + "deploy resilience" — different
      layers of the stack.
    
    Also adds the section to the TL;DR on-page nav so someone scanning
    the page can jump straight to the uWS comparison.
    ---
     src/compare/socket-io/index.html | 180 +++++++++++++++++++++++++++++++
     1 file changed, 180 insertions(+)
    
    diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html
    index 2689125..5d30a04 100644
    --- a/src/compare/socket-io/index.html
    +++ b/src/compare/socket-io/index.html
    @@ -71,6 +71,7 @@ 

    TL;DR On this page Delivery + uWebSockets? Deploys Capacity Migration @@ -251,6 +252,185 @@

    Why a 1-second blip

    + {{!-- What about uWebSockets.js? — addresses the "Socket.io is old, just use uWS" + pushback we hear from founders comparing AnyCable to Socket.io. The + "10× faster" claim is genuinely true on the wire (memory + latency + + idle capacity), so we acknowledge that head-on with measured numbers, + then show where the wire isn't the bottleneck (replay-less = lossy on + jitter, single-process = same deploy problem). --}} +
    +
    +
    +
    +

    What about uWebSockets.js?#

    +

    + A common pushback from founders evaluating AnyCable: "Socket.io is old — uWebSockets.js is 10× faster, just use that." The wire-speed claim is genuinely true. We measured it on the same Railway hardware as everything else on this page. +

    +

    On the wire, uWS wins decisively

    +

    + At 10K reconnecting clients, uWS uses 72 MB of server memory — about 4× less than AnyCable Pro and 9× less than Socket.io+CSR. Replay latency p99 is 993 ms, the lowest of any setup we tested. And on a 32 vCPU / 32 GB box held idle to 1,018,366 connections on just 5.45 GB — a 5.35 KB-per-connection footprint that beats AnyCable Pro's already-impressive 19 KB at the same scale. +

    +

    + uWS is the most efficient WebSocket library you can run inside a Node.js app. The "10× faster than Socket.io" headline isn't marketing — it's reproducible. +

    +

    But the wire is rarely the bottleneck

    +

    + Under jitter, uWS loses ~14% of messages — almost identical to Socket.io's ~13% loss without CSR. The faster wire doesn't change the math: 1-second blip × ~8 events / 60 s of publishing ≈ 13% blind window. Wire speed determines how fast the messages that do arrive land. The ones missed during the offline window are still gone. +

    +

    + To deliver every message on uWS, you'd buffer broadcasts per stream, track per-client offsets, replay on reconnect, and absorb reconnect storms — basically reimplement AnyCable's broker by hand. uWS is a WebSocket library; AnyCable is a delivery framework. Different layer of the stack. +

    +

    Reconnect storms scale with N — regardless of library

    +

    + uWS pushes the avalanche cliff far past Socket.io's, but the user-facing recovery time grows non-linearly. Both libraries hit a "real avalanche" point where every broadcast is lost for minutes — uWS just at a higher N. +

    +

    + Two thresholds emerge from the data. Clean-recovery (avalanche resolves in seconds — your operational headroom): Socket.io ~10K, uWS ~20K. Survivable cliff (server doesn't OOM): Socket.io 25K, uWS ~90K. uWS doubles your operating headroom, roughly quadruples your catastrophic-failure ceiling. Beyond clean-recovery, the storm becomes the user experience: at 50K on uWS, half the fleet waits 2.5 minutes before reconnecting, every broadcast during that window lost. +

    +

    + And the deploy problem stays the same. Whether the embedded library is Socket.io or uWS, your app and your WebSocket layer are the same Node process — every deploy still kills every connection, and the new container has to absorb a reconnect storm during cold-start. AnyCable's separate-process architecture skips the avalanche entirely: anycable-go doesn't restart on app deploys, connections persist, replay resumes whatever was missed. +

    +

    Different problems, different solutions

    +

    + uWS is the right answer to "Socket.io's wire is too heavy." AnyCable is the right answer to "we lose messages during network blips" or "every deploy hits our users." Both are honest improvements over default Socket.io — they just sit at different layers of the stack. +

    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    10,000 clients under jitteruWSSocket.ioAnyCable OSSAnyCable Pro
    Server memory peak72 MB675 MB760 MB271 MB
    Replay latency p99993 msno replay6.2 s6.2 s
    Delivery rate86.4%~87%100%100%
    Messages lost163,371156,85600
    +

    Same 32 vCPU / 32 GB Railway box. uWS reconnect uses 2–5 s exponential backoff matched to socket.io-client's defaults. Comparing uWS vs default Socket.io on delivery: nearly identical — both at-most-once, no replay, lose during the same blind windows. Latency is identical between AnyCable OSS and Pro (same protocol, same broker); memory differs because Pro's broker is more compact.

    +
    +
    +
    + + {{!-- Avalanche scaling: uWS vs Socket.io on the same 0.5 GB / 1 vCPU + small box used for the Pillar 2 cliff test. Two thresholds: + clean-recovery (operational headroom) and survivable cliff + (catastrophic-failure ceiling). --}} +
    +
    +

    Avalanche scaling: uWS vs Socket.io on the same 0.5 GB box

    +

    + We re-ran the deploy-avalanche test from Pillar 2 against uWS on the same 1 vCPU / 0.5 GB Railway box that exposed Socket.io's 25K cliff. p50 = the typical user's blackout window (Connecting… with broadcasts disappearing); recovery = the time until 95% of the fleet is back. +

    +

    + At 20K, uWS handles the storm cleanly — full recovery in 4.6 s, typical user back in 3 s. At 25K, the same N where Socket.io OOMs, uWS recovers 100% — but a 25% increase from 20K to 25K already pushes recovery from 4.6 s to 57 s (12×). At 50K, half the fleet waits 2.5 minutes. By 100K, the box can't even ramp the full target. +

    +

    + The diagnostic point: even uWS's wire advantage doesn't make reconnect storms scale linearly. The handshake serialization, kernel rate limiting, and TCP backpressure are server-resource problems, not library-overhead problems. AnyCable doesn't have an avalanche to recover from at any of these scales because the WebSocket layer doesn't restart. +

    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ClientsSocket.io recoveryuWS recoveryuWS p50AnyCable
    5,0004.5 s, 100%0 s
    10,0003.9 s, 100%0 s
    15,0005.8 s, 98.5%0 s
    20,0008.0 s, 96.2%4.6 s, 100%3.1 s0 s
    25,000never (cliff)57 s, 100%6.6 s0 s
    50,000cliff223 s, 98.6%151 s0 s
    ~90–100Kclifframp cliff0 s
    +

    Same 1 vCPU / 0.5 GB Railway box used for Socket.io's 25K cliff. uWS recovery = wall-clock time to 95% reconnected; p50 = median per-client blackout (typical user "Connecting…" window). At ~90–100K, uws-server-small can't even ramp the full target — it's the server-side cliff. AnyCable's WebSocket layer is a separate Go binary, so app deploys never restart it — no avalanche at any scale.

    +
    +
    +
    +
    +
    + {{!-- Use cases / impact --}}
    From 4883c278c1f53203dcc173acb721d78f964c78e5 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Thu, 7 May 2026 20:17:02 -0700 Subject: [PATCH 31/57] Compare page: full integration of uWS data + architecture context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dedicated uWS section was carrying the full comparison story alone; this weaves the data into the rest of the page so the reader gets a consistent picture from hero through TL;DR through pillars and FAQ. What changed: - New "Where the differences come from" section between TL;DR and Pillar 1 — sets up the architecture mental model (where the WS layer runs, whether the protocol carries replay state) so every benchmark number that follows is predictable. - Hero subtitle reframed to lead on delivery + deploy resilience (where AnyCable wins decisively) instead of memory (where uWS now wins on bare-wire density). - All three hero cards updated to 4 rows including uWS: • Delivery rate: shows uWS at 86%, paired with Default Socket.io as the no-replay losers • Server memory: shows uWS at 72 MB with a † footnote noting it doesn't include a replay buffer; AnyCable Pro stays positioned as "lightest setup that delivers 100%" • Idle capacity: shows uWS at 1,018,366 / 5.4 KB per conn, AnyCable Pro at 999,954 / 19 KB per conn — both reach 1M, framing the AnyCable footprint as "broker overhead, not bloat" - TL;DR bullets rewritten to weave uWS in naturally: replay-less setups lumped together on delivery; deploy-fragile architectures lumped together; capacity bullet shows uWS leading on bare wire but the trade-off is broker features. - Pillar 1 (Delivery) lead paragraph adds uWS's ~14% loss alongside Socket.io default's ~13%. - Pillar 3 (Capacity) lead + table updated: 4-way comparison with per-connection memory + replay column, header changed to clarify that AnyCable's heavier footprint is the broker overhead. - Feature comparison table gets a uWebSockets.js column — every framework feature beyond raw transport is "DIY" or "No", same as Default Socket.io. The diagnostic point that uWS solves Socket.io's wire problem but not its framework gaps. - "What you don't have to build" intro rewritten to cover uWS too. - New FAQ entry "What about uWebSockets.js?" with the same data, mirrored into the JSON-LD FAQPage block. - Page title, meta description, TechArticle JSON-LD, and SEO keywords updated to surface uWS comparison terms ("uWebSockets.js vs Socket.io", "uWebSockets.js vs AnyCable") so visitors searching for those queries find this page. - TL;DR on-page nav gets an "Architecture" link. - dateModified bumped to 2026-05-08. --- src/compare/socket-io/index.html | 139 +++++++++++++++++++++++-------- 1 file changed, 102 insertions(+), 37 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 5d30a04..9fbd3b8 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -1,6 +1,6 @@ - {{> dochead pageTitle="AnyCable vs Socket.io | 100% delivery, deploy-resilient WebSockets, 1M connections per node" pageDescription="Same Railway box, same 1M-connection test: Socket.io accepts 119,826 (single Node event loop saturates), open-source AnyCable holds 994K (32 GB RAM ceiling), AnyCable Pro holds 1M on 19 GB. Plus: AnyCable delivers 100% under jitter (vs Socket.io's 87%), AnyCable Pro holds 10K reconnecting clients on ~2.3× less server memory than Socket.io+CSR, and connections survive deploys. All numbers reproducible." pageUrl="https://anycable.io/compare/socket-io"}} + {{> dochead pageTitle="AnyCable vs Socket.io vs uWebSockets.js | 100% delivery, deploy-resilient WebSockets, 1M connections per node" pageDescription="Same Railway box, four setups: Socket.io accepts 119,826 (event loop saturates), uWebSockets.js holds 1,018,366 on 5.45 GB, AnyCable OSS holds 994K, AnyCable Pro holds 1M on 19 GB. Replay-less setups (Socket.io, uWS) lose ~13–14% of messages under jitter; AnyCable + Socket.io+CSR deliver 100%. Only AnyCable's separate-process architecture survives every deploy without dropping a connection. All numbers reproducible." pageUrl="https://anycable.io/compare/socket-io"}}
    {{> header}} @@ -15,7 +15,7 @@

    vs Socket.io

    - 10,000 clients on Railway, including Socket.io's Connection State Recovery. CSR and AnyCable both deliver 100%; AnyCable Pro does it on less than half the server memory. + 10,000 clients on Railway across four setups — Socket.io, Socket.io + CSR, uWebSockets.js, and AnyCable. Only AnyCable delivers every message under jitter and survives deploys without dropping a single connection.

    Open source — run it yourself @@ -30,10 +30,11 @@

    Default Socket.io
    675 MB

    Socket.io + CSR
    627 MB
    AnyCable Pro
    271 MB
    +
    uWebSockets.js
    72 MB†
    -
    Peak RSS during the jitter run. AnyCable Pro: ~2.3× less than CSR. (Open-source AnyCable: 760 MB.)
    +
    Peak RSS during the jitter run. †uWS uses the least memory but loses 14% of messages — that footprint doesn't include a replay buffer. AnyCable Pro is the lightest setup that delivers 100%. (OSS: 760 MB.)
    @@ -52,8 +54,9 @@

    TL;DR#

      -
    1. Delivery. Default Socket.io loses ~13% of messages under jitter at 10K clients. CSR fixes delivery; replay tail ~8 s p99. AnyCable matches delivery, clears the tail ~22% faster (~6 s p99), and Pro does it on ~2.3× less server memory.
    2. -
    3. Deploys. Every Socket.io process restart severs every WebSocket. AnyCable runs as a separate Go binary — your app deploys, the WebSocket layer stays up.
    4. -
    5. Capacity. Same hardware, same 1M-connection test. Socket.io topped out at 119,826 (single Node event loop). Open-source AnyCable held 993,994 at the box's 32 GB ceiling. AnyCable Pro held 999,954 on 19 GB — ~1.7× more memory-efficient, with headroom remaining.
    6. +
    7. Delivery. Default Socket.io loses ~13% of messages under jitter at 10K clients; uWebSockets.js loses ~14% (no replay layer either, despite being lighter on the wire). CSR fixes delivery with an ~8 s p99 replay tail. AnyCable matches delivery and clears the tail ~22% faster (~6 s p99).
    8. +
    9. Deploys. Every Node-embedded WebSocket layer dies with the app process — Socket.io, CSR, uWS, all the same. AnyCable runs as a separate Go binary; your app deploys, the WebSocket layer stays up, no avalanche to recover from.
    10. +
    11. Capacity. Same hardware, same 1M-connection test. Socket.io topped out at 119,826 (single Node event loop). uWebSockets.js held 1,018,366 on 5.45 GB. AnyCable OSS held 993,994 (32 GB ceiling); AnyCable Pro held 999,954 on 19 GB. uWS wins on bare-wire density; AnyCable's heavier per-connection footprint is the broker (replay, history, restart-survivable).

    Same JS client patterns on the frontend (@anycable/core). On the backend, broadcasts go over plain HTTP — Node, FastAPI, Laravel, Go, anything that can issue an HTTP POST.

    + {{!-- Delivery guarantees — two-halves like about-slide --}}
    @@ -89,7 +131,7 @@

    TL;DR

    1. AnyCable delivers every message — with the lowest server cost#

    - Two questions matter on the open internet: does every message arrive, and at what server cost? Under simulated jitter — 1-second TCP drops every ~15 s, the pattern WiFi handoffs produce — default Socket.io drops ~13% of messages. CSR fixes delivery; its replay tail tops out near 11 s, p99 ~8 s. AnyCable matches CSR on delivery, clears the tail ~1.7 s sooner, and Pro holds 10K reconnecting clients on less than half CSR's memory. + Two questions matter on the open internet: does every message arrive, and at what server cost? Under simulated jitter — 1-second TCP drops every ~15 s, the pattern WiFi handoffs produce — default Socket.io drops ~13% of messages; uWebSockets.js drops ~14% (also no replay). CSR fixes delivery; its replay tail tops out near 11 s, p99 ~8 s. AnyCable matches CSR on delivery, clears the tail ~1.7 s sooner, and Pro holds 10K reconnecting clients on less than half CSR's memory.

    Default Socket.io: messages sent during a disconnect aren't recoverable

    @@ -631,13 +673,16 @@

    CSR with the in-memo

    3. One AnyCable node holds 1,000,000 idle connections — Pro on 19 GB, OSS on 32 GB#

    - Same Railway box as the jitter benchmark — Pro tier, 32 vCPU / 32 GB, single instance, no Redis or NATS backplane. All three runtimes targeted 1,000,000 idle connections across 25 sharded test clients (each container has its own ~64K outbound-port pool). The table shows what each held at peak. + Same Railway box as the jitter benchmark — Pro tier, 32 vCPU / 32 GB, single instance, no Redis or NATS backplane. All four runtimes targeted 1,000,000 idle connections across sharded test clients (each container has its own ~64K outbound-port pool). The table below shows what each held at peak. +

    +

    + Each runtime hits a different wall. Socket.io accepted 119,826 and rejected ~880K during ramp — the single Node event loop saturates handshakes serially, regardless of memory. Open-source AnyCable held 993,994, capping at the box's 32 GB ceiling (~33 KB / connection); more from there means a bigger box.

    - Each runtime hits a different wall. Socket.io accepted 119,826 and rejected ~880K during ramp — the single Node event loop saturates handshakes serially, regardless of memory. Open-source AnyCable held 993,994, capping at the box's 32 GB ceiling (~33 KB / connection). More connections from there means a bigger box. + AnyCable Pro held 999,954 on 19 GB with 13 GB headroom — about 1.7× more memory-efficient than OSS at 1M (~19 vs ~33 KB / connection). And uWebSockets.js held 1,018,366 on 5.45 GB — the lightest of the four at ~5.4 KB per connection, because it's a bare wire layer with no broker overhead. The trade-off is what that overhead buys you: the AnyCable footprint is per-stream replay, history retention, and broker framing — features uWS doesn't include.

    - AnyCable Pro held 999,954 on 19 GB with 13 GB headroom — about 1.7× more memory-efficient than OSS at 1M (~19 vs ~33 KB / connection). Same gap at smaller scale: 200K is 3.5 GB vs OSS's 8.3 GB. The headline of this page is delivery and deploy resilience above; the connection ceiling — when you go looking — is comfortably past a million on a single node. + The connection ceiling, when you go looking: comfortably past a million on a single node, on either AnyCable or uWS. The headline of this page is delivery and deploy resilience above — not raw connection count, where Node-embedded uWS and separate-process AnyCable both clear 1M.

    @@ -645,20 +690,22 @@

    3. One AnyCable node holds 1,000,00 - - - + + + + - - - - - - + + + + + +
    Idle connectionsAnyCable memoryAnyCable CPU
    (of 32 vCPU)
    Idle connections heldMemory at peakPer-connReplay?
    10,000 OSS280 MB0%
    200,000 OSS8.35 GB2.63% (~0.8 vCPU)
    200,000 Pro3.56 GB1.94% (~0.6 vCPU)
    993,994 OSS, RAM-bound32.00 GB (ceiling)12.22% (~3.9 vCPU)
    999,954 Pro19.34 GB9.37% (~3.0 vCPU)
    Socket.io ceiling (1M attempted)119,826 connssingle core saturated
    200,000 AC OSS8.35 GB42 KByes
    200,000 AC Pro3.56 GB18 KByes
    Socket.io (1M attempted)119,826 conns~52 KBno
    993,994 AC OSS, RAM-bound32.00 GB (ceiling)33 KByes
    999,954 AC Pro19.34 GB19 KByes
    1,018,366 uWS5.45 GB5.4 KBno
    +

    Same 32 vCPU / 32 GB Railway box. uWS leads on bare-wire density — no broker overhead. AnyCable Pro is the lightest setup that includes built-in replay; the extra ~14 KB / connection is per-stream history, broker framing, and reconnect-resume protocol.

    {{!-- Memory + CPU during the 1M Pro run, captured live @@ -778,10 +825,10 @@

    Try it i

    What you don't have to build

    - Socket.io gives you WebSocket transport and rooms. Everything else — reliability, presence, auth, clustering, monitoring — is weeks of engineering plus months of hardening. + Socket.io gives you WebSocket transport and rooms. uWebSockets.js gives you a faster transport. Both stop there. Everything else — reliability, presence, auth, clustering, monitoring — is weeks of engineering plus months of hardening, on a single Node event loop you also have to keep alive across deploys.

    - AnyCable ships these as primitives, hardened in production since 2017. + AnyCable ships these as primitives, hardened in production since 2017, in a process you don't have to restart when your app deploys.

    @@ -792,21 +839,23 @@

    What you don't have to build

    Feature Default Socket.io Socket.io + CSR + uWebSockets.js AnyCable - Reliable deliveryNoYes (opt-in)Yes (default) - Replay latency p99lost~8 s~6 s - Survives server restartNoRedis Streams / MongoNATS / Redis - Multi-node setupRedis pub/subRedis pub/sub incompat.Any broker - Deploy resilienceAll dropAll dropConnections survive - Presence trackingDIYDIYBuilt-in - AuthenticationDIYDIYJWT, signed streams - Backend languageNode.js onlyNode.js onlyAny (HTTP API) - MonitoringAdmin UIAdmin UIPrometheus & StatsD + Reliable deliveryNoYes (opt-in)NoYes (default) + Replay latency p99lost~8 slost~6 s + Survives server restartNoRedis Streams / MongoNoNATS / Redis + Multi-node setupRedis pub/subRedis pub/sub incompat.DIY (Redis)Any broker + Deploy resilienceAll dropAll dropAll dropConnections survive + Presence trackingDIYDIYDIYBuilt-in + AuthenticationDIYDIYDIYJWT, signed streams + Backend languageNode.js onlyNode.js onlyNode.js onlyAny (HTTP API) + MonitoringAdmin UIAdmin UIDIYPrometheus & StatsD +

    uWS solves Socket.io's wire-overhead problem — not its framework gaps. Every feature beyond raw transport is still DIY, on the same single Node process. The "what you don't have to build" question is the same.

    @@ -867,7 +916,12 @@

    FAQ<
    How does AnyCable compare on performance? -
    In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), we pushed three setups to a 1,000,000-connection idle target on a single instance. Socket.io accepted 119,826 and rejected ~880K — its single Node event loop saturated under handshake load. Open-source AnyCable held 993,994, peaking at the box's 32 GB RAM ceiling (~33 KB / connection). AnyCable Pro held 999,954 on only 19 GB — ~1.7× more memory-efficient than OSS at the same load, with 13 GB headroom remaining. At 10,000 dynamic clients with simulated jitter (1-second TCP drops every ~15 s), AnyCable delivered all 1.2M messages with a ~6 s p99 replay tail (vs ~8 s for Socket.io+CSR), and AnyCable Pro held the same fleet on 271 MB peak memory — ~2.3× less than CSR. Run the numbers yourself: github.com/irinanazarova/anycable-socketio-benchmarks.
    +
    In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), we pushed four setups to a 1,000,000-connection idle target on a single instance. Socket.io accepted 119,826 and rejected ~880K — its single Node event loop saturated under handshake load. uWebSockets.js held 1,018,366 on 5.45 GB — the lightest bare wire layer, but with no built-in replay or broker. Open-source AnyCable held 993,994, peaking at the box's 32 GB RAM ceiling (~33 KB / connection). AnyCable Pro held 999,954 on only 19 GB — ~1.7× more memory-efficient than OSS at the same load, with 13 GB headroom remaining. At 10,000 dynamic clients with simulated jitter (1-second TCP drops every ~15 s), AnyCable delivered all 1.2M messages with a ~6 s p99 replay tail (vs ~8 s for Socket.io+CSR); replay-less setups (default Socket.io, uWS) lost ~13–14% of messages during the jitter blind windows. Run the numbers yourself: github.com/irinanazarova/anycable-socketio-benchmarks.
    +
    + +
    + What about uWebSockets.js? It's faster than Socket.io. +
    uWS is genuinely faster on the wire — we measured it. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 19 GB). The "10× faster than Socket.io" claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses ~14% of messages — the same loss profile as Socket.io without CSR — because both at-most-once and embedded in the same Node process as your app. uWS solves "Socket.io's wire is too heavy"; it doesn't solve "we lose messages during disruption" or "every deploy hits our users." See the full uWebSockets.js comparison above.
    @@ -967,7 +1021,15 @@

    Drop AnyCable into your Node.js app today

    "name": "How does AnyCable compare on performance?", "acceptedAnswer": { "@type": "Answer", - "text": "In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), we pushed three setups to a 1,000,000-connection idle target on a single instance. Socket.io accepted 119,826 and rejected ~880K — its single Node event loop saturated under handshake load. Open-source AnyCable held 993,994, peaking at the box's 32 GB RAM ceiling (~33 KB / connection). AnyCable Pro held 999,954 on only 19 GB — about 1.7× more memory-efficient than OSS at the same load, with 13 GB headroom remaining. At 10,000 dynamic clients with simulated jitter, AnyCable delivered all 1.2M messages with a ~6 s p99 replay tail (vs ~8 s for Socket.io+CSR), and AnyCable Pro held the same fleet on 271 MB peak memory — about 2.3× less than CSR. Source: github.com/irinanazarova/anycable-socketio-benchmarks." + "text": "In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), we pushed four setups to a 1,000,000-connection idle target on a single instance. Socket.io accepted 119,826 and rejected ~880K — its single Node event loop saturated under handshake load. uWebSockets.js held 1,018,366 on 5.45 GB — the lightest bare wire layer, but with no built-in replay or broker. Open-source AnyCable held 993,994, peaking at the box's 32 GB RAM ceiling (~33 KB / connection). AnyCable Pro held 999,954 on only 19 GB — about 1.7× more memory-efficient than OSS at the same load. At 10,000 dynamic clients with simulated jitter, AnyCable delivered all 1.2M messages with a ~6 s p99 replay tail (vs ~8 s for Socket.io+CSR); replay-less setups (default Socket.io, uWS) lost about 13–14% of messages during the jitter blind windows. Source: github.com/irinanazarova/anycable-socketio-benchmarks." + } + }, + { + "@type": "Question", + "name": "What about uWebSockets.js? It's faster than Socket.io.", + "acceptedAnswer": { + "@type": "Answer", + "text": "uWS is genuinely faster on the wire — measured. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 19 GB). The '10× faster than Socket.io' claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses about 14% of messages — the same loss profile as Socket.io without CSR — because both are at-most-once and embedded in the same Node process as your app. uWS solves 'Socket.io's wire is too heavy'; it doesn't solve 'we lose messages during disruption' or 'every deploy hits our users.'" } }, { @@ -1044,12 +1106,12 @@

    Drop AnyCable into your Node.js app today

    { "@context": "https://schema.org", "@type": "TechArticle", - "headline": "AnyCable vs Socket.io: 100% delivery, deploy-resilient WebSockets, 1M idle connections per node", - "description": "Same Railway box, same 1,000,000-connection idle test: Socket.io accepted 119,826 (single Node event loop saturated), open-source AnyCable held 993,994 (32 GB RAM ceiling), AnyCable Pro held 999,954 on 19 GB. Plus AnyCable delivers 100% under jitter (vs Socket.io's 87%) with a ~6 s p99 replay tail (vs ~8 s for CSR), AnyCable Pro holds the same 10K reconnecting fleet on 271 MB (~2.3× less than CSR), and connections survive deploys. All numbers reproducible from the open-source bench repo.", + "headline": "AnyCable vs Socket.io vs uWebSockets.js: 100% delivery, deploy-resilient WebSockets, 1M idle connections per node", + "description": "Same Railway box, four-way comparison: Socket.io accepted 119,826 (single Node event loop saturated); uWebSockets.js held 1,018,366 on 5.45 GB (lightest wire, no replay); open-source AnyCable held 993,994 (32 GB RAM ceiling); AnyCable Pro held 999,954 on 19 GB. Replay-less setups (default Socket.io, uWS) lost about 13–14% of messages under jitter; AnyCable and Socket.io+CSR delivered 100%. Only AnyCable's separate-process architecture survives every deploy without dropping a connection. All numbers reproducible from the open-source bench repo.", "url": "https://anycable.io/compare/socket-io", "mainEntityOfPage": "https://anycable.io/compare/socket-io", "datePublished": "2026-04-30", - "dateModified": "2026-05-03", + "dateModified": "2026-05-08", "author": { "@type": "Organization", "name": "AnyCable team", @@ -1063,6 +1125,9 @@

    Drop AnyCable into your Node.js app today

    "about": [ "WebSocket server", "Socket.io alternative", + "uWebSockets.js comparison", + "uWebSockets.js vs Socket.io", + "uWebSockets.js vs AnyCable", "Real-time messaging", "Node.js WebSocket server", "WebSocket scaling", From b162e327bc8c5ad770154a113e6bcf2265f82aea Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Fri, 8 May 2026 13:09:43 -0700 Subject: [PATCH 32/57] Compare page: refactor inline styles into tokens + BEM + utility classes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 386 inline style="" attrs → 16. Reach for design tokens (compare.scss local + global variables.scss) before hard-coded hex; reach for BEM blocks before utility classes; reach for utilities before inline. New BEM blocks: compare-hero, compare-data-table (+ --compact modifier for the feature matrix), compare-data-table__footnote, compare-bullet-list, compare-code-block, compare-try-card, compare-quote-card__*. Utility set: t-eyebrow, t-mute/meta/quiet/strong/accent/best, t-num, t-tiny, t-bold; code tints c-key/str/err/com; row modifiers is-row-best/worst; auto-spacing for sequential h3.about-slide__subtitle (kills the 11× margin-top: 32px). Code palette tokens: \$compare-code-keyword/string/ error/comment plus \$compare-best. Architecture contract documented at the top of compare.scss so the next person reaches for the right tool. Visual diff: identical. --- src/compare/socket-io/index.html | 582 ++++++++++++++++--------------- src/modules/blocks/compare.scss | 340 ++++++++++++++++++ 2 files changed, 636 insertions(+), 286 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 9fbd3b8..46b1078 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -7,18 +7,18 @@
    {{!-- Comparison hero — focused on the comparison, not branding --}} -
    -
    -
    -

    Comparison

    -

    - AnyCable vs Socket.io +
    +
    +
    +

    Comparison

    +

    + AnyCable vs Socket.io & uWebSockets.js

    -

    - 10,000 clients on Railway across four setups — Socket.io, Socket.io + CSR, uWebSockets.js, and AnyCable. Only AnyCable delivers every message under jitter and survives deploys without dropping a single connection. +

    + Three findings from the same Railway box. AnyCable guarantees every message arrives — Socket.io and uWebSockets.js both lose ~14% under jitter. uWS and AnyCable both reach 1M idle connections; Socket.io caps at ~120K. Only AnyCable survives every deploy — every Node-embedded WebSocket layer (Socket.io, CSR, uWS) dies with its app process.

    -

    - Open source — run it yourself +

    + Open source — run it yourself

    @@ -31,10 +31,11 @@

    Default Socket.io
    675 MB
    Socket.io + CSR
    627 MB
    -
    AnyCable Pro
    271 MB
    -
    uWebSockets.js
    72 MB†
    +
    AnyCable Pro
    271 MB
    +
    uWebSockets.js
    72 MB†
    Peak RSS during the jitter run. †uWS uses the least memory but loses 14% of messages — that footprint doesn't include a replay buffer. AnyCable Pro is the lightest setup that delivers 100%. (OSS: 760 MB.)
    + See the jitter benchmark

    @@ -53,10 +55,11 @@

    TL;DR#

      -
    1. Delivery. Default Socket.io loses ~13% of messages under jitter at 10K clients; uWebSockets.js loses ~14% (no replay layer either, despite being lighter on the wire). CSR fixes delivery with an ~8 s p99 replay tail. AnyCable matches delivery and clears the tail ~22% faster (~6 s p99).
    2. -
    3. Deploys. Every Node-embedded WebSocket layer dies with the app process — Socket.io, CSR, uWS, all the same. AnyCable runs as a separate Go binary; your app deploys, the WebSocket layer stays up, no avalanche to recover from.
    4. -
    5. Capacity. Same hardware, same 1M-connection test. Socket.io topped out at 119,826 (single Node event loop). uWebSockets.js held 1,018,366 on 5.45 GB. AnyCable OSS held 993,994 (32 GB ceiling); AnyCable Pro held 999,954 on 19 GB. uWS wins on bare-wire density; AnyCable's heavier per-connection footprint is the broker (replay, history, restart-survivable).
    6. +
    7. Delivery. Default Socket.io loses ~13% of messages under jitter at 10K clients; uWebSockets.js loses ~14% (no replay layer either, despite being lighter on the wire). CSR fixes delivery with an ~8 s p99 replay tail. AnyCable matches delivery and clears the tail ~22% faster (~6 s p99). Full benchmark →
    8. +
    9. Deploys. Every Node-embedded WebSocket layer dies with the app process — Socket.io, CSR, uWS, all the same. AnyCable runs as a separate Go binary; your app deploys, the WebSocket layer stays up, no avalanche to recover from. Avalanche test →
    10. +
    11. Capacity. Same hardware, same 1M-connection test. Socket.io topped out at 119,826 (single Node event loop). uWebSockets.js held 1,018,366 on 5.45 GB. AnyCable OSS held 993,994 (32 GB ceiling); AnyCable Pro held 999,954 on 19 GB. uWS wins on bare-wire density; AnyCable's heavier per-connection footprint is the broker (replay, history, restart-survivable). 1M idle test →

    Same JS client patterns on the frontend (@anycable/core). On the backend, broadcasts go over plain HTTP — Node, FastAPI, Laravel, Go, anything that can issue an HTTP POST.

    -
    # Default Socket.io — 10K clients, 120 msgs each
    -Client 0: missing 6, 7, 36, 37, 73, 74, 104, 105
    -Client 1: missing 3, 4, 38, 39, 69, 70, 108, 109
    -Client 2: missing 4, 5, 41, 42, 76, 77, 111-113
    +                
    # Default Socket.io — 10K clients, 120 msgs each
    +Client 0: missing 6, 7, 36, 37, 73, 74, 104, 105
    +Client 1: missing 3, 4, 38, 39, 69, 70, 108, 109
    +Client 2: missing 4, 5, 41, 42, 76, 77, 111-113
     
    -# Each cluster = one jitter event. ~15 lost per client.
    -# Total: 156,856 of 1,200,000 expected. ~87% delivery.
    +# Each cluster = one jitter event. ~15 lost per client.
    +# Total: 156,856 of 1,200,000 expected. ~87% delivery.
     
    -# Socket.io + CSR — same workload
    -Client 0: all 120 received  (replay tail: max 10.8s)
    -Client 1: all 120 received  (p99: 7.9s)
    -Client 2: all 120 received  (p95: 4.0s)
    +# Socket.io + CSR — same workload
    +Client 0: all 120 received  (replay tail: max 10.8s)
    +Client 1: all 120 received  (p99: 7.9s)
    +Client 2: all 120 received  (p95: 4.0s)
     
    -# AnyCable — same workload
    -Client 0: all 120 received  (replay tail: max 9.3s)
    -Client 1: all 120 received  (p99: 6.2s)
    -Client 2: all 120 received  (p95: 4.1s)
    +# AnyCable — same workload +Client 0: all 120 received (replay tail: max 9.3s) +Client 1: all 120 received (p99: 6.2s) +Client 2: all 120 received (p95: 4.1s)

    @@ -195,11 +198,11 @@

    Benchmark: 10,000 clients under simulated jitt

    AnyCable: 100% delivery, ~6-second p99 replay tail. ~1.7 s ahead of CSR. Server peak: 760 MB OSS, 271 MB Pro — ~2.3× less than CSR for the same 10K fleet.

    -

    Why default Socket.io's 13% loss is structural

    +

    Why default Socket.io's 13% loss is structural

    Each jitter event creates d seconds of blind window. Over publishing window T with N events, expected loss ≈ (d × N) / T. For our run: 1.3 s × ~8 / 60 ≈ 17% upper bound. We measured 13.1% — lower because not every event fully overlaps publishing. Not a configuration bug; default Socket.io loses messages in proportion to frequency × disruption.

    -

    Why a 1-second blip becomes a multi-second tail

    +

    Why a 1-second blip becomes a multi-second tail

    Every realtime client — socket.io-client, @anycable/core, anything that wants to stay up under load — reconnects with a 0.5–5 s backoff window to avoid stampeding the server after a disruption. The implication: any TCP-level blip becomes multiple seconds of offline window, by design. Without delivery guarantees, those seconds are lost messages. With replay, they're delayed messages — same backoff cost, completely different user experience.

    @@ -209,85 +212,85 @@

    Why a 1-second blip

    - +
    - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - + + + +
    Default Socket.ioSocket.io + CSRAnyCable
    Default Socket.ioSocket.io + CSRAnyCable
    Clients10,00010,00010,000
    Clients10,00010,00010,000
    Expected deliveries1,200,0001,200,0001,200,000
    Expected deliveries1,200,0001,200,0001,200,000
    Jitter events78,74882,06883,328
    Jitter events78,74882,06883,328
    CSR resume raten/a99.5%n/a
    CSR resume raten/a99.5%n/a
    Deliveries lost156,85600
    Deliveries lost156,85600
    Delivery rate~87%100%100%
    Delivery rate~87%100%100%
    Latency p50140 ms230 ms260 ms
    Latency p50140 ms230 ms260 ms
    Latency p951.1 s4.0 s4.1 s
    Latency p951.1 s4.0 s4.1 s
    Latency p991.6 s7.9 s6.2 s
    Latency p99 (replay tail)n/a7.9 s6.2 s
    Latency max2.1 s10.8 s9.3 s
    Latency maxn/a10.8 s9.3 s
    Server memory peak675 MB627 MB760 / 271 MBServer memory peak675 MB627 MB760 / 271 MB
    -

    AnyCable cell shows open-source / AnyCable Pro. Same Railway box (32 vCPU / 32 GB) for all four runs. Server memory pulled from Railway's metrics API for the test window. CPU was negligible across all setups (under 1.1% of the 32 vCPU box).

    +

    AnyCable cell shows open-source / AnyCable Pro. Same Railway box (32 vCPU / 32 GB) for all four runs. Server memory pulled from Railway's metrics API for the test window. CPU was negligible across all setups (under 1.1% of the 32 vCPU box).

    @@ -315,14 +318,14 @@

    On the wire, uWS wins decisively

    uWS is the most efficient WebSocket library you can run inside a Node.js app. The "10× faster than Socket.io" headline isn't marketing — it's reproducible.

    -

    But the wire is rarely the bottleneck

    +

    But the wire is rarely the bottleneck

    Under jitter, uWS loses ~14% of messages — almost identical to Socket.io's ~13% loss without CSR. The faster wire doesn't change the math: 1-second blip × ~8 events / 60 s of publishing ≈ 13% blind window. Wire speed determines how fast the messages that do arrive land. The ones missed during the offline window are still gone.

    To deliver every message on uWS, you'd buffer broadcasts per stream, track per-client offsets, replay on reconnect, and absorb reconnect storms — basically reimplement AnyCable's broker by hand. uWS is a WebSocket library; AnyCable is a delivery framework. Different layer of the stack.

    -

    Reconnect storms scale with N — regardless of library

    +

    Reconnect storms scale with N — regardless of library

    uWS pushes the avalanche cliff far past Socket.io's, but the user-facing recovery time grows non-linearly. Both libraries hit a "real avalanche" point where every broadcast is lost for minutes — uWS just at a higher N.

    @@ -332,55 +335,55 @@

    Reconnect storms sca

    And the deploy problem stays the same. Whether the embedded library is Socket.io or uWS, your app and your WebSocket layer are the same Node process — every deploy still kills every connection, and the new container has to absorb a reconnect storm during cold-start. AnyCable's separate-process architecture skips the avalanche entirely: anycable-go doesn't restart on app deploys, connections persist, replay resumes whatever was missed.

    -

    Different problems, different solutions

    +

    Different problems, different solutions

    uWS is the right answer to "Socket.io's wire is too heavy." AnyCable is the right answer to "we lose messages during network blips" or "every deploy hits our users." Both are honest improvements over default Socket.io — they just sit at different layers of the stack.

- +
- - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + +
10,000 clients under jitteruWSSocket.ioAnyCable OSSAnyCable Pro
10,000 clients under jitteruWSSocket.ioAnyCable OSSAnyCable Pro
Server memory peak72 MB675 MB760 MB271 MB
Server memory peak72 MB675 MB760 MB271 MB
Replay latency p99993 msno replay6.2 s6.2 s
Replay latency p99993 msno replay6.2 s6.2 s
Delivery rate86.4%~87%100%100%
Delivery rate86.4%~87%100%100%
Messages lost163,371156,85600Messages lost163,371156,85600
-

Same 32 vCPU / 32 GB Railway box. uWS reconnect uses 2–5 s exponential backoff matched to socket.io-client's defaults. Comparing uWS vs default Socket.io on delivery: nearly identical — both at-most-once, no replay, lose during the same blind windows. Latency is identical between AnyCable OSS and Pro (same protocol, same broker); memory differs because Pro's broker is more compact.

+

Same 32 vCPU / 32 GB Railway box. uWS reconnect uses 2–5 s exponential backoff matched to socket.io-client's defaults. Comparing uWS vs default Socket.io on delivery: nearly identical — both at-most-once, no replay, lose during the same blind windows. Latency is identical between AnyCable OSS and Pro (same protocol, same broker); memory differs because Pro's broker is more compact.

@@ -404,69 +407,69 @@

Avalanche scaling: uWS vs Socket.io on the sam

- +
- - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - - + + + + + + - - - - - + + + + +
ClientsSocket.io recoveryuWS recoveryuWS p50AnyCable
ClientsSocket.io recoveryuWS recoveryuWS p50AnyCable
5,0004.5 s, 100%0 s
5,0004.5 s, 100%0 s
10,0003.9 s, 100%0 s
10,0003.9 s, 100%0 s
15,0005.8 s, 98.5%0 s
15,0005.8 s, 98.5%0 s
20,0008.0 s, 96.2%4.6 s, 100%3.1 s0 s
20,0008.0 s, 96.2%4.6 s, 100%3.1 s0 s
25,000never (cliff)57 s, 100%6.6 s0 s
25,000never (cliff)57 s, 100%6.6 s0 s
50,000cliff223 s, 98.6%151 s0 s
50,000cliff223 s, 98.6%151 s0 s
~90–100Kclifframp cliff0 s~90–100Kclifframp cliff0 s
-

Same 1 vCPU / 0.5 GB Railway box used for Socket.io's 25K cliff. uWS recovery = wall-clock time to 95% reconnected; p50 = median per-client blackout (typical user "Connecting…" window). At ~90–100K, uws-server-small can't even ramp the full target — it's the server-side cliff. AnyCable's WebSocket layer is a separate Go binary, so app deploys never restart it — no avalanche at any scale.

+

Same 1 vCPU / 0.5 GB Railway box used for Socket.io's 25K cliff. uWS recovery = wall-clock time to 95% reconnected; p50 = median per-client blackout (typical user "Connecting…" window). At ~90–100K, uws-server-small can't even ramp the full target — it's the server-side cliff. AnyCable's WebSocket layer is a separate Go binary, so app deploys never restart it — no avalanche at any scale.

@@ -577,20 +580,20 @@

AnyCable's WebSocket server is a separate proc

-
# Socket.io deploy — 5,000 clients (1 vCPU / 0.5 GB box)
+                
# Socket.io deploy — 5,000 clients (1 vCPU / 0.5 GB box)
 t=0.0s → deploy restarts server
-t=0.5s → all 5,000 connections dropped
+t=0.5s → all 5,000 connections dropped
 t=2.6s → p50 reconnected
 t=4.5s → 95% recovery (avalanche over)
 
-# Socket.io deploy — 25,000 clients (same box) — the cliff
+# Socket.io deploy — 25,000 clients (same box) — the cliff
 t=0.0s   → deploy restarts server
-t=0.5s   → all 25,000 connections dropped
-t=300s+  → 0 reconnected — box can't absorb the storm
+t=0.5s   → all 25,000 connections dropped
+t=300s+  → 0 reconnected — box can't absorb the storm
 
-# AnyCable deploy — any client count, any box
+# AnyCable deploy — any client count, any box
 t=0.0s → deploy restarts app
-t=0.0s → zero connections dropped
+t=0.0s → zero connections dropped
 t=0.0s → users don't notice

@@ -607,7 +610,7 @@

Benchmark: the avalanche scales — until it d

The architectural cost of co-locating WebSocket and app: deploy resilience is bounded by what the box absorbs during its weakest moment — cold start.

-

CSR with the in-memory adapter doesn't help here

+

CSR with the in-memory adapter doesn't help here

CSR depends on server-side state. The in-memory adapter loses it on restart — nothing to replay. Redis Streams or MongoDB persist the state, but the connections still drop. No protocol papers over architecture.

@@ -617,49 +620,49 @@

CSR with the in-memo
- +
- - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + +
ClientsSocket.io recoveryReconnectedAnyCable
ClientsSocket.io recoveryReconnectedAnyCable
5,0004.5 s100%0 s
5,0004.5 s100%0 s
10,0003.9 s100%0 s
10,0003.9 s100%0 s
15,0005.8 s98.5% (224 lost)0 s
15,0005.8 s98.5% (224 lost)0 s
20,0008.0 s96.2% (753 lost)0 s
20,0008.0 s96.2% (753 lost)0 s
25,000never0% (all 25K lost)0 s
25,000never0% (all 25K lost)0 s
-

1 vCPU / 0.5 GB Railway box. "Recovery" = wall-clock time until 95% of clients reconnect. AnyCable's WebSocket layer is a separate Go binary, so app deploys don't restart it — no avalanche to recover from at any scale.

+

1 vCPU / 0.5 GB Railway box. "Recovery" = wall-clock time until 95% of clients reconnect. AnyCable's WebSocket layer is a separate Go binary, so app deploys don't restart it — no avalanche to recover from at any scale.

@@ -687,25 +690,25 @@

3. One AnyCable node holds 1,000,00
- +
- - - - - + + + + + - - - - - - + + + + + +
Idle connections heldMemory at peakPer-connReplay?
Idle connections heldMemory at peakPer-connReplay?
200,000 AC OSS8.35 GB42 KByes
200,000 AC Pro3.56 GB18 KByes
Socket.io (1M attempted)119,826 conns~52 KBno
993,994 AC OSS, RAM-bound32.00 GB (ceiling)33 KByes
999,954 AC Pro19.34 GB19 KByes
1,018,366 uWS5.45 GB5.4 KBno
200,000 AC OSS8.35 GB42 KByes
200,000 AC Pro3.56 GB18 KByes
Socket.io (1M attempted)119,826 conns~52 KBno
993,994 AC OSS, RAM-bound32.00 GB (ceiling)33 KByes
999,954 AC Pro19.34 GB19 KByes
1,018,366 uWS5.45 GB5.4 KBno
-

Same 32 vCPU / 32 GB Railway box. uWS leads on bare-wire density — no broker overhead. AnyCable Pro is the lightest setup that includes built-in replay; the extra ~14 KB / connection is per-stream history, broker framing, and reconnect-resume protocol.

+

Same 32 vCPU / 32 GB Railway box. uWS leads on bare-wire density — no broker overhead. AnyCable Pro is the lightest setup that includes built-in replay; the extra ~14 KB / connection is per-stream history, broker framing, and reconnect-resume protocol.

{{!-- Memory + CPU during the 1M Pro run, captured live @@ -770,16 +773,16 @@

Migrating from Socket.io: replace

-
// Before: Socket.io (embedded in your app)
-io.to('chat:42').emit('message', payload);
+                
// Before: Socket.io (embedded in your app)
+io.to('chat:42').emit('message', payload);
 
-// After: broadcast to AnyCable via HTTP
-await fetch('http://anycable:8080/_broadcast', {
-  method: 'POST',
-  headers: { 'Content-Type': 'application/json' },
-  body: JSON.stringify({
-    stream: 'chat:42',
-    data: JSON.stringify(payload),
+// After: broadcast to AnyCable via HTTP
+await fetch('http://anycable:8080/_broadcast', {
+  method: 'POST',
+  headers: { 'Content-Type': 'application/json' },
+  body: JSON.stringify({
+    stream: 'chat:42',
+    data: JSON.stringify(payload),
   }),
 });
@@ -797,20 +800,20 @@

Try it i Three places to start: read the code, run a working demo, or wire it into a serverless setup.

@@ -833,29 +836,36 @@

What you don't have to build

- +
+ + + + + + + - - - - - - + + + + + + - - - - - - - - - + + + + + + + + +
FeatureDefault Socket.ioSocket.io + CSRuWebSockets.jsAnyCable
FeatureDefault
Socket.io
Socket.io
+ CSR
uWeb-
Sockets.js
AnyCable
Reliable deliveryNoYes (opt-in)NoYes (default)
Replay latency p99lost~8 slost~6 s
Survives server restartNoRedis Streams / MongoNoNATS / Redis
Multi-node setupRedis pub/subRedis pub/sub incompat.DIY (Redis)Any broker
Deploy resilienceAll dropAll dropAll dropConnections survive
Presence trackingDIYDIYDIYBuilt-in
AuthenticationDIYDIYDIYJWT, signed streams
Backend languageNode.js onlyNode.js onlyNode.js onlyAny (HTTP API)
MonitoringAdmin UIAdmin UIDIYPrometheus & StatsD
Reliable deliveryNoYes (opt-in)NoYes (default)
Replay latency p99lost~8 slost~6 s
Survives server restartNoRedis Streams / MongoNoNATS / Redis
Multi-node setupRedis pub/subRedis pub/sub incompat.DIY (Redis)Any broker
Deploy resilienceAll dropAll dropAll dropConnections survive
Presence trackingDIYDIYDIYBuilt-in
AuthenticationDIYDIYDIYJWT, signed streams
Backend languageNode.js onlyNode.js onlyNode.js onlyAny (HTTP API)
MonitoringAdmin UIAdmin UIDIYPrometheus & StatsD
-

uWS solves Socket.io's wire-overhead problem — not its framework gaps. Every feature beyond raw transport is still DIY, on the same single Node process. The "what you don't have to build" question is the same.

+

uWS solves Socket.io's wire-overhead problem — not its framework gaps. Every feature beyond raw transport is still DIY, on the same single Node process. The "what you don't have to build" question is the same.

@@ -877,17 +887,17 @@

When Socket.io is the right choice

-
Proven at scale
+
Proven at scale
{{!-- Doximity quote — public; from On Rails Podcast. Trimmed to focus on the deploy-resilience point. --}} -
+
"We use AnyCable for our dialer products, which is where real-time is critical — it's our video and voice platform. Anytime you restart your application, which happens when you deploy, you're gonna get connection severances. AnyCable allows them to keep that connection open. That Go service stays up, and you can continue shipping your application as normal."
-
- Ryan Stawarz & Austin Story
+ -
+
diff --git a/src/modules/blocks/compare.scss b/src/modules/blocks/compare.scss index 668a41e..393846d 100644 --- a/src/modules/blocks/compare.scss +++ b/src/modules/blocks/compare.scss @@ -7,6 +7,21 @@ // // Global tokens ($accentPrimaryColor, $backgroundSecondaryColor, etc.) // come from src/modules/variables.scss, imported in index.scss. +// +// Architecture (read this before reaching for inline style): +// 1. Local tokens below cover the gray scale, code palette, radius, +// and header offset. Anything brand-related (accent red, primary +// surface) lives in the global variables.scss. +// 2. BEM blocks own component-level structure: .compare-hero, +// .compare-hero-card, .compare-tldr, .compare-data-table, +// .compare-faq, .compare-cta, etc. Add a new block when a +// structural pattern appears > 1×. +// 3. Utility classes (.t-eyebrow, .t-mute, .t-strong, .t-num, the +// green-best/.is-worst variants) cover repeating typography +// and color decisions. Reach for these before inline. +// 4. Inline style="" is reserved for true one-offs that don't +// warrant a class — e.g. a single section's max-width wrapper. +// If a value repeats, promote it to a token or class. // ===================================================================== @@ -30,6 +45,16 @@ $compare-bg-code: #1a1a1a; $compare-radius: 8px; $compare-header-offset: 80px; // header height + a touch of breathing +// Inline syntax-tint palette for the dark
 blocks (try-it cards
+// and the bench-chart figure). Kept narrow on purpose; if we ever need
+// real syntax highlighting, swap in a Prism/Highlight.js theme instead
+// of growing this list.
+$compare-code-keyword:   #fcd34d;  // amber
+$compare-code-string:    #86efac;  // green
+$compare-code-error:     #ff6b6b;  // red
+$compare-code-comment:   #cce6ff;  // pale blue
+$compare-best:           #16a34a;  // green winner accent
+
 
 .compare-page {
   // Notebook-feel dotted background — continuous from hero through CTA.
@@ -132,6 +157,162 @@ $compare-header-offset:  80px; // header height + a touch of breathing
   }
 
 
+  // -------------------------------------------------------------------
+  // Utility classes — small, composable, reach for these before inline.
+  // Naming: t-* for typography/color, c-* for code-block tints.
+  // -------------------------------------------------------------------
+  .t-eyebrow {
+    font-size: 12px;
+    font-weight: 600;
+    text-transform: uppercase;
+    letter-spacing: 0.06em;
+    color: $compare-text-mute;
+  }
+
+  .t-mute   { color: $compare-text-mute; }     // #888
+  .t-meta   { color: $compare-text-meta; }     // #555
+  .t-quiet  { color: $compare-text-quiet; }    // #777
+  .t-strong { color: $compare-text-strong; }   // #1a1a1a
+  .t-accent { color: $accentPrimaryColor; }
+  .t-best   { color: $compare-best; }
+  .t-num    { font-variant-numeric: tabular-nums; }
+
+  .c-key { color: $compare-code-keyword; }
+  .c-str { color: $compare-code-string; }
+  .c-err { color: $compare-code-error; }
+  .c-com { color: $compare-code-comment; }
+
+  .t-tiny  { font-size: 11px; }
+  .t-bold  { font-weight: 600; }
+
+
+  // -------------------------------------------------------------------
+  // Sub-section spacing inside long prose columns (architecture, each
+  // pillar). The first h3 sits flush to the lead paragraph; subsequent
+  // h3s need breathing room above. Replaces the per-element
+  // style="margin-top: 32px" sprinkled across the markup.
+  // -------------------------------------------------------------------
+  .about-slide__content {
+    h3.about-slide__subtitle + p + h3.about-slide__subtitle,
+    h3.about-slide__subtitle ~ h3.about-slide__subtitle {
+      margin-top: 32px;
+    }
+  }
+
+
+  // -------------------------------------------------------------------
+  // Caveats / red-dot bullet list. Used wherever we want to call out
+  // a small set of structural points (Socket.io CSR caveats, etc.).
+  // -------------------------------------------------------------------
+  .compare-bullet-list {
+    margin: 0 0 84px;
+    padding: 0;
+    list-style: none;
+    line-height: 1.6;
+
+    li {
+      position: relative;
+      padding-left: 18px;
+      margin-bottom: 12px;
+
+      &::before {
+        content: '·';
+        position: absolute;
+        left: 0;
+        top: 0;
+        color: $accentPrimaryColor;
+        font-weight: 700;
+      }
+    }
+  }
+
+
+  // -------------------------------------------------------------------
+  // Inline code block — dark slab with monospace text and tinted spans
+  // (.c-key, .c-str, .c-err, .c-com). Used for the delivery sample
+  // output and the try-it snippets. The bench-chart figure has its own
+  // .compare-bench-chart__pre with smaller type.
+  // -------------------------------------------------------------------
+  .compare-code-block {
+    width: 100%;
+    margin: 0;
+    padding: 32px;
+    background: $compare-bg-code;
+    color: #e0e0e0;
+    border-radius: $compare-radius;
+    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
+    font-size: 13px;
+    line-height: 1.7;
+    white-space: pre;
+    overflow-x: auto;
+    box-sizing: border-box;
+  }
+
+
+  // -------------------------------------------------------------------
+  // Hero block — the comparison eyebrow + H1 + subhead at the top of
+  // the page. Sits on the dotted page bg directly (no panel).
+  // -------------------------------------------------------------------
+  .compare-hero {
+    padding: 80px 24px 60px;
+
+    &__inner {
+      width: 100%;
+      max-width: 1200px;
+      margin: 0 auto;
+    }
+
+    &__intro {
+      margin-bottom: 48px;
+      text-align: center;
+    }
+
+    &__eyebrow {
+      margin-bottom: 16px;
+      font-size: 14px;
+      font-weight: 600;
+      letter-spacing: 0.08em;
+      text-transform: uppercase;
+      color: $accentPrimaryColor;
+    }
+
+    &__title {
+      margin: 0 0 20px;
+      font-size: clamp(28px, 6vw, 64px);
+      font-weight: 700;
+      line-height: 1.1;
+      letter-spacing: -0.02em;
+
+      // The dimmed connector words ("vs", "&") between framework names.
+      .is-mute { color: $compare-text-fade; font-weight: 400; }
+    }
+
+    &__subtitle {
+      max-width: 820px;
+      margin: 0 auto;
+      font-size: 22px;
+      line-height: 1.5;
+      color: $compare-text-meta;
+
+      strong { color: $compare-text-strong; }
+    }
+
+    // "Open source — run it yourself" line beneath the subhead.
+    &__source {
+      margin-top: 16px;
+      font-size: 14px;
+      color: $compare-text-mute;
+
+      a {
+        color: inherit;
+        text-decoration: underline;
+        text-underline-offset: 3px;
+        text-decoration-color: $compare-text-faint;
+      }
+    }
+  }
+
+
   // -------------------------------------------------------------------
   // Heading anchor (Stripe-style hover-revealed # icon)
   // -------------------------------------------------------------------
@@ -208,6 +389,7 @@ $compare-header-offset:  80px; // header height + a touch of breathing
         letter-spacing: -0.01em;
 
         &.is-worst { color: $accentPrimaryColor; }
+        &.is-best  { color: $compare-best; }
         &.is-na    { color: $compare-text-fade; font-weight: 400; }
       }
     }
@@ -220,6 +402,28 @@ $compare-header-offset:  80px; // header height + a touch of breathing
       line-height: 1.5;
       color: $compare-text-mute;
     }
+
+    // Inline link at the bottom of each hero card pointing readers to
+    // the full data table + methodology for the experiment that
+    // produced the card's numbers. Subtle by default, accent on hover.
+    &__deeplink {
+      display: inline-block;
+      margin-top: 10px;
+      font-size: 12px;
+      font-weight: 600;
+      letter-spacing: 0.02em;
+      color: $compare-text-quiet;
+      text-decoration: none;
+
+      &::after {
+        content: ' →';
+        color: inherit;
+      }
+
+      &:hover {
+        color: $accentPrimaryColor;
+      }
+    }
   }
 
 
@@ -255,6 +459,16 @@ $compare-header-offset:  80px; // header height + a touch of breathing
       li               { margin-bottom: 8px; }
       li:last-child    { margin-bottom: 0; }
       strong           { color: #111; }
+
+      // Section-link anchor inside  inherits the strong color
+      // — the visual weight comes from the parent, not the link.
+      strong a         { color: inherit; text-decoration: none; }
+    }
+
+    // Trailing "Full benchmark →" link in each TL;DR bullet; bolder
+    // than body text, accent on hover via the global .link class.
+    &__more {
+      font-weight: 600;
     }
 
     &__note {
@@ -323,6 +537,42 @@ $compare-header-offset:  80px; // header height + a touch of breathing
     &.compare-quote-card {
       padding: 40px 32px;
       background-color: $compare-bg-soft;
+
+      .compare-quote-card__eyebrow {
+        margin-bottom: 20px;
+        font-size: 12px;
+        font-weight: 600;
+        text-transform: uppercase;
+        letter-spacing: 0.06em;
+        color: $compare-text-mute;
+      }
+
+      .compare-quote-card__text {
+        margin: 0 0 20px;
+        padding: 0;
+        font-size: 17px;
+        font-style: normal;
+        line-height: 1.6;
+        color: $compare-text-strong;
+        quotes: none;
+      }
+
+      .compare-quote-card__byline {
+        margin-bottom: 24px;
+        font-size: 14px;
+        line-height: 1.5;
+        color: $compare-text-meta;
+
+        strong { color: $compare-text-strong; }
+      }
+
+      .compare-quote-card__footer {
+        padding-top: 20px;
+        border-top: 1px solid $compare-border;
+        font-size: 13px;
+        line-height: 1.65;
+        color: $compare-text-quiet;
+      }
     }
 
     // On mobile, tables inside this card can exceed the viewport width.
@@ -425,7 +675,13 @@ $compare-header-offset:  80px; // header height + a touch of breathing
   // capacity, and feature-comparison tables. Right-aligned by default
   // (numeric); first column left-aligned (label). is-worst / is-na /
   // is-key modifiers handle per-cell semantics.
+  //
+  // Two selector forms because the page is mid-migration:
+  //   1. .compare-data-table — preferred, decoupled from where it lives
+  //   2. .slide-show__frame table — legacy, applies to existing tables
+  //      that haven't been class-tagged yet. Remove once HTML is swept.
   // -------------------------------------------------------------------
+  .compare-data-table,
   .slide-show__frame table {
     width: 100%;
     background-color: transparent;
@@ -478,12 +734,25 @@ $compare-header-offset:  80px; // header height + a touch of breathing
       color: #111;
     }
 
+    // Soft-tinted rows: green for the AnyCable / uWS winners
+    // (e.g. 1M-conn capacity), red for the cliff/loser rows.
+    tr.is-row-best  { background: #f0fdf4; }
+    tr.is-row-worst { background: #fff5f5; }
+
     // "Worst per metric" — single accent, used sparingly.
     td.is-worst,
     td.is-worst strong {
       color: $accentPrimaryColor;
     }
 
+    // "Best per metric" — green, used sparingly. Pair only with a
+    // visibly-worst cell in the same row so the eye reads winner+loser
+    // rather than "everything's color-coded."
+    td.is-best,
+    td.is-best strong {
+      color: $compare-best;
+    }
+
     td.is-na,
     td.is-na strong {
       color: $compare-text-fade;
@@ -506,6 +775,77 @@ $compare-header-offset:  80px; // header height + a touch of breathing
   }
 
 
+  // -------------------------------------------------------------------
+  // Footnote that sits directly under a data table — quiet gray label
+  // explaining methodology / measurement source.
+  // -------------------------------------------------------------------
+  .compare-data-table__footnote {
+    margin: 12px 4px 0;
+    font-size: 12px;
+    line-height: 1.5;
+    color: $compare-text-mute;
+  }
+
+
+  // -------------------------------------------------------------------
+  // Try-it card — clickable link cards under "Run it yourself" pointing
+  // to GitHub demos. Border + padding + title/desc rows.
+  // -------------------------------------------------------------------
+  .compare-try-card {
+    display: block;
+    padding: 24px;
+    background: $backgroundPrimaryColor;
+    border: 1px solid $compare-border;
+    border-radius: $compare-radius;
+    color: inherit;
+    text-align: left;
+    text-decoration: none;
+    transition: border-color 0.15s, transform 0.15s;
+
+    &:hover { border-color: #d6d6d6; }
+
+    &__title {
+      margin-bottom: 6px;
+      font-size: 18px;
+      font-weight: 600;
+    }
+
+    &__desc {
+      font-size: 14px;
+      line-height: 1.5;
+      color: #666;
+    }
+  }
+
+
+  // -------------------------------------------------------------------
+  // Compact data-table modifier — used when a 5-column matrix would
+  // overflow the right-column frame at default padding (the feature
+  // comparison table). Tighter padding + smaller type + table-layout:
+  // fixed so column widths obey the explicit colgroup percentages.
+  // The feature table is also center-aligned (yes/no/lost values),
+  // unlike the right-aligned numeric tables.
+  // -------------------------------------------------------------------
+  .compare-data-table--compact {
+    font-size: 12px;
+    table-layout: fixed;
+
+    th {
+      padding: 8px 4px;
+      text-align: center;
+
+      &:first-child { text-align: left; }
+    }
+
+    td {
+      padding: 6px 4px;
+      text-align: center;
+
+      &:first-child { text-align: left; }
+    }
+  }
+
+
   // -------------------------------------------------------------------
   // Closing CTA — leads with self-hosted Pro (monetized), free Managed
   // is the secondary low-friction path, open source gets a single line

From 32bd0e1d6a8d19b147a924aad0a46bdce94c6ee2 Mon Sep 17 00:00:00 2001
From: Irina Nazarova 
Date: Thu, 14 May 2026 01:24:04 -0700
Subject: [PATCH 33/57] Compare/Socket.io: hero/wrong-choice/CTA rewrite +
 multi-process throughput rows
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Major narrative pass on /compare/socket-io targeting the Series A Node.js
CTO ICP. Substantive content changes:

* Hero rewritten with three comparison cards (Reliability / Deploys /
  Headroom) and a load-bearing TL;DR.
* New wrong-choice section before CTA + CTA reorder ("run anycable-go
  locally" primary, Managed secondary).
* Throughput section restructured into two methodology groups: WS-layer
  ceiling (publisher in-process) and multi-process (fan-out crosses a
  network bus). The lower group adds five rows of apples-to-apples
  multi-node configurations: Socket.io+Redis with in-proc and HTTP-pool
  publishers, AnyCable single-instance HTTP-pool (OSS + Pro), and
  AnyCable 2-node cluster with NATS. Honest finding: once the publisher
  is operationally separate from the WS layer, both architectures hit
  similar publisher-bound ceilings (~50–75K at 1M target).
* Avalanche reframed as architectural lesson, not 'only AnyCable can'.
* Migration section: both server- and client-side TS diff.
* FAQ: anycable-go restart story expanded with broker-specific behavior;
  new 'How do I run anycable-go in production?' entry.
* Ops footnote: commit-count one-liner explaining why low velocity is
  fine for infrastructure.
* Rubric 2: 25K-cliff caveat with pointer to uWS sidebar.
* Meta description + JSON-LD updated to match new narrative.

CSS:
* New compare-* BEM block namespace replacing the global slide-show /
  cases-slide / about-slide classes the page was sharing.
* New blocks: compare-rubric, compare-frame, compare-impact-card{,s},
  compare-prose, compare-quote-card, compare-code-tabs (CSS-only radio
  tabs), compare-callout.
* Table first-column wraps long labels (white-space: normal) so 6-column
  tables don't overflow.

Reproducible benchmarks at github.com/irinanazarova/anycable-socketio-benchmarks.
---
 src/compare/socket-io/index.html | 1021 +++++++++++++++++++-----------
 src/modules/blocks/compare.scss  |  864 +++++++++++++++++++------
 2 files changed, 1315 insertions(+), 570 deletions(-)

diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html
index 46b1078..f829432 100644
--- a/src/compare/socket-io/index.html
+++ b/src/compare/socket-io/index.html
@@ -1,88 +1,110 @@
 
 
-  {{> dochead pageTitle="AnyCable vs Socket.io vs uWebSockets.js | 100% delivery, deploy-resilient WebSockets, 1M connections per node" pageDescription="Same Railway box, four setups: Socket.io accepts 119,826 (event loop saturates), uWebSockets.js holds 1,018,366 on 5.45 GB, AnyCable OSS holds 994K, AnyCable Pro holds 1M on 19 GB. Replay-less setups (Socket.io, uWS) lose ~13–14% of messages under jitter; AnyCable + Socket.io+CSR deliver 100%. Only AnyCable's separate-process architecture survives every deploy without dropping a connection. All numbers reproducible." pageUrl="https://anycable.io/compare/socket-io"}}
+  {{> dochead pageTitle="AnyCable vs Socket.io vs uWebSockets.js | 100% delivery, deploy-resilient WebSockets, 1M connections per node" pageDescription="A measured comparison for Node.js teams hitting Socket.io's limits. Same Railway hardware, five rubrics. Default Socket.io and uWS lose ~13–14% of messages under simulated WiFi jitter; AnyCable and Socket.io+CSR deliver 100%. In-process WS (Socket.io, +CSR, uWS) dies on every deploy and past ~20K connections doesn't recover; AnyCable runs WS as a separate process — 0 s reconnect storm. On one 32 vCPU/32 GB box: AnyCable Pro holds 1M idle connections, uWS holds 1M, Socket.io caps at ~120K. WS-layer throughput in-process: uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. All numbers reproducible." pageUrl="https://anycable.io/compare/socket-io"}}
   
     
{{> header}}
- {{!-- Comparison hero — focused on the comparison, not branding --}} -
+ {{!-- Hero — three at-a-glance cards, one per load-bearing rubric + finding. Each compares the headline metric across the four + tested setups; the page below lays out the methodology and + the next two rubrics (operations + optionality). --}} +

Comparison

- AnyCable vs Socket.io & uWebSockets.js + AnyCable vs Socket.io

- Three findings from the same Railway box. AnyCable guarantees every message arrives — Socket.io and uWebSockets.js both lose ~14% under jitter. uWS and AnyCable both reach 1M idle connections; Socket.io caps at ~120K. Only AnyCable survives every deploy — every Node-embedded WebSocket layer (Socket.io, CSR, uWS) dies with its app process. -

-

- Open source — run it yourself + A measured comparison of AnyCable, uWebSockets.js, and Socket.io with Connection State Recovery — for Node.js teams who've outgrown default Socket.io. Same Railway hardware, five rubrics, reproducible numbers.

- {{!-- Headline numbers — three cards. Each is a small comparison - table with a single highlighted "worst" cell so the analytical - reader can scan all three results in one glance. --}}
-
Delivery rate at 10,000 clients
+
Reliability under jitter
-
Default Socket.io
87%
-
uWebSockets.js
86%
-
Socket.io + CSR
100%
-
AnyCable
100%
+
+
Default Socket.io
+
87%
+
+
+
uWebSockets.js
+
86%
+
+
+
AnyCable, +CSR
+
100%
+
-
1.2M expected deliveries; replay-less setups lose ~13–14% during 1-second jitter blips. CSR + AnyCable both deliver 100%; AnyCable's replay tail is ~22% faster.
- See the jitter benchmark +
+ 10K clients, WiFi-drop pattern, 1.2M deliveries. AnyCable + Socket.io+CSR carry replay state; Socket.io default and uWS don't — messages during disconnect are gone. Rubric 1 → +
-
Server memory at 10,000 clients
+
Deploy reconnect storm
-
Default Socket.io
675 MB
-
Socket.io + CSR
627 MB
-
AnyCable Pro
271 MB
-
uWebSockets.js
72 MB†
+
+
Socket.io 25K conns
+
never
+
+
+
uWS 25K conns
+
never
+
+
+
AnyCable any scale
+
0 s
+
-
Peak RSS during the jitter run. †uWS uses the least memory but loses 14% of messages — that footprint doesn't include a replay buffer. AnyCable Pro is the lightest setup that delivers 100%. (OSS: 760 MB.)
- See the jitter benchmark +
+ In-process WS dies on every Node deploy. Past ~20K connections the reconnect storm doesn't recover. AnyCable's WS layer runs as a separate process — deploys don't touch it. Rubric 2 → +
-
Idle connections, single instance
+
Idle headroom one 32 vCPU box
-
Socket.io
119,826
-
AnyCable (open source)
993,994
-
AnyCable Pro
999,954
-
uWebSockets.js
1,018,366
+
+
Default Socket.io
+
120K
+
+
+
uWebSockets.js
+
1.0M
+
+
+
AnyCable Pro
+
1.0M
+
-
Same Railway box (32 vCPU / 32 GB), 1M attempted. Per-connection memory: uWS 5.4 KB, AnyCable Pro 19 KB, OSS 33 KB, Socket.io ~52 KB. uWS and AnyCable both reach 1M; Socket.io's single Node event loop saturates at ~120K. (uWS doesn't include a replay broker — the lighter footprint reflects that.)
- See the 1M idle test +
+ Single Node event loop saturates handshakes at ~120K regardless of memory. AnyCable Pro at 19 GB / 19 KB per conn — the lightest setup with built-in replay. Rubric 3 → +
- {{!-- TL;DR — analytical 3-point summary that doubles as on-page nav. - LLMs and search engines extract this kind of structured summary - directly. The findings list mirrors the three pillars below. --}}

TL;DR#

-
    -
  1. Delivery. Default Socket.io loses ~13% of messages under jitter at 10K clients; uWebSockets.js loses ~14% (no replay layer either, despite being lighter on the wire). CSR fixes delivery with an ~8 s p99 replay tail. AnyCable matches delivery and clears the tail ~22% faster (~6 s p99). Full benchmark →
  2. -
  3. Deploys. Every Node-embedded WebSocket layer dies with the app process — Socket.io, CSR, uWS, all the same. AnyCable runs as a separate Go binary; your app deploys, the WebSocket layer stays up, no avalanche to recover from. Avalanche test →
  4. -
  5. Capacity. Same hardware, same 1M-connection test. Socket.io topped out at 119,826 (single Node event loop). uWebSockets.js held 1,018,366 on 5.45 GB. AnyCable OSS held 993,994 (32 GB ceiling); AnyCable Pro held 999,954 on 19 GB. uWS wins on bare-wire density; AnyCable's heavier per-connection footprint is the broker (replay, history, restart-survivable). 1M idle test →
  6. -
-

Same JS client patterns on the frontend (@anycable/core). On the backend, broadcasts go over plain HTTP — Node, FastAPI, Laravel, Go, anything that can issue an HTTP POST.

+

+ If your users are mobile or your team deploys daily, default Socket.io drops messages and disconnects every user on every deploy. Socket.io+CSR fixes the message loss in-place (replay buffer, opt-in), but doesn't fix the deploys. uWebSockets.js trades the Socket.io framework for a faster wire (1M idle on one box, lowest memory of anything we tested), but inherits the same in-process deploy problem and ships nothing for replay, observability, or multi-language clients. AnyCable solves both — replay on by default, WS layer runs as its own service so deploys don't touch it — at the cost of one extra process to operate. +

+

+ On raw WS-layer throughput in-process: uWS leads at 907K msgs/sec, AnyCable does 522K, Socket.io+CSR 496K, default Socket.io 437K. Once the publisher is operationally separate from the WS layer — the shape both architectures end up in past one node — both cap around 50–75K msg/sec at the 1M target, publisher-bound, not WS-layer bound. The Socket.io co-located publisher case (in-process on one of the 2 Redis-cluster nodes) does 773K but it requires the app code to live inside one of the WS processes — the assumption AnyCable rejects. Throughput isn't our headline win — capacity, reliability, and deploys are. +

@@ -94,74 +116,148 @@

TL;DR -
-
-
-

Where the differences come from#

-

- Every metric on this page traces back to two architecture choices: where the WebSocket layer lives (inside your Node app process, or alongside it as a separate service) and whether the protocol carries replay state (most don't, by default). Once you know each setup's answers, the benchmark numbers are predictable. -

-

Default Socket.io — pure-JS WebSocket inside your Node process

-

- The Socket.io library runs inside the same Node.js process as your app routes, DB calls, and business logic. One event loop handles all of it. No replay buffer. Per-connection memory is heavy because Engine.IO + the Socket.io protocol add framing, room state, and event metadata on top of the raw WS — ~50 KB per connection at scale. At ~120K connections the event loop saturates handshakes; every deploy kicks every client because the WS layer dies with your Node process. +

+
+
+
+

What do we compare?#

+

+ You have a Node.js (or TypeScript / Deno / Bun) backend and you need realtime — chat, presence, live cursors, AI streaming, collaboration, dashboards. You're either standing one up for the first time, or you have Socket.io in production and looking for a replacement. This page is a comparison for that decision.

-

Socket.io + CSR — same process, with a per-socket recovery buffer

-

- Same architecture as Socket.io, with Connection State Recovery bolted on: the server stashes per-socket state (id, rooms, socket.data) and buffers packets for maxDisconnectionDuration. On reconnect, the client passes pid + last-offset and the server replays. Delivery: solved (100%). The latency cost is that the buffer drain is per socket and serializes through the same single event loop — at 10K reconnecting clients those drains queue up, hence the ~8 s p99 replay tail. Default in-memory adapter loses state on restart; multi-node needs Redis Streams or MongoDB. +

+ We benchmark five production-shaped options on the same hardware:

-

uWebSockets.js — native C++ WebSocket library, still in your Node process

-

- uWebSockets.js is a C++ WebSocket implementation with Node bindings. Same Node event-loop model, embedded the same way as Socket.io, but the C++ wire layer is dramatically lighter — ~5 KB per connection. That gap is real, and it's where the "10× faster than Socket.io" claim comes from. What it doesn't give you: replay (you'd build it yourself, including the broker, history per stream, reconnect storm handling). Architecturally the rest is identical to Socket.io: one Node process for app + WS, one event loop, every deploy still kicks every connection, no built-in delivery guarantees. +

    +
  • Default Socket.io — the baseline most teams have today, in-process Node library.
  • +
  • Socket.io + Connection State Recovery (CSR) — the in-place delivery upgrade Socket.io shipped in 4.6, often missed because it's opt-in and has adapter constraints.
  • +
  • uWebSockets.js + topics — the most-cited "just use uWS, it's 10× faster than Socket.io" alternative, using the library's built-in subscribe/publish API (the canonical pattern).
  • +
  • AnyCable OSS — a separate Go binary (anycable-go) your app broadcasts to over HTTP, with a broker (replay, history, presence) built in.
  • +
  • AnyCable Pro — same protocol and broadcast code as OSS, different server binary (anycable-go-pro): commercial license, more efficient broker, denser per-connection memory, horizontal scale with shared replay state.
  • +
+ +

+ Server-side broadcast code, side by side — what your Node app actually looks like for each option. Click a tab. Production-typical patterns; the bench harness mirrors them closely.

-

AnyCable — separate Go process, broker built in

-

- The WebSocket layer is anycable-go, a separate Go binary running alongside your app. Your app broadcasts to it over plain HTTP. Two consequences: (a) goroutines parallelize handshakes across all available CPU cores instead of serializing through one event loop, and (b) app deploys don't touch the WebSocket process — connections persist. The protocol carries (stream, epoch, offset) on every broadcast; the broker keeps per-stream history (replay) by default. Per-connection memory is heavier than uWS's bare wire footprint (~19 KB Pro, ~33 KB OSS at 1M), but that overhead is the broker — replay buffers, stream history, broadcaster framing. + +

+ + + + + +
+ + + + + +
+ +
// Server
+const io = new Server(httpServer);
+
+io.on('connection', (socket) => {
+  socket.on('subscribe', (t) => socket.join(t));
+});
+
+// Broadcast from anywhere:
+io.to('chat:42').emit('message', payload);
+
+// Trade-off: at-most-once delivery — messages
+// during a disconnect are gone.
+ +
// Server — same code, plus the CSR option
+const io = new Server(httpServer, {
+  connectionStateRecovery: {
+    maxDisconnectionDuration: 2 * 60 * 1000,
+  },
+});
+
+// Same broadcast call:
+io.to('chat:42').emit('message', payload);
+
+// Trade-off: replay buffers held in memory.
+// Adapter must support CSR (in-memory or pg).
+ +
// Server — uWS + built-in topics API
+const app = uWS.App();
+
+app.ws('/ws', {
+  message: (ws, msg) => {
+    const { topic } = JSON.parse(msg);
+    ws.subscribe(topic);
+  },
+});
+
+// Broadcast from anywhere:
+app.publish('chat:42', JSON.stringify(payload));
+
+// Trade-off: no replay, no built-in observability,
+// "bug reports only" maintainer posture.
+ +
// Your Node app stays put. anycable-go runs as
+// a separate Go process; you broadcast over HTTP:
+await fetch('http://anycable:8080/_broadcast', {
+  method: 'POST',
+  body: JSON.stringify({
+    stream: 'chat:42',
+    data: JSON.stringify(payload),
+  }),
+});
+
+// Trade-off: extra process to run; broadcast hop
+// over HTTP caps single-publisher throughput.
+ +
// Same broadcast code as OSS — just a different
+// binary (anycable-go-pro) for the WS server:
+await fetch('http://anycable:8080/_broadcast', {
+  method: 'POST',
+  body: JSON.stringify({
+    stream: 'chat:42',
+    data: JSON.stringify(payload),
+  }),
+});
+
+// Trade-off: commercial license; in return you get
+// the embedded broker, lower per-conn memory, and
+// horizontal scale with shared replay state.
+
+ +

+ Same workloads against all five, scored against five rubrics — the questions we actually hear from CTOs choosing realtime infrastructure for a growing Node app:

-

The pattern

-

- Anything Node-embedded — Socket.io, Socket.io+CSR, uWS — shares the same deploy-avalanche and the same single-event-loop ceiling, just at different N. Replay is a feature you either get from a framework or build yourself; only Socket.io+CSR (opt-in, lossy on restart) and AnyCable (default, restart-survivable with NATS or Redis brokers) provide it. AnyCable's advantages on delivery and deploy resilience aren't a tuning win — they're architectural. +

    +
  1. Will my product feel smooth and reliable even on unstable networks?
  2. +
  3. Can my team ship code without disrupting the product?
  4. +
  5. How much can we scale before we redo the architecture?
  6. +
  7. When this breaks at 3am, can my team diagnose and fix it?
  8. +
  9. If we're wrong about this, what does it cost to leave?
  10. +
+

+ Each rubric below states the question, the answer in a paragraph, and the data underneath. Benchmark code & raw output are open source: github.com/irinanazarova/anycable-socketio-benchmarks.

- {{!-- Delivery guarantees — two-halves like about-slide --}} -
-
-
-
-

1. AnyCable delivers every message — with the lowest server cost#

-

- Two questions matter on the open internet: does every message arrive, and at what server cost? Under simulated jitter — 1-second TCP drops every ~15 s, the pattern WiFi handoffs produce — default Socket.io drops ~13% of messages; uWebSockets.js drops ~14% (also no replay). CSR fixes delivery; its replay tail tops out near 11 s, p99 ~8 s. AnyCable matches CSR on delivery, clears the tail ~1.7 s sooner, and Pro holds 10K reconnecting clients on less than half CSR's memory. -

-

Default Socket.io: messages sent during a disconnect aren't recoverable

-

- From the Socket.io delivery-guarantees doc: "if the connection is broken while an event is being sent, then there is no guarantee that the other side has received it." No buffering, no catch-up protocol. The default — and what most production Socket.io apps ship. + {{!-- Delivery guarantees — content + media halves --}} +

+
+
+
+

1. Will my product feel smooth and reliable even on unstable networks?#

+

+ Your users are not necessarily always connected to a high-speed fiber network. Micro disruptions are largely not noticeable for HTTP requests, but they are the deal breaker for real-time features — or how reliable and smooth they are going to be. Two things decide it: how often messages are lost during disruption, and how long the recovery window feels. Both come down to whether the protocol carries replay state.

-

Socket.io + CSR: opt-in catch-up with adapter and replay constraints

-

- Connection State Recovery (Socket.io 4.6+) buffers per-socket state — id, rooms, socket.data — for up to maxDisconnectionDuration (default 2 min). On reconnect, the client passes its pid + last-seen offset; the server replays the missed packets. +

+ Default Socket.io and uWebSockets.js don't carry replay state — they lose ~13–14% of messages under simulated jitter (1-second TCP drops every ~15 s, the pattern WiFi handoffs produce). Socket.io with Connection State Recovery fixes delivery to 100%, with constraints on the adapter and a ~8 s p99 replay tail. AnyCable also delivers 100%, clears the tail ~1.7 s sooner, and AnyCable Pro holds the same 10K reconnecting fleet on less than half of CSR's server memory. The data:

-

- Four caveats from the Socket.io docs: -

-
    -
  • Opt-in. Most apps don't enable it.
  • -
  • Adapter constraints. Doesn't work with Socket.io's most common scaling adapter (Redis pub/sub). Multi-node needs Redis Streams or MongoDB.
  • -
  • Restart-fragile. The default in-memory adapter wipes all state on every server restart.
  • -
  • "The recovery will not always be successful" — their words. Application-level reconciliation is still required.
  • -
-

AnyCable: at-least-once by default, replay is per-stream

-

- AnyCable's protocol (actioncable-v1-ext-json) tracks each stream by epoch + offset. On reconnect the client sends a history command; the server replays the missed range as a batch and acks with confirm_history (or reject_history if evicted). -

-

- Retention is per stream: history_limit (default 100), history_ttl (default 300 s). With NATS JetStream or Redis brokers, history survives restart and works across nodes. Default — not opt-in, not experimental. +

+ Default Socket.io is at-most-once; CSR adds replay (opt-in, with adapter constraints). AnyCable ships replay by default via reliable streams — per-stream history, epoch + offset, restart-survivable with NATS or Redis. Mechanism & caveats: see appendix.

-
+
# Default Socket.io — 10K clients, 120 msgs each
 Client 0: missing 6, 7, 36, 37, 73, 74, 104, 105
 Client 1: missing 3, 4, 38, 39, 69, 70, 108, 109
@@ -183,114 +279,69 @@ 

AnyCable: at-least-once by default, replay is

{{!-- Benchmark table --}} -
-
-

Benchmark: 10,000 clients under simulated jitter

-

- 10,000 clients on Railway. Publisher sends 120 numbered messages at 2/sec — 1.2M expected deliveries. Each client force-closes its TCP socket for 1 s every ~15 s (no clean close — like a WiFi drop). ~8 jitter events per client. -

-

- Default Socket.io: ~87% delivery. 156,856 messages lost. Loss matches the blind window — nothing arrives during the outage, nothing comes back. -

-

- Socket.io + CSR: 100% delivery, ~8-second p99 replay tail. 99.5% of events resume the session; worst case ~11 s. Server peak: 627 MB. -

-

- AnyCable: 100% delivery, ~6-second p99 replay tail. ~1.7 s ahead of CSR. Server peak: 760 MB OSS, 271 MB Pro — ~2.3× less than CSR for the same 10K fleet. -

-

Why default Socket.io's 13% loss is structural

-

- Each jitter event creates d seconds of blind window. Over publishing window T with N events, expected loss ≈ (d × N) / T. For our run: 1.3 s × ~8 / 60 ≈ 17% upper bound. We measured 13.1% — lower because not every event fully overlaps publishing. Not a configuration bug; default Socket.io loses messages in proportion to frequency × disruption. -

-

Why a 1-second blip becomes a multi-second tail

-

- Every realtime client — socket.io-client, @anycable/core, anything that wants to stay up under load — reconnects with a 0.5–5 s backoff window to avoid stampeding the server after a disruption. The implication: any TCP-level blip becomes multiple seconds of offline window, by design. Without delivery guarantees, those seconds are lost messages. With replay, they're delayed messages — same backoff cost, completely different user experience. -

-

- The remaining gap (CSR ~8 s, AnyCable ~6 s) is the replay protocol itself: CSR drains a buffered packet list per socket — at 10K reconnecting clients those drains serialize through one event loop. AnyCable's history replay is per-stream, returns the missed range as a batch, parallel across streams. That's the ~1.7 s on the p99. +

+
+

+ 10K clients on Railway, publisher sends 120 messages at 2/sec, each client force-closes TCP for 1 s every ~15 s (WiFi-drop pattern). 1.2M expected deliveries. Per-test methodology & reproducer: bench repo README.

-
-
+
+
- - - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - + + + + + - - - - + + + + + + - - - + + + - - - - - + - - - - - - - - - - - + + + + + + +
Default Socket.ioSocket.io + CSRAnyCableSetupDeliveryLost of 1.2Mp95p99 replay tailServer mem
Clients10,00010,00010,000
Expected deliveries1,200,0001,200,0001,200,000
Jitter events78,74882,06883,328
CSR resume raten/a99.5%n/a
Deliveries lost156,85600Socket.io default~87%156,8561.1 sno replay675 MB
Delivery rate~87%100%Socket.io + CSR 100%04.0 s7.9 s627 MB
Latency p50140 ms230 ms260 msuWS topics86.4%163,371no replay72 MB
Latency p951.1 s4.0 sAnyCable OSS100%0 4.1 s
Latency p99 (replay tail)n/a7.9 s 6.2 s760 MB
Latency maxn/a10.8 s9.3 s
Server memory peak675 MB627 MB760 / 271 MB
AnyCable Pro100%04.1 s6.2 s271 MB
-

AnyCable cell shows open-source / AnyCable Pro. Same Railway box (32 vCPU / 32 GB) for all four runs. Server memory pulled from Railway's metrics API for the test window. CPU was negligible across all setups (under 1.1% of the 32 vCPU box).

+

Two clusters: no-replay protocols (Socket.io default, uWS) lose during disconnect windows; replay protocols (CSR, AnyCable) deliver 100% but pay a replay-tail latency cost. uWS's tiny memory is the no-replay-broker dodge; AnyCable Pro is the lightest replay-capable setup. uWS reconnects fast (~1 s) but messages during the disconnect are gone — nothing to replay. Same 32 vCPU / 32 GB Railway box; CPU negligible across all setups.

@@ -303,45 +354,29 @@

Why a 1-second blip becomes a multi-second tai idle capacity), so we acknowledge that head-on with measured numbers, then show where the wire isn't the bottleneck (replay-less = lossy on jitter, single-process = same deploy problem). --}} -
-
-
-
-

What about uWebSockets.js?#

-

- A common pushback from founders evaluating AnyCable: "Socket.io is old — uWebSockets.js is 10× faster, just use that." The wire-speed claim is genuinely true. We measured it on the same Railway hardware as everything else on this page. -

-

On the wire, uWS wins decisively

-

- At 10K reconnecting clients, uWS uses 72 MB of server memory — about 4× less than AnyCable Pro and 9× less than Socket.io+CSR. Replay latency p99 is 993 ms, the lowest of any setup we tested. And on a 32 vCPU / 32 GB box held idle to 1,018,366 connections on just 5.45 GB — a 5.35 KB-per-connection footprint that beats AnyCable Pro's already-impressive 19 KB at the same scale. -

-

- uWS is the most efficient WebSocket library you can run inside a Node.js app. The "10× faster than Socket.io" headline isn't marketing — it's reproducible. -

-

But the wire is rarely the bottleneck

-

- Under jitter, uWS loses ~14% of messages — almost identical to Socket.io's ~13% loss without CSR. The faster wire doesn't change the math: 1-second blip × ~8 events / 60 s of publishing ≈ 13% blind window. Wire speed determines how fast the messages that do arrive land. The ones missed during the offline window are still gone. +

+
+
+
+

Sidebar: what about uWebSockets.js?#

+

+ Not a rubric on its own — uWS sits inside Rubrics 1, 2, and 3 above. But it deserves direct treatment because it's the most common pushback we hear from founders evaluating AnyCable: "Socket.io is old — uWebSockets.js is 10× faster, just use that." The wire-speed claim is genuinely true. We measured it on the same Railway hardware as everything else on this page.

-

- To deliver every message on uWS, you'd buffer broadcasts per stream, track per-client offsets, replay on reconnect, and absorb reconnect storms — basically reimplement AnyCable's broker by hand. uWS is a WebSocket library; AnyCable is a delivery framework. Different layer of the stack. -

-

Reconnect storms scale with N — regardless of library

-

- uWS pushes the avalanche cliff far past Socket.io's, but the user-facing recovery time grows non-linearly. Both libraries hit a "real avalanche" point where every broadcast is lost for minutes — uWS just at a higher N. -

-

- Two thresholds emerge from the data. Clean-recovery (avalanche resolves in seconds — your operational headroom): Socket.io ~10K, uWS ~20K. Survivable cliff (server doesn't OOM): Socket.io 25K, uWS ~90K. uWS doubles your operating headroom, roughly quadruples your catastrophic-failure ceiling. Beyond clean-recovery, the storm becomes the user experience: at 50K on uWS, half the fleet waits 2.5 minutes before reconnecting, every broadcast during that window lost. +

+ Note on the uWS column. The uWS setup we test is uWebSockets.js with the built-in subscribe/publish topics API — the canonical Broadcast.js pattern from the library's own examples. Production teams who pick uWS typically deploy exactly this shape (for multi-node, they'd add Redis pub/sub on top — orthogonal to single-node behavior). Numbers below are not the "raw socket" wire floor; they're what a production-shaped uWS app actually does. +
+

+ uWS wins on the wire, decisively. 72 MB at 10K reconnecting clients, 5.45 GB at 1M idle, sub-second reconnect time (p99 ~993 ms) — lowest of anything we tested. The "10× faster than Socket.io" headline is reproducible. But — that's reconnect speed, not replay: uWS has no replay buffer, so the messages sent during the disconnect window are gone, no matter how fast the client reconnects.

-

- And the deploy problem stays the same. Whether the embedded library is Socket.io or uWS, your app and your WebSocket layer are the same Node process — every deploy still kills every connection, and the new container has to absorb a reconnect storm during cold-start. AnyCable's separate-process architecture skips the avalanche entirely: anycable-go doesn't restart on app deploys, connections persist, replay resumes whatever was missed. +

+ But the wire is rarely the decision. uWS still loses ~14% of messages under jitter (no replay buffer), the deploy avalanche still hits every connection (in-process, just like Socket.io), and the reconnect storm cliff is just at higher N (~20K clean recovery, ~90K survivable, vs Socket.io's ~10K / ~25K). uWS is the right answer to "Socket.io's wire is too heavy." AnyCable is the right answer to "we lose messages during network blips" or "every deploy hits our users." Different layers of the stack.

-

Different problems, different solutions

-

- uWS is the right answer to "Socket.io's wire is too heavy." AnyCable is the right answer to "we lose messages during network blips" or "every deploy hits our users." Both are honest improvements over default Socket.io — they just sit at different layers of the stack. +

+ Mechanism + per-N avalanche walkthrough: bench repo README.

-
-
+
+
@@ -362,7 +397,7 @@

Different problems, different solutions

- + @@ -392,21 +427,15 @@

Different problems, different solutions

small box used for the Pillar 2 cliff test. Two thresholds: clean-recovery (operational headroom) and survivable cliff (catastrophic-failure ceiling). --}} -
-
-

Avalanche scaling: uWS vs Socket.io on the same 0.5 GB box

-

- We re-ran the deploy-avalanche test from Pillar 2 against uWS on the same 1 vCPU / 0.5 GB Railway box that exposed Socket.io's 25K cliff. p50 = the typical user's blackout window (Connecting… with broadcasts disappearing); recovery = the time until 95% of the fleet is back. -

-

- At 20K, uWS handles the storm cleanly — full recovery in 4.6 s, typical user back in 3 s. At 25K, the same N where Socket.io OOMs, uWS recovers 100% — but a 25% increase from 20K to 25K already pushes recovery from 4.6 s to 57 s (12×). At 50K, half the fleet waits 2.5 minutes. By 100K, the box can't even ramp the full target. -

-

- The diagnostic point: even uWS's wire advantage doesn't make reconnect storms scale linearly. The handshake serialization, kernel rate limiting, and TCP backpressure are server-resource problems, not library-overhead problems. AnyCable doesn't have an avalanche to recover from at any of these scales because the WebSocket layer doesn't restart. +

+
+

Avalanche scaling: uWS just shifts the cliff to higher N

+

+ Same 1 vCPU / 0.5 GB Railway box. uWS pushes the catastrophic cliff from ~25K (Socket.io) to ~90K, but the curve is non-linear: 20K → 25K already takes recovery from 4.6 s to 57 s. The bottleneck is server-resource (handshake serialization, kernel rate limiting), not library overhead. AnyCable's separate process means there's no avalanche to recover from at any scale.

-
-
+
+
Replay latency p99993 msno replay (reconnect < 1 s) no replay 6.2 s 6.2 s
@@ -477,33 +506,33 @@

Avalanche scaling: uWS vs Socket.io on the sam {{!-- Use cases / impact --}} -
-
-
-
-

Impact

-

Loss and slow replay break workflows where the next message depends on the previous one

-
+
+
+
+
+

Impact

+

Loss and slow replay break workflows where the next message depends on the previous one

+

Lost messages cluster around network events — exactly when the user is watching. CSR recovers them, but 8 seconds late reads as "the app froze"; AnyCable lands them around 6. For sequential workloads, loss and delay both break the flow.

-
-
-
- Live chat & notifications - Messages disappear during network blips. Users see incomplete conversations, no indication anything is missing. +
+
+
+ Live chat & notifications + Messages disappear during network blips. Users see incomplete conversations, no indication anything is missing.
-
- LLM / AI streaming - Responses stream word by word. A mid-sentence disconnect leaves output truncated or garbled — chunks lost, no recovery. +
+ LLM / AI streaming + Responses stream word by word. A mid-sentence disconnect leaves output truncated or garbled — chunks lost, no recovery.
-
- Real-time collaboration & presence - Lost operations make documents diverge silently. Cursors jump, presence flickers; users lose trust in what they see. +
+ Real-time collaboration & presence + Lost operations make documents diverge silently. Cursors jump, presence flickers; users lose trust in what they see.
-
- Dashboards & monitoring - Permanent gaps in time-series data. The 200 ms blip during a traffic spike is the data point you needed. +
+ Dashboards & monitoring + Permanent gaps in time-series data. The 200 ms blip during a traffic spike is the data point you needed.
@@ -512,11 +541,17 @@

Loss and slow replay break workflows where the ne

{{!-- Standalone server — second pillar --}} -
-
-
-
-

2. Every Socket.io deploy severs every connection. This is architectural.#

+
+
+
+
+

2. Can my team ship code without disrupting the product?#

+

+ Modern teams deploy multiple times a day. Realtime apps have a structural choice: the WebSocket layer lives inside the app process, or it runs as its own service. The choice you make at small scale becomes a load-bearing decision at growing scale. Below is what happens when the WS layer is co-located with the app and the app restarts on every deploy — default for Socket.io, Socket.io+CSR, and uWS, because all three are Node libraries. At small scale the reconnect storm is recoverable; past a certain N, it stops being recoverable. +

+

+ The test below is real railway redeploy events on a 1 vCPU / 0.5 GB box, recording how long the WS layer takes to come back. The architecture and the benchmark, side by side: +

{{!-- Architecture diagram — Socket.io's app+WS in one process vs AnyCable's app | anycable-go separated. The visual @@ -570,16 +605,11 @@

2. Every Socket.io deploy severs eve -

Socket.io's WebSocket server is your application server

-

- One Node.js process handles HTTP, business logic, and WebSockets — sharing one event loop. HTTP requests come and go; WebSockets stay open for hours. Restart the process to deploy and every WebSocket dies with it. -

-

AnyCable's WebSocket server is a separate process

-

- Anycable-go is a separate Go binary. Your app — Node.js, Laravel, FastAPI, Django, anything that speaks HTTP — broadcasts to it. Deploy your app; the WebSocket layer stays up. +

+ The takeaway is architectural, not vendor-specific: once you're past a few thousand persistent connections, the WS layer should be its own service. Socket.io / uWS can be deployed that way with extra engineering — multiple Node processes, a Redis-backed adapter, sticky sessions, careful deploy choreography. AnyCable defaults to it: anycable-go is a separate Go binary; deploying the app process leaves it untouched. The 0 s recovery row in the table below is what that buys you.

-
+
# Socket.io deploy — 5,000 clients (1 vCPU / 0.5 GB box)
 t=0.0s → deploy restarts server
 t=0.5s → all 5,000 connections dropped
@@ -598,35 +628,21 @@ 

AnyCable's WebSocket server is a separate proc

-
-
-

Benchmark: the avalanche scales — until it doesn't

-

- We deployed Socket.io to a 1 vCPU / 0.5 GB Railway box (typical starter tier), connected N clients, then triggered a real railway redeploy. Every Socket.io deploy is a thundering herd: every connection drops, every client races back through the same load balancer. -

-

- Through 20K clients, the box absorbs the storm with degrading-but-graceful recovery. At 25K, recovery never completes. Memory hits 95% of the cap (~489 MB on a 512 MB box) just before redeploy; the post-redeploy reconnect storm OOMs the new container before any client establishes a session. 0% reconnect rate — every connection permanently lost. -

-

- The architectural cost of co-locating WebSocket and app: deploy resilience is bounded by what the box absorbs during its weakest moment — cold start. -

-

CSR with the in-memory adapter doesn't help here

-

- CSR depends on server-side state. The in-memory adapter loses it on restart — nothing to replay. Redis Streams or MongoDB persist the state, but the connections still drop. No protocol papers over architecture. -

-

- AnyCable: 0 seconds, every deploy, every scale. The WebSocket server is a separate Go binary — deploying your app doesn't touch it. No avalanche to absorb. +

+
+

+ Through 20K connections, recovery is graceful for the in-process WS setups. At 25K, recovery never completes — the post-redeploy reconnect storm OOMs the new container. CSR doesn't help: its in-memory adapter loses state on restart, and connections still drop. On a bigger box the cliff exists too, just at higher N. The uWS sidebar above shows the same shape on the same hardware: uWS pushes the catastrophic cliff to ~90K instead of ~25K, but recovery at 25K already takes 57 seconds, and at 90K it stops being recoverable. More RAM moves where the wall is; the wall is still there, because it's the reconnect storm itself — not the box size — that overwhelms the new process. Any setup with the WS layer as a separate service avoids this entirely — the WS layer doesn't restart, so there's nothing to recover from. AnyCable ships that shape by default; anyone scaling Socket.io / uWS in production ends up building it themselves. Methodology & per-N walkthrough: bench repo README. Background reading: thundering-herd safety tips.

-
-
+
+

- + - + @@ -662,7 +678,7 @@

CSR with the in-memory adapter doesn't help he

ClientsSocket.io recoverySocket.io recovery (also +CSR, uWS) ReconnectedAnyCableAnyCable OSS & Pro
-

1 vCPU / 0.5 GB Railway box. "Recovery" = wall-clock time until 95% of clients reconnect. AnyCable's WebSocket layer is a separate Go binary, so app deploys don't restart it — no avalanche to recover from at any scale.

+

The split here isn't really library-vs-library — it's in-process vs out-of-process WS. Socket.io, Socket.io+CSR, and uWS all run the WS layer inside the Node app, so railway redeploy restarts it; CSR's in-memory adapter even loses replay state on restart. AnyCable (OSS and Pro) runs the WS layer as a separate Go binary; deploys touch only the app process, never the WS connections. 1 vCPU / 0.5 GB box, "recovery" = wall-clock until 95% of clients reconnect.

@@ -670,26 +686,23 @@

CSR with the in-memory adapter doesn't help he

{{!-- Connection capacity --}} -
-
-
-
-

3. One AnyCable node holds 1,000,000 idle connections — Pro on 19 GB, OSS on 32 GB#

-

- Same Railway box as the jitter benchmark — Pro tier, 32 vCPU / 32 GB, single instance, no Redis or NATS backplane. All four runtimes targeted 1,000,000 idle connections across sharded test clients (each container has its own ~64K outbound-port pool). The table below shows what each held at peak. -

-

- Each runtime hits a different wall. Socket.io accepted 119,826 and rejected ~880K during ramp — the single Node event loop saturates handshakes serially, regardless of memory. Open-source AnyCable held 993,994, capping at the box's 32 GB ceiling (~33 KB / connection); more from there means a bigger box. +

+
+
+
+

3. How much can we scale before we redo the architecture?#

+

+ Your user base will grow, and your realtime layer needs to grow with it. The question isn't how cheap is this today — it's when we 10× users, does the bill or the box cliff before we have time to react? Cost predictability matters more than absolute cost: a $5 box that scales smoothly to 100K users beats a $499 plan that hits a wall at 50K. Three things decide it: how many idle connections one node holds (steady-state headroom), how the same node behaves during a reconnect storm (the avalanche from above), and useful throughput — messages per second × delivery rate, since raw msg/sec without delivery is vanity.

-

- AnyCable Pro held 999,954 on 19 GB with 13 GB headroom — about 1.7× more memory-efficient than OSS at 1M (~19 vs ~33 KB / connection). And uWebSockets.js held 1,018,366 on 5.45 GB — the lightest of the four at ~5.4 KB per connection, because it's a bare wire layer with no broker overhead. The trade-off is what that overhead buys you: the AnyCable footprint is per-stream replay, history retention, and broker framing — features uWS doesn't include. +

+ Same Railway box (32 vCPU / 32 GB), four setups, 1,000,000-connection idle target. Socket.io accepted 119,826 — the single Node event loop saturates handshakes regardless of memory available. uWS held 1,018,366 on 5.45 GB (lightest wire, no replay broker). AnyCable Pro held 999,954 on 19 GB (the lightest setup that includes built-in replay). On cost shape: AnyCable Pro is flat-rate ($1,490/yr, unlimited instances); Pusher is per-connection (Premium $499/mo for 10K, Growth $699/mo for 15K, Plus $899/mo for 20K — 2026 plans); uWS and self-hosted Socket.io are box-priced.

-

- The connection ceiling, when you go looking: comfortably past a million on a single node, on either AnyCable or uWS. The headline of this page is delivery and deploy resilience above — not raw connection count, where Node-embedded uWS and separate-process AnyCable both clear 1M. +

+ Single instance, no Redis or NATS backplane. Socket.io's idle wall is the event loop (saturates at ~120K); AnyCable OSS hits the 32 GB box ceiling (~33 KB/conn); AnyCable Pro and uWS both clear 1M comfortably (Pro at 19 KB/conn with the broker, uWS at 5.4 KB/conn without). Per-test setup & harness: bench repo README.

-
-
+
+
@@ -700,9 +713,9 @@

3. One AnyCable node holds 1,000,00

+ - @@ -746,37 +759,174 @@

3. One AnyCable node holds 1,000,00 + + {{!-- Throughput sub-section — sits inside the same Rubric 3 + wrapper but gets its own sticky media so the table doesn't + fight the capacity table for viewport height. --}} +
+
+

+ On useful throughput — 10,000 subscribers, 100% delivery, 1M deliveries/sec target: the WS-layer ceiling (publisher in the same process as the WS server) is uWS 907K/sec (hand-tuned C++ broadcast), AnyCable 522K/sec, Socket.io+CSR 496K/sec, Socket.io 437K/sec. Apples to apples; this is what each WS server can fan out when nothing is in the way. +

+

+ Once you separate the publisher from the WS layer — the architectural shape AnyCable assumes by default and Socket.io adopts the moment you scale past one Node — both architectures hit similar production-shape ceilings around 50–75K msg/sec at the 1M target. The bottleneck is the publisher and its message bus, not the WS layer's fan-out capacity (Socket.io+CSR has 496K of headroom locally per node; AnyCable's per-node WS layer has 522K). The Socket.io + Redis "in-process publisher" number (773K/sec at 2 nodes) is real, but it's a fundamentally different shape: the publisher and the WS server share a Node process. That co-location is what Socket.io's architecture lets you do; it's also what makes Socket.io fragile on every deploy. Throughput is not AnyCable's headline win — capacity, reliability, and deploy resilience are. Per-test methodology: bench repo README. +

+
+
+
+

Socket.io (1M attempted)119,826 conns~52 KBno
200,000 AC OSS8.35 GB42 KByes
200,000 AC Pro3.56 GB18 KByes
Socket.io (1M attempted)119,826 conns~52 KBno
993,994 AC OSS, RAM-bound32.00 GB (ceiling)33 KByes
999,954 AC Pro19.34 GB19 KByes
1,018,366 uWS5.45 GB5.4 KBno
+ + + + + + + + + + + + + + + + + + + + + +
Outbound deliveries/sec10K/s mild100K/s1M/s stress
WS layer ceiling — publisher in-process
Socket.io default9,99798,717437,063
Socket.io + CSR9,98697,447496,278
AnyCable benchi, embedded9,99699,966522,167
uWS topics9,99793,853907,441
Multi-process — fan-out crosses a message bus
Socket.io + Redis 2 nodes, in-proc pub9,99694,616772,798
Socket.io + Redis 2 nodes, HTTP pool=169,99348,56363,036
AnyCable OSS 1 node, HTTP pool=169,99691,583163,693
AnyCable Pro 1 node, HTTP pool=169,99786,625163,773
AnyCable cluster OSS 2 nodes + NATS, NATS pub9,99347,55372,145
+

Top group: WS server fans out broadcasts to its locally-connected sockets, publisher in the same process. Apples to apples — isolates the WS layer's own fan-out cost. The AnyCable in-process number is from anycable-go's benchi harness (embedded server, loopback httptest.Server) — same hub, same broker, same WS framing. Bottom group: multiple processes, broadcasts cross a network bus — the production shape for either architecture once you scale past one node.

Two findings to take from the bottom group. (1) In-process publishing wins, but it's a different shape. The Socket.io + Redis "in-proc pub" row hits 773K because the publisher and one of the WS servers share an event loop and the publisher has zero network hops to reach instance A's clients (only Redis pub/sub for instance B). This is the best case for Socket.io+Redis, but it requires the app code to live inside one of the WS processes — the architectural assumption AnyCable explicitly rejects. (2) Once the publisher is separated from the WS layer, both architectures hit a similar publisher-rate floor. Socket.io+Redis HTTP pool=16 caps at ~63K and AnyCable cluster NATS pub caps at ~72K — in both cases the 10K clients × 100 messages of fan-out work is finished long before the publisher manages to push the next batch. AnyCable's single-instance HTTP pool=16 wins at 164K because anycable-go's HTTP /_broadcast is a Go binary returning 200 as soon as the broadcast is enqueued, so the 16-way concurrent publisher can keep multiple broadcasts in flight; Socket.io's /_broadcast on the same shape is bound by Express's request handling.

Methodology details. Socket.io + Redis: two Node instances behind one Redis, 10K clients split 5K / 5K. "in-proc pub" runs the publisher inside instance A (io.to().emit() in the same process). "HTTP pool=16" has the bench-runner POSTing to instance A's /_broadcast at 16-way concurrency — instance A then fans out locally to its 5K subs and publishes to Redis for instance B. AnyCable cluster: two OSS anycable-go instances behind a shared NATS service, 10K clients split 5K / 5K, bench-runner publishes directly to NATS (the realistic shape for a NATS-backed AnyCable cluster). uWS has no built-in adapter; multi-node uWS is DIY.

+
+
+
+
+
+ + {{!-- RUBRIC 4: Operational surface — new section. The reader's + question is "when this breaks at 3am, can my team diagnose + and fix it?" Three sub-questions: how many moving parts, + what's instrumented out of the box, and is the project + actively maintained? --}} +
+
+
+
+

4. When this breaks at 3am, can my team diagnose and fix it?#

+

+ Realtime infrastructure breaks at the worst possible time, often in the middle of the night. When it does, the on-call engineer is also the person shipping features tomorrow morning — every incident burns two days of velocity. The choice here is whether the system is observable, debuggable, and well-supported enough that an incident is a two-hour fix instead of a two-day investigation. Easy to set up doesn't matter — setup is one Saturday, operations is forever. Three things decide it: how many moving parts the system has, what's instrumented out of the box, and whether someone actually responds when you file an issue. +

+

+ AnyCable ships Prometheus + StatsD and a /health endpoint in the box; Socket.io ships an Admin UI; uWS ships nothing observability-wise. All three projects are actively maintained — the table breaks down commit / contributor signal and known failure modes. +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + +
Operational surfaceSocket.io+ CSRuWSAC OSSAC Pro
Process count1 (Node)1 (Node)1 (Node)2 (app + Go)2 (app + Go)
Prometheus endpointAdmin UI onlyAdmin UI onlyDIYBuilt-inBuilt-in
Health check endpointDIYDIYDIY/health/health
Structured logsPluggablePluggableDIYJSON / textJSON / text
Graceful shutdown controlsManualManualManualConfigurable drainConfigurable drain
Replay buffer storagen/aMemory or Postgres adaptern/aMemory or NATS JS / RedisEmbedded broker
Commits / 90d23231012929
Maintainer supportGitHub issuesGitHub issues"Bug reports only"GitHub + DiscordGitHub + Discord + commercial
+

Observability splits by where the WS layer runs — in-process Node options inherit Node's defaults (Admin UI at best, DIY for everything else); AnyCable's separate Go binary ships Prometheus, /health, structured logs, and a configurable drain. Pro adds commercial support and the embedded broker; the day-to-day surface is otherwise OSS's. On commit counts: uWS's 101 commits / 90d reflects active wire-level optimization work on a small surface (one C++ library, one author); AnyCable's 29 commits / 90d reflects a multi-language ecosystem in maintenance mode (8+ years in production, stable protocol, breaking changes are rare on purpose). High commit velocity at the framework layer is a debugging-surface signal, not necessarily a quality one — for a piece of infrastructure you don't want changing under you, lower is fine. Commit / contributor counts: 90 days ending 2026-05-09, via the GitHub API on socketio/socket.io, uNetworking/uWebSockets.js, anycable/anycable. uWS support quote: READMORE.md.

+
+
+
+
+
+ + {{!-- RUBRIC 5: Optionality / exit cost — new section. The + reader's question is "if we're wrong about this, what does + it cost to leave?" Two pillars: protocol portability (can I + swap the server without rewriting the client?) and license + (is the open-source path real or a feeder for a SaaS?) --}} +
+
+
+
+

5. If we're wrong about this, what does it cost to leave?#

+

+ Vendors die, get acquired, pivot, or quietly change pricing. Choosing realtime infrastructure is a multi-year bet, and the cost of unwinding the wrong bet matters as much as the cost of running the right one. Real optionality is about the protocol, not just the license — an open-source tool with a custom protocol that nothing else speaks is still de facto lock-in. Two things decide it: whether you can swap the server out without rewriting the client, and whether the open-source path leads to the same product as the commercial one (or is a teaser for a paywalled fork). +

+

+ AnyCable speaks two open protocols: Action Cable (a documented JSON-over-WS protocol with public extensions) and Pusher protocol (drop-in for Pusher / Soketi / Laravel Reverb). The TypeScript client lives in @anycable/core and uses the standard browser WebSocket API underneath, so you can move clients off AnyCable without rewriting them. Socket.io is the inverse: protocol is documented but the ecosystem is officially JS-only, so the client is locked to the server. AnyCable Pro is not a paywalled fork — same protocol surface as OSS; you can move between OSS, Pro, and Managed without code changes. +

+
+
+
+ + + + + + + + + + + + + + + + + + + +
Optionality dimensionSocket.io+ CSRuWSAC OSSAC Pro
Protocol spec publicYesYesn/a (raw WS)YesYes
Other servers speak itNo (JS-only)No (JS-only)Any WS libDocumented WSDocumented WS
Other protocols supportedNoneNonen/aPusher (drop-in)Pusher (drop-in)
Client SDK portabilityLocked to serverLocked to serverNative WSNative WS + TS wrapperNative WS + TS wrapper
License (server)MITMITApache 2.0 + renameMITCommercial
Migration to/from the commercial tiern/an/an/aSame protocol → ProSame protocol → OSS
+

The optionality story tracks the architecture choice too: Socket.io variants share lock-in (the JS-only ecosystem clients can't read messages from another server), uWS has none (raw WebSocket protocol — any client speaks it), AnyCable has none (documented Action Cable + Pusher protocols, native WebSocket underneath). Pro and OSS are the same surface; you can move between them, or to AnyCable+ Managed, without code changes. Action Cable spec: docs.anycable.io/misc/action_cable_protocol. Pusher protocol: docs.anycable.io/anycable-go/pusher. Socket.io protocol spec: socketio/socket.io-protocol — reference implementations are TypeScript only.

+
+
+
{{!-- Migration path for Node.js teams --}} -
-
-
-
-

Migrating from Socket.io: replace the broadcast, keep the app#

-

+

+
+
+
+

Migrating from Socket.io: replace the broadcast, keep the app#

+

Your Node.js app stays. Swap io.to().emit() for an HTTP POST to AnyCable's broadcast endpoint. Auth, validation, DB access — all unchanged.

-

+

On the client: socket.io-client@anycable/core. The API is similar (connect, subscribe, receive).

-

+

Trade-off: one extra process (anycable-go, a Go binary). Your Node.js app becomes stateless for WebSockets.

-

Migration checklist — what changes in your code:

-
    +

    Migration checklist — what changes in your code:

    +
    1. Replace io.to(room).emit(event, payload) with an HTTP POST to anycable-go's /_broadcast.
    2. Swap socket.io-client for @anycable/core on the frontend (or @anycable/web for browser-only). Same connect / subscribe / receive shape.
    3. Drop the socket.io server dependency. Your app no longer holds WebSocket connections — anycable-go does.
    4. Run anycable-go alongside your app (one Go binary, Docker image, or our Managed tier — same protocol either way).
-
-
// Before: Socket.io (embedded in your app)
+              
+
// SERVER — broadcast
+// Before: io.to().emit() in your Node app
 io.to('chat:42').emit('message', payload);
 
-// After: broadcast to AnyCable via HTTP
+// After: HTTP POST to anycable-go
 await fetch('http://anycable:8080/_broadcast', {
   method: 'POST',
   headers: { 'Content-Type': 'application/json' },
@@ -784,19 +934,33 @@ 

Migrating from Socket.io: replace stream: 'chat:42', data: JSON.stringify(payload), }), -});

+}); + + +// CLIENT (browser / React / RN) — subscribe +// Before: socket.io-client +import { io } from 'socket.io-client'; +const socket = io('/chat'); +socket.emit('subscribe', 'chat:42'); +socket.on('message', render); + +// After: @anycable/core (native WebSocket inside) +import { createCable } from '@anycable/core'; +const cable = createCable('ws://anycable:8080/cable'); +const channel = cable.streamFrom('chat:42'); +channel.on('message', render);
{{!-- Try-it-in-your-stack: low-friction links to runnable JS code --}} -
-
-
-
-

Try it in your stack#

-

+

+
+
+
+

Try it in your stack#

+

Three places to start: read the code, run a working demo, or wire it into a serverless setup.

@@ -822,35 +986,28 @@

Try it i

{{!-- Feature comparison --}} -
-
-
-
-

What you don't have to build

-

+

+
+
+
+

What you don't have to build

+

Socket.io gives you WebSocket transport and rooms. uWebSockets.js gives you a faster transport. Both stop there. Everything else — reliability, presence, auth, clustering, monitoring — is weeks of engineering plus months of hardening, on a single Node event loop you also have to keep alive across deploys.

-

+

AnyCable ships these as primitives, hardened in production since 2017, in a process you don't have to restart when your app deploys.

-
-
+
+
- - - - - - - - + @@ -865,7 +1022,7 @@

What you don't have to build

Feature Default
Socket.io
Socket.io
+ CSR
uWeb-
Sockets.js
AnyCableAnyCable
OSS & Pro
MonitoringAdmin UIAdmin UIDIYPrometheus & StatsD
-

uWS solves Socket.io's wire-overhead problem — not its framework gaps. Every feature beyond raw transport is still DIY, on the same single Node process. The "what you don't have to build" question is the same.

+

uWS solves Socket.io's wire-overhead problem — not its framework gaps. Every feature beyond raw transport is still DIY, on the same single Node process. AnyCable OSS and Pro share every feature in this table; Pro's differentiators are per-connection memory efficiency, the embedded broker, and commercial support (see operations).

@@ -873,20 +1030,20 @@

What you don't have to build

{{!-- When Socket.io is right / Proven at scale --}} -
-
-
-
-

When Socket.io is the right choice

-

+

+
+
+
+

When Socket.io is the right choice

+

Small Node.js app. Prototype. Custom protocol you control end-to-end. Socket.io is well-documented, widely used, and free.

-

+

If your users are on mobile or switching networks, you need delivery guarantees. If you deploy more than occasionally, you need connections to survive deploys. AnyCable: both by default.

-
-
+
+
Proven at scale
{{!-- Doximity quote — public; from On Rails Podcast. Trimmed to focus on the deploy-resilience point. --}} @@ -906,13 +1063,61 @@

When Socket.io is the right choice

+ {{!-- APPENDIX — high-value mechanism explanations cut from the + rubric sections above. Slim by design; the deep-dive + methodology + per-test walkthroughs live in the benchmark + repo README, linked from each rubric. --}} +
+
+
+
+

Appendix: how the numbers come together#

+

+ The high-value mechanism behind each rubric, in one place. Full per-test methodology, raw output, and reproduction steps live in the benchmark repo README. +

+ +

Why the message loss is structural, not a bug

+

+ Each jitter event creates d seconds of blind window. Over publishing window T with N events, expected loss ≈ (d × N) / T. For our run: 1.3 s × ~8 / 60 ≈ 17% upper bound; we measured 13.1% (lower because not every event fully overlaps publishing). Default Socket.io and naked uWS lose messages in proportion to frequency × disruption — tuning doesn't change the curve, only adding replay does. +

+ +

Why a 1-second blip becomes a multi-second recovery tail

+

+ Every realtime client — socket.io-client, @anycable/core, anything that wants to stay up under load — reconnects with a 0.5–5 s backoff window to avoid stampeding the server. Any TCP-level blip becomes multiple seconds of offline window by design. Without delivery guarantees, those seconds are lost messages; with replay, they're delayed. The CSR → AnyCable gap on p99 (~8 s vs ~6 s) is the replay protocol itself: CSR drains a buffered packet list per socket through one event loop; AnyCable's history replay is per-stream, batched, parallel. +

+ +

CSR's known constraints

+
    +
  • Opt-in. Most production Socket.io apps don't enable it.
  • +
  • Adapter compatibility. Incompatible with the most common scaling adapter (Redis pub/sub). Multi-node CSR needs Redis Streams or MongoDB; Postgres + Cluster adapters are WIP. Source: Socket.io docs.
  • +
  • Restart-fragile by default. The in-memory adapter wipes all state on every server restart.
  • +
  • Fallible by design. "The recovery will not always be successful" — their words. Application-level reconciliation is still required.
  • +
+ +

Why the deploy avalanche cliffs at ~25K (Socket.io) and ~90K (uWS)

+

+ The bottleneck is server-resource, not library overhead: handshake serialization, kernel rate limiting, and TCP backpressure. On a 1 vCPU / 0.5 GB box, Socket.io's ~25K cliff is the cold-start OOM — memory hits 95% of cap right before redeploy, the post-redeploy reconnect storm OOMs the new container before any client establishes a session. uWS pushes that cliff past 90K but the curve is still non-linear (20K → 25K already takes recovery from 4.6 s to 57 s). AnyCable's separate Go binary doesn't restart on app deploys, so there's no avalanche to recover from at any scale. +

+ +

AnyCable's epoch + offset replay protocol, in two paragraphs

+

+ AnyCable's protocol identifier is actioncable-v1-ext-json — an extended Action Cable. Each stream is tracked by epoch (server-cycle id) + offset (monotonic per-stream counter). On reconnect, the client sends a history command; the server replays the missed range as a batch and acks with confirm_history (or reject_history if the offset is older than the retention window). +

+

+ Retention is per stream: history_limit (default 100 messages), history_ttl (default 300 s). With NATS JetStream or Redis brokers, history survives restart and works across nodes. Default behavior — not opt-in, not experimental. Full spec: docs.anycable.io/misc/action_cable_protocol. +

+
+
+
+
+ {{!-- FAQ — accordion. Native
so it works without JS; closed-by-default keeps the page short for skimmers. --}} -
-
-
-
-

FAQ#

+
+
+
+
+

FAQ#

Does AnyCable replace Socket.io, or work alongside it? @@ -926,7 +1131,7 @@

FAQ<
How does AnyCable compare on performance? -
In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), we pushed four setups to a 1,000,000-connection idle target on a single instance. Socket.io accepted 119,826 and rejected ~880K — its single Node event loop saturated under handshake load. uWebSockets.js held 1,018,366 on 5.45 GB — the lightest bare wire layer, but with no built-in replay or broker. Open-source AnyCable held 993,994, peaking at the box's 32 GB RAM ceiling (~33 KB / connection). AnyCable Pro held 999,954 on only 19 GB — ~1.7× more memory-efficient than OSS at the same load, with 13 GB headroom remaining. At 10,000 dynamic clients with simulated jitter (1-second TCP drops every ~15 s), AnyCable delivered all 1.2M messages with a ~6 s p99 replay tail (vs ~8 s for Socket.io+CSR); replay-less setups (default Socket.io, uWS) lost ~13–14% of messages during the jitter blind windows. Run the numbers yourself: github.com/irinanazarova/anycable-socketio-benchmarks.
+
Three workloads, same 32 vCPU / 32 GB Railway box.

1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB (lightest bare wire layer; no replay broker). AnyCable Pro holds 999,954 on 19 GB — the lightest setup with built-in replay. Open-source AnyCable holds 993,994 at the box's 32 GB ceiling. Socket.io caps at 119,826 connections — its single Node event loop saturates handshakes regardless of memory.

10K reconnecting clients under jitter (WiFi-drop pattern, 1.2M expected deliveries): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR). Default Socket.io and uWS lose ~13–14% of messages — both at-most-once.

WS-layer throughput at 1M deliveries/sec target, publisher in-process (apples-to-apples WS fan-out): uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. Multi-process — fan-out crosses a network bus (publisher separated from WS layer): Socket.io + Redis with HTTP pool=16 publisher caps at ~63K/sec; AnyCable cluster (2 anycable-go + NATS, NATS publisher) caps at ~72K/sec — both publisher-bound, not WS-layer bound. The 773K Socket.io + Redis "in-process publisher" number (publisher colocated with one of the WS nodes) is a different architectural shape — one Socket.io's design supports but AnyCable's deliberately rejects. AnyCable single-instance HTTP pool=16 still wins at 164K because anycable-go's broadcast handler returns 200 fast, letting the 16-way publisher keep multiple broadcasts in flight. Reproduce: github.com/irinanazarova/anycable-socketio-benchmarks.
@@ -956,12 +1161,12 @@

FAQ<
Is there a managed (hosted) AnyCable? -
Yes. AnyCable+ is the managed tier — zero ops, free up to 2,000 concurrent connections, paid plans above that. Same protocol and feature surface as self-hosted, so you can switch in either direction without changing app code.
+
Yes. AnyCable+ is the managed tier — zero ops, free for early users, paid plans for scale. Same protocol and feature surface as self-hosted, so you can switch in either direction without changing app code.
How does the cost compare to Pusher or Ably? -
Pusher and Ably charge per concurrent connection — costs scale linearly with your user base. AnyCable Pro (self-hosted, unlimited connections and instances) is a flat-rate annual license. AnyCable+ Managed is free up to 2,000 concurrent connections and priced predictably above that. At 10,000+ connections, the flat-rate or self-hosted options typically save thousands per month versus per-connection pricing.
+
Pusher and Ably charge per concurrent connection — costs scale linearly with your user base. AnyCable Pro (self-hosted, unlimited connections and instances) is a flat-rate annual license — $1,490/yr with a 2-month free trial. AnyCable+ Managed is free for early users. At 10K+ concurrent connections, the flat-rate or self-hosted options typically save thousands per month versus per-connection pricing.
@@ -971,7 +1176,12 @@

FAQ<
What happens if AnyCable itself restarts? -
Clients reconnect and resume from the last offset they saw — missed messages are replayed (up to a configurable retention window). Restarting AnyCable is rare: you deploy your app, not the WebSocket layer, because they're decoupled.
+
Less often than app deploys (you don't ship anycable-go on every code change), but it does happen — version upgrades, config changes, host reboots. The behavior depends on the broker: with an external broker (NATS JetStream or Redis Streams), replay state survives the restart, so clients reconnect and resume from the last offset they saw. With the default in-memory broker (or AnyCable Pro's embedded broker), replay state is lost on restart and clients fall back to "live from now" — same delivery profile as a cold reconnect to default Socket.io. For production, run multiple anycable-go instances behind a load balancer and a shared broker; restart one at a time during upgrades and clients seamlessly reconnect to the others.
+
+ +
+ How do I run anycable-go in production? +
One Go binary. Docker images at hub.docker.com/r/anycable/anycable-go (and anycable-go-pro for Pro). Minimum to run: docker run -p 8080:8080 anycable/anycable-go --broadcast_key=YOUR_SECRET. Configurable via flags or ANYCABLE_* env vars. Health endpoint at /health, Prometheus metrics at /metrics. Graceful drain on SIGTERM (configurable via --shutdown_timeout) so rolling deploys don't drop connections. Behind a load balancer with sticky sessions for multi-instance setups. Helm chart and Fly/Railway templates linked from the docs.

@@ -979,22 +1189,61 @@

FAQ<

- {{!-- CTA — leads with self-hosted Pro (the monetized product), - with free Managed as the lower-friction secondary path. - Open source / GitHub get a single trailing line — already - covered in the FAQ above. --}} -
+ {{!-- When AnyCable is the WRONG choice — voluntary disqualification. + Sits between FAQ and CTA on purpose: a CTO reading this far is + close to deciding, and naming the cases where AnyCable is overkill + or unsuited builds more trust than another green checkmark. --}} +
+
+
+
+

When AnyCable is the wrong choice#

+

+ This page argued for AnyCable on the rubrics above. Honesty cuts both ways — here are the cases where you should keep what you have, or pick something else. If any of these describe you, AnyCable isn't the right migration. +

+

+ Real-time infrastructure choices are sticky and they matter; "we don't need this yet" is a valid answer. +

+
+
+
+
+ You have under ~500 concurrent connections + Default Socket.io is fine. You don't have the deploy storm, you don't have the memory pressure, and a separate WS process is complexity you don't yet need. Re-evaluate when messages drop under jitter, or deploys flicker the product. +
+
+ Your app server is the bottleneck + AnyCable scales the WebSocket layer — not your database queries, auth middleware, or business logic. If your slow request is the slow query, AnyCable doesn't help. Profile the app first. +
+
+ You need binary or transport-level features + AnyCable speaks JSON over WebSocket. If you need Protobuf / MsgPack framing, QUIC, datagrams, or sub-millisecond financial-tier tick distribution, this is the wrong tool. Build on raw uWS or a purpose-built transport. +
+
+ Your scale is stable and you deploy rarely + The deploy-resilience and headroom story matters because deploys hit users and traffic grows. If you ship weekly at off-hours and your concurrent count is a stable plateau, default Socket.io stays the simpler answer. +
+
+
+
+
+
+ + {{!-- CTA — leads with evaluation paths (OSS local, then Managed + free trial). Pro pricing is mentioned in the footnote because + a skeptical CTO won't pay $1,490 before running anything. --}} +
-

Drop AnyCable into your Node.js app today

+

Try AnyCable in your stack

- AnyCable Pro runs on your infrastructure: unlimited connections and instances, flat $1,490/year, with a 2-month free trial. Or skip the deploy and start with free Managed AnyCable. + The shortest evaluation path: run anycable-go locally with one Docker command and point your Node app at it. Or skip the deploy and try the hosted version.

- Open source under MIT — anycable/anycable. Built by Evil Martians, in production since 2017 at Doximity, CompanyCam, Headway, and 30+ others. Benchmark code: github.com/irinanazarova/anycable-socketio-benchmarks. + When you're ready for production, AnyCable Pro is flat $1,490/yr (unlimited instances) with a 2-month free trial. Open source under MIT — anycable/anycable. Built by Evil Martians, in production since 2017 at Doximity, CompanyCam, Headway, and 30+ others. Benchmark code: github.com/irinanazarova/anycable-socketio-benchmarks.

@@ -1031,7 +1280,7 @@

Drop AnyCable into your Node.js app today

"name": "How does AnyCable compare on performance?", "acceptedAnswer": { "@type": "Answer", - "text": "In our Railway benchmark (Pro tier, 32 vCPU / 32 GB RAM allocated), we pushed four setups to a 1,000,000-connection idle target on a single instance. Socket.io accepted 119,826 and rejected ~880K — its single Node event loop saturated under handshake load. uWebSockets.js held 1,018,366 on 5.45 GB — the lightest bare wire layer, but with no built-in replay or broker. Open-source AnyCable held 993,994, peaking at the box's 32 GB RAM ceiling (~33 KB / connection). AnyCable Pro held 999,954 on only 19 GB — about 1.7× more memory-efficient than OSS at the same load. At 10,000 dynamic clients with simulated jitter, AnyCable delivered all 1.2M messages with a ~6 s p99 replay tail (vs ~8 s for Socket.io+CSR); replay-less setups (default Socket.io, uWS) lost about 13–14% of messages during the jitter blind windows. Source: github.com/irinanazarova/anycable-socketio-benchmarks." + "text": "Three workloads on the same 32 vCPU / 32 GB Railway box. (1) 1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB (lightest bare wire, no replay broker); AnyCable Pro holds 999,954 on 19 GB (lightest setup with built-in replay); open-source AnyCable holds 993,994 at the box's 32 GB ceiling; Socket.io caps at 119,826 because its single Node event loop saturates handshakes regardless of memory. (2) 10K reconnecting clients under jitter (WiFi-drop pattern, 1.2M expected deliveries): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR); default Socket.io and uWS lose ~13–14% of messages, both being at-most-once. (3) WS-layer throughput at 1M deliveries/sec target, publisher in-process: uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. Multi-process — fan-out crosses a network bus (publisher separated from WS layer): Socket.io + Redis HTTP pool=16 publisher caps at ~63K/sec; AnyCable cluster (2 anycable-go + NATS) caps at ~72K/sec — both publisher-bound. The 773K Socket.io + Redis 'in-process publisher' number is a different shape (publisher colocated with one of the WS nodes), which Socket.io supports and AnyCable rejects. AnyCable single-instance HTTP pool=16 wins at 164K because anycable-go's broadcast handler is faster than Socket.io's at returning 200. Throughput is not AnyCable's headline win on this page; capacity, reliability, and deploys are. Source: github.com/irinanazarova/anycable-socketio-benchmarks." } }, { @@ -1079,7 +1328,7 @@

Drop AnyCable into your Node.js app today

"name": "Is there a managed (hosted) AnyCable?", "acceptedAnswer": { "@type": "Answer", - "text": "Yes. AnyCable+ is the managed tier — zero ops, free up to 2,000 concurrent connections, paid plans above that. Same protocol and feature surface as self-hosted, so you can switch in either direction without changing app code." + "text": "Yes. AnyCable+ is the managed tier — zero ops, free for early users, paid plans for scale. Same protocol and feature surface as self-hosted, so you can switch in either direction without changing app code." } }, { @@ -1087,7 +1336,7 @@

Drop AnyCable into your Node.js app today

"name": "How does the cost compare to Pusher or Ably?", "acceptedAnswer": { "@type": "Answer", - "text": "Pusher and Ably charge per concurrent connection — costs scale linearly with your user base. AnyCable Pro (self-hosted, unlimited connections and instances) is a flat-rate annual license. AnyCable+ Managed is free up to 2,000 concurrent connections and priced predictably above that. At 10,000+ connections, the flat-rate or self-hosted options typically save thousands per month versus per-connection pricing." + "text": "Pusher and Ably charge per concurrent connection — costs scale linearly with your user base. AnyCable Pro (self-hosted, unlimited connections and instances) is a flat-rate annual license — $1,490/yr with a 2-month free trial. AnyCable+ Managed is free for early users. At 10K+ concurrent connections, the flat-rate or self-hosted options typically save thousands per month versus per-connection pricing." } }, { @@ -1103,7 +1352,15 @@

Drop AnyCable into your Node.js app today

"name": "What happens if AnyCable itself restarts?", "acceptedAnswer": { "@type": "Answer", - "text": "Clients reconnect and resume from the last offset they saw — missed messages are replayed (up to a configurable retention window). Restarting AnyCable is rare: you deploy your app, not the WebSocket layer, because they're decoupled." + "text": "Less often than app deploys, but it does happen — version upgrades, config changes, host reboots. With an external broker (NATS JetStream or Redis Streams), replay state survives the restart, so clients reconnect and resume from the last offset they saw. With the default in-memory broker, replay state is lost on restart and clients fall back to live-from-now. For production, run multiple anycable-go instances behind a load balancer with a shared broker; restart one at a time during upgrades so clients seamlessly reconnect to the others." + } + }, + { + "@type": "Question", + "name": "How do I run anycable-go in production?", + "acceptedAnswer": { + "@type": "Answer", + "text": "One Go binary. Docker images at hub.docker.com/r/anycable/anycable-go. Minimum: docker run -p 8080:8080 anycable/anycable-go --broadcast_key=YOUR_SECRET. Configurable via flags or ANYCABLE_* env vars. Health endpoint at /health, Prometheus metrics at /metrics. Graceful drain on SIGTERM so rolling deploys don't drop connections. Behind a load balancer with sticky sessions for multi-instance. Helm chart and Fly/Railway templates linked from the docs." } } ] @@ -1117,11 +1374,11 @@

Drop AnyCable into your Node.js app today

"@context": "https://schema.org", "@type": "TechArticle", "headline": "AnyCable vs Socket.io vs uWebSockets.js: 100% delivery, deploy-resilient WebSockets, 1M idle connections per node", - "description": "Same Railway box, four-way comparison: Socket.io accepted 119,826 (single Node event loop saturated); uWebSockets.js held 1,018,366 on 5.45 GB (lightest wire, no replay); open-source AnyCable held 993,994 (32 GB RAM ceiling); AnyCable Pro held 999,954 on 19 GB. Replay-less setups (default Socket.io, uWS) lost about 13–14% of messages under jitter; AnyCable and Socket.io+CSR delivered 100%. Only AnyCable's separate-process architecture survives every deploy without dropping a connection. All numbers reproducible from the open-source bench repo.", + "description": "Same Railway box, four setups: Socket.io accepted 119,826 (single Node event loop saturated); uWebSockets.js held 1,018,366 on 5.45 GB (lightest wire, no replay); open-source AnyCable held 993,994 (32 GB RAM ceiling); AnyCable Pro held 999,954 on 19 GB. Replay-less setups (default Socket.io, uWS) lost ~13–14% of messages under jitter; AnyCable and Socket.io+CSR delivered 100%. In-process WS (Socket.io, +CSR, uWS) dies on every Node deploy and past ~20K connections doesn't recover; AnyCable runs WS as a separate process so deploys don't touch it. In-process WS-layer throughput: uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. All numbers reproducible from the open-source bench repo.", "url": "https://anycable.io/compare/socket-io", "mainEntityOfPage": "https://anycable.io/compare/socket-io", "datePublished": "2026-04-30", - "dateModified": "2026-05-08", + "dateModified": "2026-05-13", "author": { "@type": "Organization", "name": "AnyCable team", diff --git a/src/modules/blocks/compare.scss b/src/modules/blocks/compare.scss index 393846d..0cc382e 100644 --- a/src/modules/blocks/compare.scss +++ b/src/modules/blocks/compare.scss @@ -1,9 +1,10 @@ // ===================================================================== // Compare-page styles — anycable.io/compare/socket-io // -// All rules are scoped under .compare-page so global components like -// .about-slide and .slide-show__frame can be safely re-used elsewhere -// without inheriting the comparison-page treatment. +// All rules are scoped under .compare-page. The page uses its own +// compare-rubric / compare-frame / compare-impact-* primitives instead +// of reusing the .about-slide / .slide-show__frame slideshow classes +// so the BEM naming reflects the actual content (no "slides" here). // // Global tokens ($accentPrimaryColor, $backgroundSecondaryColor, etc.) // come from src/modules/variables.scss, imported in index.scss. @@ -58,7 +59,7 @@ $compare-best: #16a34a; // green winner accent .compare-page { // Notebook-feel dotted background — continuous from hero through CTA. - // Gray panels inside about-slide paint over it. + // Gray .compare-rubric__content panels paint over it on the left side. background-color: $backgroundPrimaryColor; background-image: radial-gradient( circle, @@ -67,82 +68,6 @@ $compare-best: #16a34a; // green winner accent ); background-size: 22px 22px; - // !important here defeats per-slide bg rules set globally on - // .slide.about-slide, .slide.cases-slide, etc. — we want every - // slide on this page to show the dotted page bg through. - .slide { - background: transparent !important; - } - - - // ------------------------------------------------------------------- - // Section rhythm - // ------------------------------------------------------------------- - // Padding lives on the COLUMNS, not the slide. The slide stays - // unpadded so the gray left-column bg flows continuously between - // adjacent sections; the columns each get top + bottom padding so - // their content (FAQ items, tables, code, cards) never sits flush - // against a gray edge. - // - // We zero out __title margin-top because the column padding above - // already handles the H2's top spacing — the global 120px would - // double-stack. - .about-slide__content, - .about-slide__content-full, - .about-slide__media { - padding-top: 80px; - padding-bottom: 80px; - } - - .about-slide__title { - margin-top: 0; - } - - // Right column: transparent so the dotted page bg shows through - // instead of the global white panel fill. - .about-slide__media { - background-color: transparent !important; - background-image: none !important; - } - - - // ------------------------------------------------------------------- - // Mobile + tablet: bring the right column (data, tables, code, charts) - // back into view. The global about-slide hides .about-slide__media on - // any screen ≤1023px; that's intentional for marketing pages where the - // media is decorative, but on this comparison page the media IS the - // argument — the benchmark tables, scaling tables, code samples, and - // feature matrix all live there. Without this override, mobile and - // tablet users see only the prose, which is roughly half the page. - // - // Two coordinated changes: - // 1. Section flips to flex-direction: column so the media stacks - // below the content instead of trying to share a row. - // 2. Media gets display:flex back, full width, and its own bottom - // padding (the column-stack gap is provided by content's bottom - // padding + media's lack of top padding so they read as one - // visual unit). - // - // Inside the media, tables and
 blocks may be wider than the
-  // narrow viewport; we switch the rounded-frame's overflow from hidden
-  // to auto so the data scrolls horizontally rather than getting cropped.
-  @include mediaMax($tablet) {
-    .about-slide__section {
-      flex-direction: column;
-    }
-
-    .about-slide__media {
-      display: flex;
-      width: 100%;
-      padding: 0 0 48px;
-    }
-
-    // overflow-x:auto for the frame and pre blocks lives in their own
-    // rules below — placing it here would source-order-lose to the
-    // later `overflow: hidden` shorthand on .slide-show__frame.
-  }
-
-
   // -------------------------------------------------------------------
   // Mobile: collapse multi-column grids to a single column. The
   // try-it-grid uses auto-fit at 280px minmax which technically already
@@ -187,16 +112,337 @@ $compare-best:           #16a34a;  // green winner accent
 
 
   // -------------------------------------------------------------------
-  // Sub-section spacing inside long prose columns (architecture, each
-  // pillar). The first h3 sits flush to the lead paragraph; subsequent
-  // h3s need breathing room above. Replaces the per-element
-  // style="margin-top: 32px" sprinkled across the markup.
+  // compare-rubric: the page's main section wrapper. Replaces the
+  // formerly-reused `.slide.about-slide` combo (every rubric, the
+  // architecture intro, the migration section, etc.). Owns the
+  // two-column content/media layout, the sticky media variant, and
+  // mobile reflow. Self-contained: pulls in the rules previously
+  // inherited from slide.scss / about-slide.scss / compare.scss
+  // overrides so this page no longer reuses cross-page slideshow CSS.
+  // -------------------------------------------------------------------
+  .compare-rubric {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 0;
+    scroll-margin-top: 64px;
+    background: transparent;
+
+    &__wrapper {
+      width: 100%;
+      max-width: 1920px;
+      display: flex;
+      flex-direction: column;
+    }
+
+    &__section {
+      display: flex;
+      flex-direction: row;
+      width: 100%;
+      align-items: stretch;
+      position: relative;
+
+      &:last-of-type {
+        margin-bottom: 0;
+        border-bottom: 1px solid $borderPrimaryColor;
+      }
+
+      @include mediaMax($tablet) {
+        padding: 0 24px;
+        flex-direction: column;
+        background-color: $backgroundSecondaryColor;
+      }
+    }
+
+    // Left column: the prose. Gray fill, vertical 80px gutter so H2 and
+    // bullets don't sit flush against section edges.
+    &__content {
+      width: 50%;
+      display: flex;
+      flex-direction: column;
+      align-items: stretch;
+      height: auto;
+      background-color: $backgroundSecondaryColor;
+      border-right: 1px solid $borderPrimaryColor;
+      padding: 80px 64px;
+      z-index: 2;
+      position: relative;
+
+      @include mediaMax($tablet) {
+        width: 100%;
+        border-right: none;
+        padding: 0;
+
+        &:first-child { padding-top: 48px; }
+      }
+
+      // For sections without a media partner (FAQ, code samples).
+      &--full {
+        width: 100%;
+        border-right: none;
+      }
+
+      // Sub-section spacing in long prose columns: subsequent h3 subtitles
+      // need breathing room above the previous paragraph.
+      h3.compare-rubric__subtitle + p + h3.compare-rubric__subtitle,
+      h3.compare-rubric__subtitle ~ h3.compare-rubric__subtitle {
+        margin-top: 32px;
+      }
+    }
+
+    // Right column: the data. Transparent so the dotted page bg shows
+    // through (no white panel painted over it).
+    &__media {
+      width: 50%;
+      padding: 80px 64px;
+      background: transparent;
+      z-index: 1;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+
+      // On the comparison page the media IS the argument — tables,
+      // charts, code samples. Keep it visible on mobile/tablet (the
+      // global about-slide hides it ≤1023px; we don't).
+      @include mediaMax($tablet) {
+        display: flex;
+        width: 100%;
+        padding: 0 0 48px;
+      }
+
+      &--align-top {
+        align-self: flex-start;
+        margin-bottom: 48px;
+      }
+
+      // Pushes the media column down so it aligns with prose paragraph
+      // ~start (after the H2 title). Used where the H2 lives only on
+      // the left and the media needs to feel aligned with the lead p.
+      &--top {
+        margin-top: 120px;
+
+        @include mediaMax($tablet) {
+          margin-top: 24px;
+        }
+      }
+
+      // Pins the media column while the reader scrolls long prose.
+      &--sticky {
+        align-self: flex-start;
+        position: sticky;
+        top: 80px; // 60px page header + 20px breathing room
+        max-height: calc(100vh - 100px);
+        overflow-y: auto;
+
+        @include mediaMax($tablet) {
+          position: static;
+          max-height: none;
+          overflow: visible;
+        }
+      }
+
+      // 
 blocks inside the media column (code samples, ASCII charts)
+      // get the same compact dark slab as compare-code-block.
+      pre {
+        background-color: $compare-bg-code;
+        border-radius: $compare-radius;
+
+        @include mediaMax($tablet) {
+          overflow-x: auto;
+          -webkit-overflow-scrolling: touch;
+        }
+      }
+    }
+
+    &__title {
+      font-size: 46px;
+      line-height: 1.2;
+      font-weight: 700;
+      margin: 0 0 56px;
+      scroll-margin-top: $compare-header-offset;
+
+      @include mediaMax($mobile) {
+        font-size: 40px;
+        line-height: 1.3;
+        margin-bottom: 24px;
+      }
+
+      .heading-anchor {
+        margin-left: 8px;
+        color: $compare-text-faint;
+        font-weight: 400;
+        text-decoration: none;
+        opacity: 0;
+        transition: opacity 0.15s;
+      }
+
+      &:hover .heading-anchor { opacity: 1; }
+      .heading-anchor:hover   { color: $accentPrimaryColor; }
+    }
+
+    &__subtitle {
+      font-size: 20px;
+      line-height: 1.6;
+      font-weight: 700;
+      align-self: flex-start;
+      margin-bottom: 24px;
+      scroll-margin-top: $compare-header-offset;
+
+      .heading-anchor {
+        margin-left: 8px;
+        color: $compare-text-faint;
+        font-weight: 400;
+        text-decoration: none;
+        opacity: 0;
+        transition: opacity 0.15s;
+      }
+
+      &:hover .heading-anchor { opacity: 1; }
+      .heading-anchor:hover   { color: $accentPrimaryColor; }
+    }
+
+    // Eyebrow above a section title (e.g. "Impact" above Rubric 1's
+    // affected-workloads section). Replaces .cases-slide__pretitle.
+    &__eyebrow {
+      font-size: 20px;
+      line-height: 1.6;
+      color: $accentPrimaryColor;
+      margin-bottom: 14px;
+
+      @include mediaMax($mobile) {
+        font-size: 14px;
+        margin-bottom: 8px;
+      }
+    }
+  }
+
+
+  // -------------------------------------------------------------------
+  // compare-frame: rounded card wrapper around tables, code blocks, and
+  // figures in the media column. Replaces .slide-show__frame. On mobile
+  // the frame switches from clipping to horizontal scroll so wide data
+  // stays readable.
+  // -------------------------------------------------------------------
+  .compare-frame {
+    display: block;
+    padding: 12px 0;
+    background-color: $backgroundPrimaryColor;
+    border: 1px solid $compare-border;
+    border-radius: $compare-radius;
+    overflow: hidden;
+
+    @include mediaMax($tablet) {
+      overflow-x: auto;
+      overflow-y: hidden;
+      -webkit-overflow-scrolling: touch;
+    }
+  }
+
+
+  // -------------------------------------------------------------------
+  // compare-impact-cards: 2-up grid of affected-workload cards in the
+  // "Impact" section. Replaces .cases-slide__companies / __company-card.
+  // -------------------------------------------------------------------
+  .compare-impact-cards {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+    gap: 12px;
+    width: 100%;
+    padding: 32px 0;
+
+    @include mediaMax($mobile) {
+      grid-template-columns: 1fr;
+    }
+  }
+
+  .compare-impact-card {
+    display: block;
+    padding: 14px 16px;
+    border: 1px solid $compare-border;
+    border-radius: 6px;
+    text-decoration: none;
+    text-align: left;
+    color: inherit;
+    background: none;
+    transition: border-color 200ms, box-shadow 200ms, transform 200ms;
+
+    &:hover {
+      border-color: $accentPrimaryColor;
+      transform: translateY(-1px);
+      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+    }
+
+    &__title {
+      display: block;
+      font-size: 16px;
+      font-weight: 700;
+      line-height: 1.3;
+      margin-bottom: 2px;
+    }
+
+    &__desc {
+      display: block;
+      font-size: 13px;
+      line-height: 1.4;
+      color: $fontSecondaryColor;
+    }
+  }
+
+
+  // -------------------------------------------------------------------
+  // Compare-page prose paragraph. Replaces .about-slide__text on this
+  // page because the comparison page is not an "about slide" — the
+  // shared class brought 84px bottom margin that was too airy here.
+  // Same 20px / 1.6 typography for visual continuity; ~half the gap.
+  // Inline  inside gets the same calm pill treatment used in
+  // .about-slide__text on other pages.
   // -------------------------------------------------------------------
-  .about-slide__content {
-    h3.about-slide__subtitle + p + h3.about-slide__subtitle,
-    h3.about-slide__subtitle ~ h3.about-slide__subtitle {
-      margin-top: 32px;
+  .compare-prose {
+    font-size: 20px;
+    line-height: 1.6;
+    margin: 0 0 40px;
+
+    @include mediaMax($mobile) {
+      margin-bottom: 28px;
     }
+
+    code {
+      font-size: 0.85em;
+      font-weight: 500;
+      background: rgba(0, 0, 0, 0.05);
+      padding: 1px 6px;
+      border-radius: 4px;
+      vertical-align: 1px;
+      white-space: nowrap;
+
+      @include mediaMax($mobile) {
+        white-space: normal;
+        overflow-wrap: anywhere;
+      }
+    }
+  }
+
+
+  // -------------------------------------------------------------------
+  // Callout — soft-tinted methodology note. Used inline within prose
+  // to flag a caveat or framing for the data that follows. BEM block
+  // (not inline style!) so the visual treatment is consistent and
+  // tweakable in one place.
+  // -------------------------------------------------------------------
+  .compare-callout {
+    padding: 14px 18px;
+    margin: 0 0 32px;
+    background: $compare-bg-soft;
+    border-left: 3px solid $compare-border;
+    border-radius: 0 4px 4px 0;
+    font-size: 15px;
+    line-height: 1.55;
+    color: $compare-text-meta;
+
+    strong { color: $compare-text-strong; font-weight: 600; }
+    code { background: rgba(0, 0, 0, 0.06); padding: 1px 5px; border-radius: 3px; font-size: 0.9em; }
   }
 
 
@@ -248,6 +494,79 @@ $compare-best:           #16a34a;  // green winner accent
     box-sizing: border-box;
   }
 
+  // -------------------------------------------------------------------
+  // Tabbed code block — radio-input + label trick for no-JS tabs. One
+  // tab visible at a time; clicking a label toggles the matching pre.
+  // Used in the "What do we compare?" section to show all five setups
+  // without dominating page height.
+  // -------------------------------------------------------------------
+  .compare-code-tabs {
+    margin: 24px 0 40px;
+    border-radius: $compare-radius;
+    background: $compare-bg-code;
+    overflow: hidden;
+
+    > input[type='radio'] {
+      position: absolute;
+      opacity: 0;
+      pointer-events: none;
+    }
+
+    &__bar {
+      display: flex;
+      gap: 0;
+      padding: 0 8px;
+      background: rgba(255, 255, 255, 0.04);
+      border-bottom: 1px solid rgba(255, 255, 255, 0.08);
+      overflow-x: auto;
+      -webkit-overflow-scrolling: touch;
+    }
+
+    &__tab {
+      flex: 0 0 auto;
+      padding: 12px 16px;
+      cursor: pointer;
+      color: #9a9a9a;
+      font-size: 13px;
+      font-weight: 500;
+      border-bottom: 2px solid transparent;
+      transition: color 120ms ease, border-color 120ms ease;
+      user-select: none;
+      white-space: nowrap;
+
+      &:hover { color: #d0d0d0; }
+    }
+
+    &__panel {
+      display: none;
+
+      pre {
+        margin: 0;
+        padding: 28px 32px;
+        background: transparent;
+        color: #e0e0e0;
+        font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
+        font-size: 13px;
+        line-height: 1.7;
+        white-space: pre;
+        overflow-x: auto;
+      }
+    }
+  }
+
+  // Tab/panel toggling — kept flat (outside .compare-code-tabs) so the
+  // `~` sibling combinator doesn't get a parent selector injected
+  // between both sides of the combinator, which would break the match.
+  @for $i from 1 through 5 {
+    .compare-code-tabs__radio--#{$i}:checked ~ .compare-code-tabs__bar .compare-code-tabs__tab--#{$i} {
+      color: #fff;
+      border-bottom-color: $accentPrimaryColor;
+    }
+    .compare-code-tabs__radio--#{$i}:checked ~ .compare-code-tabs__panel--#{$i} {
+      display: block;
+    }
+  }
+
 
   // -------------------------------------------------------------------
   // Hero block — the comparison eyebrow + H1 + subhead at the top of
@@ -313,27 +632,6 @@ $compare-best:           #16a34a;  // green winner accent
   }
 
 
-  // -------------------------------------------------------------------
-  // Heading anchor (Stripe-style hover-revealed # icon)
-  // -------------------------------------------------------------------
-  .about-slide__title,
-  .about-slide__subtitle {
-    scroll-margin-top: $compare-header-offset;
-
-    .heading-anchor {
-      margin-left: 8px;
-      color: $compare-text-faint;
-      font-weight: 400;
-      text-decoration: none;
-      opacity: 0;
-      transition: opacity 0.15s;
-    }
-
-    &:hover .heading-anchor       { opacity: 1; }
-    .heading-anchor:hover         { color: $accentPrimaryColor; }
-  }
-
-
   // -------------------------------------------------------------------
   // Hero stat cards — three at-a-glance comparison cards. Each card is
   // a small label → 3 rows → footnote table. One "worst" cell per card.
@@ -520,82 +818,48 @@ $compare-best:           #16a34a;  // green winner accent
 
 
   // -------------------------------------------------------------------
-  // Right-column card (slide-show__frame override + variants).
-  // Default: bare rounded card around tables / pre blocks. Cells own
-  // their horizontal padding (14/18px) — the frame only adds vertical
-  // inset so the first/last row don't sit flush against the border.
-  // .compare-quote-card: tinted-bg variant for customer quotes.
+  // .compare-quote-card: tinted-bg variant of .compare-frame used for
+  // customer quotes (Doximity, etc.). Overrides .compare-frame's bare
+  // padding/bg with a panel-shaped soft tint.
   // -------------------------------------------------------------------
-  .about-slide__media .slide-show__frame {
-    display: block; // override the global flex centering
-    padding: 12px 0;
-    background-color: $backgroundPrimaryColor;
-    border: 1px solid $compare-border;
-    border-radius: $compare-radius;
-    overflow: hidden;
-
-    &.compare-quote-card {
-      padding: 40px 32px;
-      background-color: $compare-bg-soft;
-
-      .compare-quote-card__eyebrow {
-        margin-bottom: 20px;
-        font-size: 12px;
-        font-weight: 600;
-        text-transform: uppercase;
-        letter-spacing: 0.06em;
-        color: $compare-text-mute;
-      }
-
-      .compare-quote-card__text {
-        margin: 0 0 20px;
-        padding: 0;
-        font-size: 17px;
-        font-style: normal;
-        line-height: 1.6;
-        color: $compare-text-strong;
-        quotes: none;
-      }
+  .compare-quote-card {
+    padding: 40px 32px;
+    background-color: $compare-bg-soft;
 
-      .compare-quote-card__byline {
-        margin-bottom: 24px;
-        font-size: 14px;
-        line-height: 1.5;
-        color: $compare-text-meta;
-
-        strong { color: $compare-text-strong; }
-      }
-
-      .compare-quote-card__footer {
-        padding-top: 20px;
-        border-top: 1px solid $compare-border;
-        font-size: 13px;
-        line-height: 1.65;
-        color: $compare-text-quiet;
-      }
+    &__eyebrow {
+      margin-bottom: 20px;
+      font-size: 12px;
+      font-weight: 600;
+      text-transform: uppercase;
+      letter-spacing: 0.06em;
+      color: $compare-text-mute;
     }
 
-    // On mobile, tables inside this card can exceed the viewport width.
-    // Switch from clip-and-crop to horizontal scroll so the data stays
-    // readable. The rounded card still visually contains the data — the
-    // scroll happens inside the border, not under the rest of the page.
-    @include mediaMax($tablet) {
-      overflow-x: auto;
-      overflow-y: hidden;
-      -webkit-overflow-scrolling: touch;
+    &__text {
+      margin: 0 0 20px;
+      padding: 0;
+      font-size: 17px;
+      font-style: normal;
+      line-height: 1.6;
+      color: $compare-text-strong;
+      quotes: none;
     }
-  }
 
-  .about-slide__media pre {
-    background-color: $compare-bg-code;
-    border-radius: $compare-radius;
+    &__byline {
+      margin-bottom: 24px;
+      font-size: 14px;
+      line-height: 1.5;
+      color: $compare-text-meta;
 
-    // 
 defaults to white-space: pre, which keeps lines from
-    // wrapping. On mobile that means long lines push past the viewport.
-    // overflow-x: auto lets the user scroll the code block instead.
-    @include mediaMax($tablet) {
-      overflow-x: auto;
-      -webkit-overflow-scrolling: touch;
+      strong { color: $compare-text-strong; }
+    }
+
+    &__footer {
+      padding-top: 20px;
+      border-top: 1px solid $compare-border;
+      font-size: 13px;
+      line-height: 1.65;
+      color: $compare-text-quiet;
     }
   }
 
@@ -660,7 +924,7 @@ $compare-best:           #16a34a;  // green winner accent
     gap: 16px;
     max-width: 1100px;
     margin: 0 auto;
-    // The grid lives inside a flex column (about-slide__content-full).
+    // The grid lives inside a flex column (.compare-rubric__content--full).
     // Flex items default to min-width: auto = min-content, so grid items
     // with long unbreakable tokens can push the grid past its parent's
     // width. min-width: 0 lets the grid shrink, and width: 100% pins it
@@ -675,14 +939,8 @@ $compare-best:           #16a34a;  // green winner accent
   // capacity, and feature-comparison tables. Right-aligned by default
   // (numeric); first column left-aligned (label). is-worst / is-na /
   // is-key modifiers handle per-cell semantics.
-  //
-  // Two selector forms because the page is mid-migration:
-  //   1. .compare-data-table — preferred, decoupled from where it lives
-  //   2. .slide-show__frame table — legacy, applies to existing tables
-  //      that haven't been class-tagged yet. Remove once HTML is swept.
   // -------------------------------------------------------------------
-  .compare-data-table,
-  .slide-show__frame table {
+  .compare-data-table {
     width: 100%;
     background-color: transparent;
     border-collapse: collapse;
@@ -703,9 +961,14 @@ $compare-best:           #16a34a;  // green winner accent
       letter-spacing: 0.06em;
       color: $compare-text-quiet;
       text-align: right;
+      // Numeric headers ("10K/s", "p99") should stay on one line; the
+      // first-column label header can be a long phrase, so allow wrap.
       white-space: nowrap;
 
-      &:first-child { text-align: left; }
+      &:first-child {
+        text-align: left;
+        white-space: normal;
+      }
     }
 
     tbody tr {
@@ -722,7 +985,11 @@ $compare-best:           #16a34a;  // green winner accent
 
       &:first-child {
         text-align: left;
-        white-space: nowrap;
+        // Allow long labels ("Graceful shutdown controls", "Replay
+        // buffer storage", "Other protocols supported") to wrap into
+        // the column instead of overflowing the cell horizontally.
+        white-space: normal;
+        overflow-wrap: anywhere;
         color: $compare-text-meta;
       }
     }
@@ -958,4 +1225,225 @@ $compare-best:           #16a34a;  // green winner accent
       color: #444;
     }
   }
+
+
+  // -------------------------------------------------------------------
+  // Hub page (/compare) — landscape map + comparison cards.
+  //
+  // Two grids:
+  //   1. .compare-hub-buckets — the four-bucket map (libraries,
+  //      self-hosted brokers, 1st-gen managed, 2nd-gen managed). The
+  //      AnyCable bucket gets the --self modifier (accent border + a
+  //      small "You are here" pill) so the reader can self-locate.
+  //   2. .compare-hub-cards — one card per opponent comparison. Live
+  //      cards (--live) are real anchor links; coming-soon cards
+  //      (--soon) are non-interactive divs with a dimmed treatment.
+  //
+  // Both grids use auto-fit minmax so they reflow from 1 → 2 → 4
+  // columns without a single fixed breakpoint.
+  // -------------------------------------------------------------------
+  .compare-hub-buckets {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
+    gap: 16px;
+    margin: 32px 0 0;
+  }
+
+  .compare-hub-bucket {
+    padding: 24px 24px 20px;
+    background: $backgroundPrimaryColor;
+    border: 1px solid $compare-border;
+    border-radius: $compare-radius;
+
+    // The AnyCable bucket — accent border + "You are here" pill so the
+    // reader can locate AnyCable's category within the landscape map.
+    &--self {
+      border-color: $accentPrimaryColor;
+      box-shadow: 0 0 0 3px rgba($accentPrimaryColor, 0.08);
+    }
+
+    &__label {
+      margin: 0 0 6px;
+      font-size: 12px;
+      font-weight: 700;
+      text-transform: uppercase;
+      letter-spacing: 0.06em;
+      color: $compare-text-strong;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      flex-wrap: wrap;
+    }
+
+    &__here {
+      display: inline-block;
+      padding: 2px 8px;
+      font-size: 10px;
+      font-weight: 600;
+      letter-spacing: 0.06em;
+      color: #fff;
+      background: $accentPrimaryColor;
+      border-radius: 99px;
+    }
+
+    &__lede {
+      margin: 0 0 12px;
+      font-size: 15px;
+      line-height: 1.5;
+      color: $compare-text-body;
+    }
+
+    &__examples {
+      margin: 0 0 12px;
+      padding-bottom: 12px;
+      border-bottom: 1px solid $compare-border-faint;
+      font-size: 14px;
+      color: $compare-text-meta;
+
+      strong {
+        color: $compare-text-strong;
+      }
+    }
+
+    &__note {
+      margin: 0;
+      font-size: 13px;
+      line-height: 1.55;
+      color: $compare-text-mute;
+    }
+  }
+
+
+  // ---------- Comparison cards ----------------------------------------
+  .compare-hub-cards {
+    display: grid;
+    grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
+    gap: 16px;
+    margin: 32px 0 24px;
+  }
+
+  .compare-hub-card {
+    display: flex;
+    flex-direction: column;
+    padding: 28px 28px 24px;
+    background: $backgroundPrimaryColor;
+    border: 1px solid $compare-border;
+    border-radius: $compare-radius;
+    text-decoration: none;
+    color: inherit;
+    transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
+
+    // Live cards are  elements — hover lifts them slightly and pulls
+    // the border to accent so the reader knows they're clickable.
+    &--live {
+      cursor: pointer;
+
+      &:hover {
+        border-color: $accentPrimaryColor;
+        transform: translateY(-2px);
+        box-shadow: 0 6px 24px rgba(0, 0, 0, 0.06);
+      }
+    }
+
+    // Coming-soon cards are 
elements — dimmed, no hover, no + // pointer. The reader gets the message without a dead link. + &--soon { + opacity: 0.78; + cursor: default; + + .compare-hub-card__title, + .compare-hub-card__lede, + .compare-hub-card__axes { + color: $compare-text-meta; + } + } + + &__head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding-bottom: 14px; + border-bottom: 1px solid $compare-border-faint; + } + + &__bucket { + margin: 0; + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.06em; + color: $compare-text-mute; + } + + &__status { + margin: 0; + padding: 3px 10px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: $compare-best; + background: rgba($compare-best, 0.1); + border-radius: 99px; + white-space: nowrap; + } + + &--soon &__status { + color: $compare-text-mute; + background: $compare-bg-soft; + } + + &__title { + margin: 0 0 10px; + font-size: 26px; + font-weight: 700; + line-height: 1.2; + letter-spacing: -0.01em; + color: $compare-text-strong; + + .is-mute { + color: $compare-text-fade; + font-weight: 400; + } + } + + &__lede { + margin: 0 0 16px; + font-size: 15px; + line-height: 1.55; + color: $compare-text-body; + } + + &__axes { + margin: 0 0 20px; + padding-left: 18px; + font-size: 14px; + line-height: 1.55; + color: $compare-text-meta; + + li { + margin-bottom: 4px; + } + } + + &__cta { + margin: auto 0 0; + font-size: 14px; + font-weight: 600; + color: $accentPrimaryColor; + + &.is-soon { + color: $compare-text-mute; + } + } + } + + + .compare-hub-footnote { + margin: 8px 0 0; + font-size: 14px; + color: $compare-text-meta; + } } From 0a74eea5c8ffa40763579d2c6ee623ce1db88d81 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Sat, 30 May 2026 16:51:40 +0200 Subject: [PATCH 34/57] Compare/Socket.io: full restructure + v1.6.14 numbers + SCSS refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Page structure (5 questions → 3 sections): - New Section 1: How fast (latency suite + whispers placeholders) - Section 2: How reliable (jitter table updated, CSR row reflects heavy-jitter pattern with 72% delivery / 116s p99 max, footnote rewritten) - Section 3: How efficient to scale (idle, throughput, deploy-impact) - Added 'Note on architecture' block with embedded-vs-microservice diagram and the 10K embedded deploy-impact callout (~2s freeze per user) - Removed Q4 (operations) and Q5 (optionality) sections, folded strongest beats into FAQ - Removed standalone uWS sidebar, uWS is now a column in all tables - Added topology matrix (5 options × 3 topologies) in 'What we compare' Hero + TL;DR: - Title: Socket.io vs uWS vs AnyCable - Hero cards mapped to 3 sections, numbers shown where v1.6.14 data exists (AnyCable=100% delivery, Socket.io+CSR=72%, uWS still pending) - TL;DR rewritten around the 3 questions + whispers/Liveblocks-category beat + clustered deploy as the load-bearing finding v1.6.14 numbers plugged in: - AnyCable OSS: jitter 100%/0 loss/4.1s p95/6.2s p99/760 MB; throughput 9,996/89,334/151,953 deliv/sec; idle 821,877 conns on 28.30 GB (~34 KB/conn); latency 1k 17/55ms 10k 252/895ms - AnyCable Pro: same jitter profile, 271 MB; throughput 9,997/87,696/140,032 (avg of 2 runs); idle 822,037 conns on 14.80 GB (~18 KB/conn); latency 1k 22/145ms 10k 246/875ms - Socket.io+CSR jitter: 72.3% / 116s p99 (new pattern, footnote explains 16% resume rate under heavy jitter) - Other Socket.io / uWS rows retained at v1.6.13 values pending re-baseline - TechArticle + FAQPage JSON-LD updated to match CSR feature now linked to its docs on first prose mention; full name 'Connection state recovery' used per Socket.io docs (lowercase 's' and 'r'). Cleanup: - Moved compare hub draft to tmp/ (not needed until 2nd comparison) - .gitignore: tmp/, .zed/, /*-report.html, /*-benchmark.html SCSS refactor (parallel work): - compare-spine.scss: new layout primitive - compare.scss + index.scss updates for the page's new structure - Popup + demo TS/JS minor updates Outstanding (deferred to follow-up sessions): - Whispers test (A2b): server-side handler + per-option clients - 3-node clustered tests (A2d): needs Railway dashboard for socketio-server-redis-c - 100K latency tier (A2a): multi-shard test infrastructure - Socket.io / +CSR / uWS latency at 1k/10k tiers - Standalone deploy-impact for AnyCable (separate protocol runner) - Standalone deploy-impact rerun with default Socket.io ping settings --- .gitignore | 13 + src/compare/socket-io/index.html | 712 ++++++----- src/compare/socket-io/style/index.html | 412 ++++++ src/index.scss | 1 + src/js/components/Popup.ts | 5 + src/js/components/demo.js | 16 +- src/modules/blocks/compare-spine.scss | 1629 ++++++++++++++++++++++++ src/modules/blocks/compare.scss | 250 ++-- 8 files changed, 2606 insertions(+), 432 deletions(-) create mode 100644 src/compare/socket-io/style/index.html create mode 100644 src/modules/blocks/compare-spine.scss diff --git a/.gitignore b/.gitignore index 6698176..106a8c4 100644 --- a/.gitignore +++ b/.gitignore @@ -44,10 +44,23 @@ TODO-*.md llm-*.md *-audit.md +# Scratch directory for ephemeral notes, drafts, research dumps. +# Anything inside is ignored. Use this instead of leaving stray files +# at the repo root. +tmp/ + +# Editor settings +.zed/ + # Test screenshots / page captures at repo root (real images live under src/) /*.png /*.jpeg /*.jpg +# Stray benchmark / report HTML dumps at repo root +/*-report.html +/*-benchmark.html + # package-lock.json is generated locally; project bundles via Vite package-lock.json +.gstack/ diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index f829432..1e82727 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -1,10 +1,10 @@ - {{> dochead pageTitle="AnyCable vs Socket.io vs uWebSockets.js | 100% delivery, deploy-resilient WebSockets, 1M connections per node" pageDescription="A measured comparison for Node.js teams hitting Socket.io's limits. Same Railway hardware, five rubrics. Default Socket.io and uWS lose ~13–14% of messages under simulated WiFi jitter; AnyCable and Socket.io+CSR deliver 100%. In-process WS (Socket.io, +CSR, uWS) dies on every deploy and past ~20K connections doesn't recover; AnyCable runs WS as a separate process — 0 s reconnect storm. On one 32 vCPU/32 GB box: AnyCable Pro holds 1M idle connections, uWS holds 1M, Socket.io caps at ~120K. WS-layer throughput in-process: uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. All numbers reproducible." pageUrl="https://anycable.io/compare/socket-io"}} + {{> dochead pageTitle="AnyCable vs Socket.io vs uWebSockets.js | Benchmarking WebSocket infrastructure for JS/TS apps" pageDescription="A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions: how fast (roundtrip latency at 1k/10k/100k and broadcast throughput, including whispers), how reliable (message delivery under WiFi-drop jitter), and how efficient to scale (single-node load test plus 3-node clustered deploy-impact and avalanche tests). Tested in both embedded and standalone topologies. All numbers reproducible from the open-source benchmark repo." pageUrl="https://anycable.io/compare/socket-io"}}
{{> header}} -
+
{{!-- Hero — three at-a-glance cards, one per load-bearing rubric finding. Each compares the headline metric across the four @@ -13,132 +13,120 @@
-

Comparison

- AnyCable vs Socket.io + Socket.io vs uWS vs AnyCable

- A measured comparison of AnyCable, uWebSockets.js, and Socket.io with Connection State Recovery — for Node.js teams who've outgrown default Socket.io. Same Railway hardware, five rubrics, reproducible numbers. + Benchmarking realtime WebSocket infrastructure options for JS/TS serverfull apps.

-
Reliability under jitter
+
Latency: p50 roundtrip @ 10k connections
-
Default Socket.io
-
87%
+
Socket.io + CSR
+
TBD-A2a
uWebSockets.js
-
86%
+
TBD-A2a
-
AnyCable, +CSR
-
100%
+
AnyCable
+
TBD-A2a
- 10K clients, WiFi-drop pattern, 1.2M deliveries. AnyCable + Socket.io+CSR carry replay state; Socket.io default and uWS don't — messages during disconnect are gone. Rubric 1 → + Latency test →
-
Deploy reconnect storm
+
Reliability: % delivered under WiFi jitter
-
Socket.io 25K conns
-
never
+
Socket.io + CSR
+
72%
-
uWS 25K conns
-
never
+
uWebSockets.js
+
TBD-A1
-
AnyCable any scale
-
0 s
+
AnyCable
+
100%
- In-process WS dies on every Node deploy. Past ~20K connections the reconnect storm doesn't recover. AnyCable's WS layer runs as a separate process — deploys don't touch it. Rubric 2 → + Reliability test →
-
Idle headroom one 32 vCPU box
+
Scalability: downtime during deployment
-
Default Socket.io
-
120K
+
Socket.io embedded
+
TBD-A2c
-
uWebSockets.js
-
1.0M
+
uWS embedded
+
TBD-A2c
-
AnyCable Pro
-
1.0M
+
AnyCable standalone
+
TBD-A2c
- Single Node event loop saturates handshakes at ~120K regardless of memory. AnyCable Pro at 19 GB / 19 KB per conn — the lightest setup with built-in replay. Rubric 3 → + Scalability test →
-

TL;DR#

- If your users are mobile or your team deploys daily, default Socket.io drops messages and disconnects every user on every deploy. Socket.io+CSR fixes the message loss in-place (replay buffer, opt-in), but doesn't fix the deploys. uWebSockets.js trades the Socket.io framework for a faster wire (1M idle on one box, lowest memory of anything we tested), but inherits the same in-process deploy problem and ships nothing for replay, observability, or multi-language clients. AnyCable solves both — replay on by default, WS layer runs as its own service so deploys don't touch it — at the cost of one extra process to operate. + Your TS/JS application doesn't want to sit still: it needs WebSockets for all sorts of real-time messaging, streaming from LLMs, and collaborative features. What to choose? We ran tests to benchmark Socket.io, uWebSockets, and AnyCable based on 3 key factors: latency, reliability, and scalability. We strive to bring production-grade quality to our real-time features - if we want to dance, let's dance smoothly.

- On raw WS-layer throughput in-process: uWS leads at 907K msgs/sec, AnyCable does 522K, Socket.io+CSR 496K, default Socket.io 437K. Once the publisher is operationally separate from the WS layer — the shape both architectures end up in past one node — both cap around 50–75K msg/sec at the 1M target, publisher-bound, not WS-layer bound. The Socket.io co-located publisher case (in-process on one of the 2 Redis-cluster nodes) does 773K but it requires the app code to live inside one of the WS processes — the assumption AnyCable rejects. Throughput isn't our headline win — capacity, reliability, and deploys are. + We ran all benchmarks on Railway, and the code is in this repo.

- - {{!-- Architecture explainer — sets up the mental model BEFORE the - data sections so readers understand where each setup's wins - and losses come from. Two axes drive everything: where the - WebSocket layer runs (in your Node process or alongside it) - and whether the protocol carries replay state. --}}
-
-

What do we compare?#

-

- You have a Node.js (or TypeScript / Deno / Bun) backend and you need realtime — chat, presence, live cursors, AI streaming, collaboration, dashboards. You're either standing one up for the first time, or you have Socket.io in production and looking for a replacement. This page is a comparison for that decision. -

+
+

What we compare#

- We benchmark five production-shaped options on the same hardware: + Five production-shaped options, same hardware:

    -
  • Default Socket.io — the baseline most teams have today, in-process Node library.
  • -
  • Socket.io + Connection State Recovery (CSR) — the in-place delivery upgrade Socket.io shipped in 4.6, often missed because it's opt-in and has adapter constraints.
  • -
  • uWebSockets.js + topics — the most-cited "just use uWS, it's 10× faster than Socket.io" alternative, using the library's built-in subscribe/publish API (the canonical pattern).
  • -
  • AnyCable OSS — a separate Go binary (anycable-go) your app broadcasts to over HTTP, with a broker (replay, history, presence) built in.
  • -
  • AnyCable Pro — same protocol and broadcast code as OSS, different server binary (anycable-go-pro): commercial license, more efficient broker, denser per-connection memory, horizontal scale with shared replay state.
  • +
  • Default Socket.io — baseline popular option.
  • +
  • Socket.io + CSR — Socket.io with Connection state recovery, the opt-in for delivery and order guarantees.
  • +
  • uWebSockets.js + topics — the “just use uWS” alternative, using its built-in subscribe/publish API.
  • +
  • AnyCable OSS — a separate Go binary your app broadcasts to over HTTP, broker built in.
  • +
  • AnyCable Pro — same protocol as OSS; denser per-connection memory, shared replay state, commercial license.

- Server-side broadcast code, side by side — what your Node app actually looks like for each option. Click a tab. Production-typical patterns; the bench harness mirrors them closely. + Server-side broadcast code, side by side. Production-typical; the bench harness mirrors these closely.

- +
+
+
+
+
+
+ + {{!-- Note on architecture — sits between "What we test" and Section 1. + Left: user-written prose explaining why we test in microservice mode. + Right: embedded-vs-microservice architecture diagram + the 10k + embedded test numbers that motivate the architectural choice. --}} +
+
+
+
+

Note on architecture#

- Same workloads against all five, scored against five rubrics — the questions we actually hear from CTOs choosing realtime infrastructure for a growing Node app: + When building production-grade realtime infrastructure, we have to rule out the embedded (in-process with your Node app) mode for Socket.io/uWS in favor of deploying them as a standalone microservice. The embedded mode produces unacceptable disruption on every deployment of the main Node application: every redeployment of each node causes at least a 2-second freeze and loss of messages while clients are reconnecting to another node. +

+
+

Test: we have a 2-node cluster - identical Node.js app with embedded Socket.io. and Redis. We do a proper rolling deploy: redeploy Node 1, wait until it's fully back, then redeploy Node 2.

+ +

Result: every user experiences a ~2-second freeze when their node restarts.

+ +
+
+
+ {{!-- Architecture diagram: embedded mode (app + WS in one + Node process) vs microservice mode (separate WS service). + Adapted from the older Socket.io-vs-AnyCable diagram but + reframed around topology rather than vendor. --}} +
+ {{!-- Two stacked panels. Top = embedded mode (what we tested); + bottom = microservice mode (target setup). Typography is the + page's: Stem for box titles + descriptions, Martian Mono for + small labels + annotations. Color: accent red for the fragile + (embedded) path, dark for the stable (microservice) path. --}} + + Embedded mode (tested) vs microservice mode (target) + + {{!-- Font sizes (SVG units): 15 (box title) · 14 (description) + · 13 (mono label / annotation) · 12 (mono helper). All ≥12 + in SVG units; renders ~12–15px on screen. --}} + EMBEDDED · TESTED + + + + + + NODE 1 + App + Socket.io + ~5,000 WS connections + + + + NODE 2 + App + Socket.io + ~5,000 WS connections + + + + Redis + PUB/SUB + + + + + + + DEPLOY → RESTART NODE + every WS drops + + {{!-- BOTTOM — Microservice mode: app + WS side-by-side --}} + MICROSERVICE · TARGET + + + + NODE 1 · APP + App only + Auth, routes, DB + + + + + HTTP + + + + NODE 2 · SOCKET.IO + WS only + 10,000 WS connections + + + DEPLOY → NODE 1 RESTARTS + all WS survive + +
Embedded mode (tested) vs microservice mode (target). Embedded couples the WS server to every app deploy.
+
+ + +
+
+
+
+ + {{!-- TBD-B1: Section 1 (NEW) — How fast is it? Latency at 1k/10k/100k, + broadcast throughput, whispers. All numbers are placeholders awaiting + A2a (latency suite), A2b (whispers test), A2e (standalone hop overhead). --}} +
+
+
+
+

How fast is it?#

+

+ Latency is the floor of what realtime can feel like. We measure two things: how long a message takes to round-trip from one client to others, and how many messages per second the server can fan out before that latency starts inflating. Both are tested at three connection scales (1k, 10k, 100k) so the curve is visible, not just one point.

-
    -
  1. Will my product feel smooth and reliable even on unstable networks?
  2. -
  3. Can my team ship code without disrupting the product?
  4. -
  5. How much can we scale before we redo the architecture?
  6. -
  7. When this breaks at 3am, can my team diagnose and fix it?
  8. -
  9. If we're wrong about this, what does it cost to leave?
  10. -

- Each rubric below states the question, the answer in a paragraph, and the data underneath. Benchmark code & raw output are open source: github.com/irinanazarova/anycable-socketio-benchmarks. + We test each option in two topologies because they tell different stories. Embedded: WS server runs in the same Node process as your app code; broadcasts originate in-process with no network hop. Standalone: WS server is a separate process; the app publishes broadcasts over the network (HTTP for AnyCable, Redis for Socket.io, custom HTTP for uWS). Standalone always pays a hop tax; the question is how big the tax is. AnyCable is always standalone by design.

+
+
+ + + + + + + + + + + + + + + + +
Setup p50 roundtrip, 10K subsEmbeddedStandaloneHop tax
Socket.io defaultTBD-A2aTBD-A2aTBD-A2e
Socket.io + CSRTBD-A2aTBD-A2aTBD-A2e
uWS topicsTBD-A2aTBD-A2aTBD-A2e
AnyCable OSSn/a (always standalone)TBD-A2an/a
AnyCable Pron/a (always standalone)TBD-A2an/a
+

Placeholders pending A2a (latency at scale) and A2e (embedded-vs-standalone hop overhead). uWS has no built-in HTTP publish API; standalone uWS requires a custom publish endpoint, which is part of what we measure.

+
+
+
+ + {{!-- Scale-curve sub-section: roundtrip at 1k/10k/100k --}} +
+
+

How latency scales with concurrent connections

+

+ Same broadcast workload at three connection counts. The shape of the curve matters more than any single point: a flat line means the server has headroom; a steep climb means you're approaching the wall. Standalone mode is shown here (the production-realistic topology); the embedded vs standalone hop tax is in the table above. +

+
+
+
+ + + + + + + + + + + + + + + + +
Setup p50 / p99, standalone1k10k100k
Socket.io + Redis adapterTBD-A2aTBD-A2aTBD-A2a
Socket.io + CSRTBD-A2aTBD-A2aTBD-A2a
uWS custom publishTBD-A2aTBD-A2aTBD-A2a
AnyCable OSS17 / 55 ms252 / 895 msTBD-A2a
AnyCable Pro22 / 145 ms246 / 875 msTBD-A2a
+

Placeholders pending A2a. 100k tier feasibility depends on the bench-runner shard fanout; if 100k is unreliable we'll surface 50k as the upper tier and document why.

+
+
+
+ + {{!-- Whispers sub-section: client-to-client without server roundtrip. + This is where AnyCable plays in the Liveblocks/Yjs/PartyKit category, + not just the WS-server category. --}} +
+
+

Whispers: client-to-client updates that bypass the backend

+

+ Some realtime updates don't need the backend at all. Mouse cursors in a shared doc, typing indicators, presence pings: these go from one client to others through the WS server without ever touching your app. AnyCable supports this natively as a first-class primitive; Socket.io approximates it with rooms; uWS does it with topics but you build the access control yourself. +

+
+ Why whispers matter for category positioning. Whispers are how AnyCable competes in the collab-infrastructure category alongside Liveblocks, Yjs providers, and PartyKit, not just in the WS-server category. If your product has live cursors, shared selections, or any UI where one user's local state needs to fan out to peers without a server round-trip, this row matters more than raw throughput. Liveblocks built its entire business on this primitive being good enough to feel real-time. +
+
+
+
+ + + + + + + + + + + + + + + + +
Setup 10k clients, 100 roomsNative?p50p99CPU/msg
Socket.io roomsEmulatedTBD-A2bTBD-A2bTBD-A2b
uWS topicsEmulatedTBD-A2bTBD-A2bTBD-A2b
AnyCable OSSNativeTBD-A2bTBD-A2bTBD-A2b
AnyCable ProNativeTBD-A2bTBD-A2bTBD-A2b
+

Placeholders pending A2b. Server CPU per delivered message is the load-bearing metric here: whispers should be effectively free for the backend. "Emulated" means the test implements whisper semantics on top of the library's broadcast primitive (rooms / topics) and we measure the same workload as the native path.

+
+
@@ -246,12 +429,12 @@

What do we compare?
-

1. Will my product feel smooth and reliable even on unstable networks?#

+

How reliable is delivery?#

Your users are not necessarily always connected to a high-speed fiber network. Micro disruptions are largely not noticeable for HTTP requests, but they are the deal breaker for real-time features — or how reliable and smooth they are going to be. Two things decide it: how often messages are lost during disruption, and how long the recovery window feels. Both come down to whether the protocol carries replay state.

- Default Socket.io and uWebSockets.js don't carry replay state — they lose ~13–14% of messages under simulated jitter (1-second TCP drops every ~15 s, the pattern WiFi handoffs produce). Socket.io with Connection State Recovery fixes delivery to 100%, with constraints on the adapter and a ~8 s p99 replay tail. AnyCable also delivers 100%, clears the tail ~1.7 s sooner, and AnyCable Pro holds the same 10K reconnecting fleet on less than half of CSR's server memory. The data: + Default Socket.io and uWebSockets.js don't carry replay state — they lose ~13–14% of messages under simulated jitter (1-second TCP drops every ~15 s, the pattern WiFi handoffs produce). Socket.io with Connection state recovery fixes delivery to 100%, with constraints on the adapter and a ~8 s p99 replay tail. AnyCable also delivers 100%, clears the tail ~1.7 s sooner, and AnyCable Pro holds the same 10K reconnecting fleet on less than half of CSR's server memory. The data:

Default Socket.io is at-most-once; CSR adds replay (opt-in, with adapter constraints). AnyCable ships replay by default via reliable streams — per-stream history, epoch + offset, restart-survivable with NATS or Redis. Mechanism & caveats: see appendix. @@ -309,10 +492,10 @@

1. Will my product feel smoot Socket.io + CSR - 100% - 0 - 4.0 s - 7.9 s + 72.3% + 0 + 81.6 s + 116 s 627 MB @@ -341,177 +524,23 @@

1. Will my product feel smoot -

Two clusters: no-replay protocols (Socket.io default, uWS) lose during disconnect windows; replay protocols (CSR, AnyCable) deliver 100% but pay a replay-tail latency cost. uWS's tiny memory is the no-replay-broker dodge; AnyCable Pro is the lightest replay-capable setup. uWS reconnects fast (~1 s) but messages during the disconnect are gone — nothing to replay. Same 32 vCPU / 32 GB Railway box; CPU negligible across all setups.

-

-
-

-
-
- - {{!-- What about uWebSockets.js? — addresses the "Socket.io is old, just use uWS" - pushback we hear from founders comparing AnyCable to Socket.io. The - "10× faster" claim is genuinely true on the wire (memory + latency + - idle capacity), so we acknowledge that head-on with measured numbers, - then show where the wire isn't the bottleneck (replay-less = lossy on - jitter, single-process = same deploy problem). --}} -
-
-
-
-

Sidebar: what about uWebSockets.js?#

-

- Not a rubric on its own — uWS sits inside Rubrics 1, 2, and 3 above. But it deserves direct treatment because it's the most common pushback we hear from founders evaluating AnyCable: "Socket.io is old — uWebSockets.js is 10× faster, just use that." The wire-speed claim is genuinely true. We measured it on the same Railway hardware as everything else on this page. -

-
- Note on the uWS column. The uWS setup we test is uWebSockets.js with the built-in subscribe/publish topics API — the canonical Broadcast.js pattern from the library's own examples. Production teams who pick uWS typically deploy exactly this shape (for multi-node, they'd add Redis pub/sub on top — orthogonal to single-node behavior). Numbers below are not the "raw socket" wire floor; they're what a production-shaped uWS app actually does. -
-

- uWS wins on the wire, decisively. 72 MB at 10K reconnecting clients, 5.45 GB at 1M idle, sub-second reconnect time (p99 ~993 ms) — lowest of anything we tested. The "10× faster than Socket.io" headline is reproducible. But — that's reconnect speed, not replay: uWS has no replay buffer, so the messages sent during the disconnect window are gone, no matter how fast the client reconnects. -

-

- But the wire is rarely the decision. uWS still loses ~14% of messages under jitter (no replay buffer), the deploy avalanche still hits every connection (in-process, just like Socket.io), and the reconnect storm cliff is just at higher N (~20K clean recovery, ~90K survivable, vs Socket.io's ~10K / ~25K). uWS is the right answer to "Socket.io's wire is too heavy." AnyCable is the right answer to "we lose messages during network blips" or "every deploy hits our users." Different layers of the stack. -

-

- Mechanism + per-N avalanche walkthrough: bench repo README. -

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
10,000 clients under jitteruWSSocket.ioAnyCable OSSAnyCable Pro
Server memory peak72 MB675 MB760 MB271 MB
Replay latency p99no replay (reconnect < 1 s)no replay6.2 s6.2 s
Delivery rate86.4%~87%100%100%
Messages lost163,371156,85600
-

Same 32 vCPU / 32 GB Railway box. uWS reconnect uses 2–5 s exponential backoff matched to socket.io-client's defaults. Comparing uWS vs default Socket.io on delivery: nearly identical — both at-most-once, no replay, lose during the same blind windows. Latency is identical between AnyCable OSS and Pro (same protocol, same broker); memory differs because Pro's broker is more compact.

-
-
-
- - {{!-- Avalanche scaling: uWS vs Socket.io on the same 0.5 GB / 1 vCPU - small box used for the Pillar 2 cliff test. Two thresholds: - clean-recovery (operational headroom) and survivable cliff - (catastrophic-failure ceiling). --}} -
-
-

Avalanche scaling: uWS just shifts the cliff to higher N

-

- Same 1 vCPU / 0.5 GB Railway box. uWS pushes the catastrophic cliff from ~25K (Socket.io) to ~90K, but the curve is non-linear: 20K → 25K already takes recovery from 4.6 s to 57 s. The bottleneck is server-resource (handshake serialization, kernel rate limiting), not library overhead. AnyCable's separate process means there's no avalanche to recover from at any scale. -

-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ClientsSocket.io recoveryuWS recoveryuWS p50AnyCable
5,0004.5 s, 100%0 s
10,0003.9 s, 100%0 s
15,0005.8 s, 98.5%0 s
20,0008.0 s, 96.2%4.6 s, 100%3.1 s0 s
25,000never (cliff)57 s, 100%6.6 s0 s
50,000cliff223 s, 98.6%151 s0 s
~90–100Kclifframp cliff0 s
-

Same 1 vCPU / 0.5 GB Railway box used for Socket.io's 25K cliff. uWS recovery = wall-clock time to 95% reconnected; p50 = median per-client blackout (typical user "Connecting…" window). At ~90–100K, uws-server-small can't even ramp the full target — it's the server-side cliff. AnyCable's WebSocket layer is a separate Go binary, so app deploys never restart it — no avalanche at any scale.

+

Two clusters: no-replay protocols (Socket.io default, uWS) lose during disconnect windows; replay protocols (CSR, AnyCable) close the gap, but CSR only does so within its retention window. Under our v1.6.14 jitter pattern (~105K jitter events at 10K clients), CSR resumes only ~16% of disconnect events; the rest are partial losses, with a multi-minute replay-tail max for messages that do get through. uWS reconnects fast (~1 s) but messages during the disconnect are gone, nothing to replay. AnyCable's reliable streams replay across the full disconnect window, so delivery stays at 100% with a sub-7s p99 tail. AnyCable Pro is the lightest replay-capable setup. Same 32 vCPU / 32 GB Railway box.

+ {{!-- TBD-B1: uWS sidebar removed in restructure. uWS is now a + first-class column in every comparison table. The avalanche + cliff-at-N data (Socket.io ~25K, uWS ~90K on a 1 vCPU / 0.5 GB + box) is being replaced by clustered 3-node tests in A2d. --}} {{!-- Use cases / impact --}}
-

Impact

-

Loss and slow replay break workflows where the next message depends on the previous one

+

What this breaks

Lost messages cluster around network events — exactly when the user is watching. CSR recovers them, but 8 seconds late reads as "the app froze"; AnyCable lands them around 6. For sequential workloads, loss and delay both break the flow.

@@ -540,12 +569,17 @@

Loss and slow replay break workflows where the

- {{!-- Standalone server — second pillar --}} + {{!-- TBD-B1: This section (currently the single-node deploy/avalanche + test) will be reframed as the deploy-impact subsection of new Q3 + (scale). Content stays; numbering changes; the new clustered + 3-node rolling-restart test (A2c) is added as a sibling subsection + below once it lands. Physical reorder (load test before avalanche) + deferred to content polish; B1 keeps content placement stable. --}}
-

2. Can my team ship code without disrupting the product?#

+

Does the WS layer survive deploys?#

Modern teams deploy multiple times a day. Realtime apps have a structural choice: the WebSocket layer lives inside the app process, or it runs as its own service. The choice you make at small scale becomes a load-bearing decision at growing scale. Below is what happens when the WS layer is co-located with the app and the app restarts on every deploy — default for Socket.io, Socket.io+CSR, and uWS, because all three are Node libraries. At small scale the reconnect storm is recoverable; past a certain N, it stops being recoverable.

@@ -558,51 +592,52 @@

2. Can my team ship code without carries the structural argument; the H3s + paragraphs below reinforce it in text. --}}
- + Socket.io vs AnyCable process architecture - - Socket.io — WebSocket layer co-located with the app + + SOCKET.IO · WS LAYER CO-LOCATED WITH APP - - Node.js process + + NODE.JS PROCESS - - Your app (HTTP, business logic) - routes, auth, DB calls — single event loop + + Your app + Routes, auth, DB — single event loop - - Socket.io WebSocket hub - tens of thousands of open connections + + Socket.io WS hub + Tens of thousands of open connections - deploy → restart Node process → every WebSocket drops + DEPLOY → RESTART NODE → EVERY WS DROPS - - AnyCable — WebSocket layer is a separate process + + ANYCABLE · WS LAYER IS A SEPARATE PROCESS - - Your app - Node.js, Python, Laravel, Go, - anything that speaks HTTP + + Your app + Node.js, Python, Laravel, Go — + anything that speaks HTTP - - - HTTP /_broadcast + + + HTTP /_BROADCAST - - anycable-go - single Go binary, holds all WebSocket - connections (1M idle on one node) + + anycable-go + Single Go binary — holds all the + WS connections (1M idle / node) - deploy app → anycable-go untouched, WebSockets survive + DEPLOY APP → ANYCABLE-GO UNTOUCHED, WS SURVIVE +
Two architectures, two deploy outcomes. Socket.io shares a process with the app; AnyCable runs the WS layer separately.

@@ -631,7 +666,7 @@

2. Can my team ship code without

- Through 20K connections, recovery is graceful for the in-process WS setups. At 25K, recovery never completes — the post-redeploy reconnect storm OOMs the new container. CSR doesn't help: its in-memory adapter loses state on restart, and connections still drop. On a bigger box the cliff exists too, just at higher N. The uWS sidebar above shows the same shape on the same hardware: uWS pushes the catastrophic cliff to ~90K instead of ~25K, but recovery at 25K already takes 57 seconds, and at 90K it stops being recoverable. More RAM moves where the wall is; the wall is still there, because it's the reconnect storm itself — not the box size — that overwhelms the new process. Any setup with the WS layer as a separate service avoids this entirely — the WS layer doesn't restart, so there's nothing to recover from. AnyCable ships that shape by default; anyone scaling Socket.io / uWS in production ends up building it themselves. Methodology & per-N walkthrough: bench repo README. Background reading: thundering-herd safety tips. + Through 20K connections, recovery is graceful for the in-process WS setups. At 25K, recovery never completes — the post-redeploy reconnect storm OOMs the new container. CSR doesn't help: its in-memory adapter loses state on restart, and connections still drop. On a bigger box the cliff exists too, just at higher N. On the same hardware uWS pushes the catastrophic cliff to ~90K instead of ~25K, but recovery at 25K already takes 57 seconds, and at 90K it stops being recoverable. More RAM moves where the wall is; the wall is still there, because it's the reconnect storm itself — not the box size — that overwhelms the new process. Any setup with the WS layer as a separate service avoids this entirely — the WS layer doesn't restart, so there's nothing to recover from. AnyCable ships that shape by default; anyone scaling Socket.io / uWS in production ends up building it themselves. Methodology & per-N walkthrough: bench repo README. Background reading: thundering-herd safety tips.

@@ -690,15 +725,15 @@

2. Can my team ship code without
-

3. How much can we scale before we redo the architecture?#

+

How efficient is it to scale?#

Your user base will grow, and your realtime layer needs to grow with it. The question isn't how cheap is this today — it's when we 10× users, does the bill or the box cliff before we have time to react? Cost predictability matters more than absolute cost: a $5 box that scales smoothly to 100K users beats a $499 plan that hits a wall at 50K. Three things decide it: how many idle connections one node holds (steady-state headroom), how the same node behaves during a reconnect storm (the avalanche from above), and useful throughput — messages per second × delivery rate, since raw msg/sec without delivery is vanity.

- Same Railway box (32 vCPU / 32 GB), four setups, 1,000,000-connection idle target. Socket.io accepted 119,826 — the single Node event loop saturates handshakes regardless of memory available. uWS held 1,018,366 on 5.45 GB (lightest wire, no replay broker). AnyCable Pro held 999,954 on 19 GB (the lightest setup that includes built-in replay). On cost shape: AnyCable Pro is flat-rate ($1,490/yr, unlimited instances); Pusher is per-connection (Premium $499/mo for 10K, Growth $699/mo for 15K, Plus $899/mo for 20K — 2026 plans); uWS and self-hosted Socket.io are box-priced. + Same Railway box (32 vCPU / 32 GB), four setups, 1,000,000-connection idle target. Socket.io accepted 119,826: the single Node event loop saturates handshakes regardless of memory available. uWS held 1,018,366 on 5.45 GB (lightest wire, no replay broker). AnyCable Pro held 822,037 on 14.80 GB (the lightest setup that includes built-in replay, at ~18 KB per connection). On cost shape: AnyCable Pro is flat-rate ($1,490/yr, unlimited instances); Pusher is per-connection (Premium $499/mo for 10K, Growth $699/mo for 15K, Plus $899/mo for 20K, see 2026 plans); uWS and self-hosted Socket.io are box-priced.

- Single instance, no Redis or NATS backplane. Socket.io's idle wall is the event loop (saturates at ~120K); AnyCable OSS hits the 32 GB box ceiling (~33 KB/conn); AnyCable Pro and uWS both clear 1M comfortably (Pro at 19 KB/conn with the broker, uWS at 5.4 KB/conn without). Per-test setup & harness: bench repo README. + Single instance, no Redis or NATS backplane. Socket.io's idle wall is the event loop (saturates at ~120K). AnyCable OSS and Pro both reached ~822K connections in this run, with OSS at ~34 KB/conn (28.3 GB total) and Pro at ~18 KB/conn (14.8 GB total). uWS clears 1M at ~5.4 KB/conn without replay. Per-test setup and harness: bench repo README.

@@ -716,8 +751,8 @@

3. How much can we scale before Socket.io (1M attempted)119,826 conns~52 KBno 200,000 AC OSS8.35 GB42 KByes 200,000 AC Pro3.56 GB18 KByes - 993,994 AC OSS, RAM-bound32.00 GB (ceiling)33 KByes - 999,954 AC Pro19.34 GB19 KByes + 821,877 AC OSS28.30 GB34 KByes + 822,037 AC Pro14.80 GB18 KByes 1,018,366 uWS5.45 GB5.4 KBno @@ -760,7 +795,7 @@

3. How much can we scale before

- {{!-- Throughput sub-section — sits inside the same Rubric 3 + {{!-- Throughput sub-section — sits inside the same Section 3 wrapper but gets its own sticky media so the table doesn't fight the capacity table for viewport height. --}}
@@ -792,33 +827,47 @@

3. How much can we scale before Multi-process — fan-out crosses a message bus Socket.io + Redis 2 nodes, in-proc pub9,99694,616772,798 Socket.io + Redis 2 nodes, HTTP pool=169,99348,56363,036 - AnyCable OSS 1 node, HTTP pool=169,99691,583163,693 - AnyCable Pro 1 node, HTTP pool=169,99786,625163,773 + AnyCable OSS 1 node, HTTP pool=169,99689,334151,953 + AnyCable Pro 1 node, HTTP pool=169,99787,696140,032 AnyCable cluster OSS 2 nodes + NATS, NATS pub9,99347,55372,145 -

Top group: WS server fans out broadcasts to its locally-connected sockets, publisher in the same process. Apples to apples — isolates the WS layer's own fan-out cost. The AnyCable in-process number is from anycable-go's benchi harness (embedded server, loopback httptest.Server) — same hub, same broker, same WS framing. Bottom group: multiple processes, broadcasts cross a network bus — the production shape for either architecture once you scale past one node.

Two findings to take from the bottom group. (1) In-process publishing wins, but it's a different shape. The Socket.io + Redis "in-proc pub" row hits 773K because the publisher and one of the WS servers share an event loop and the publisher has zero network hops to reach instance A's clients (only Redis pub/sub for instance B). This is the best case for Socket.io+Redis, but it requires the app code to live inside one of the WS processes — the architectural assumption AnyCable explicitly rejects. (2) Once the publisher is separated from the WS layer, both architectures hit a similar publisher-rate floor. Socket.io+Redis HTTP pool=16 caps at ~63K and AnyCable cluster NATS pub caps at ~72K — in both cases the 10K clients × 100 messages of fan-out work is finished long before the publisher manages to push the next batch. AnyCable's single-instance HTTP pool=16 wins at 164K because anycable-go's HTTP /_broadcast is a Go binary returning 200 as soon as the broadcast is enqueued, so the 16-way concurrent publisher can keep multiple broadcasts in flight; Socket.io's /_broadcast on the same shape is bound by Express's request handling.

Methodology details. Socket.io + Redis: two Node instances behind one Redis, 10K clients split 5K / 5K. "in-proc pub" runs the publisher inside instance A (io.to().emit() in the same process). "HTTP pool=16" has the bench-runner POSTing to instance A's /_broadcast at 16-way concurrency — instance A then fans out locally to its 5K subs and publishes to Redis for instance B. AnyCable cluster: two OSS anycable-go instances behind a shared NATS service, 10K clients split 5K / 5K, bench-runner publishes directly to NATS (the realistic shape for a NATS-backed AnyCable cluster). uWS has no built-in adapter; multi-node uWS is DIY.

+

Top group: WS server fans out broadcasts to its locally-connected sockets, publisher in the same process. Apples to apples — isolates the WS layer's own fan-out cost. The AnyCable in-process number is from anycable-go's benchi harness (embedded server, loopback httptest.Server) — same hub, same broker, same WS framing. Bottom group: multiple processes, broadcasts cross a network bus — the production shape for either architecture once you scale past one node.

Two findings to take from the bottom group. (1) In-process publishing wins, but it's a different shape. The Socket.io + Redis "in-proc pub" row hits 773K because the publisher and one of the WS servers share an event loop and the publisher has zero network hops to reach instance A's clients (only Redis pub/sub for instance B). This is the best case for Socket.io+Redis, but it requires the app code to live inside one of the WS processes — the architectural assumption AnyCable explicitly rejects. (2) Once the publisher is separated from the WS layer, both architectures hit a similar publisher-rate floor. Socket.io+Redis HTTP pool=16 caps at ~63K and AnyCable cluster NATS pub caps at ~72K — in both cases the 10K clients × 100 messages of fan-out work is finished long before the publisher manages to push the next batch. AnyCable's single-instance HTTP pool=16 wins at ~152K because anycable-go's HTTP /_broadcast is a Go binary returning 200 as soon as the broadcast is enqueued, so the 16-way concurrent publisher can keep multiple broadcasts in flight; Socket.io's /_broadcast on the same shape is bound by Express's request handling.

Methodology details. Socket.io + Redis: two Node instances behind one Redis, 10K clients split 5K / 5K. "in-proc pub" runs the publisher inside instance A (io.to().emit() in the same process). "HTTP pool=16" has the bench-runner POSTing to instance A's /_broadcast at 16-way concurrency — instance A then fans out locally to its 5K subs and publishes to Redis for instance B. AnyCable cluster: two OSS anycable-go instances behind a shared NATS service, 10K clients split 5K / 5K, bench-runner publishes directly to NATS (the realistic shape for a NATS-backed AnyCable cluster). uWS has no built-in adapter; multi-node uWS is DIY.

- {{!-- RUBRIC 4: Operational surface — new section. The reader's - question is "when this breaks at 3am, can my team diagnose - and fix it?" Three sub-questions: how many moving parts, - what's instrumented out of the box, and is the project - actively maintained? --}} + {{!-- TBD-B1: Clustered topology placeholders. Three new tests land here: + A2c (deploy-impact under rolling restart of a 3-node cluster), + A2d (clustered load + avalanche), A2e clustered tier (hop overhead + under cluster pub/sub). This is where the "is in-process WS + sustainable at scale?" question gets its empirical answer. --}}
-
-

4. When this breaks at 3am, can my team diagnose and fix it?#

+
+

What does a 3-node cluster look like?#

- Realtime infrastructure breaks at the worst possible time, often in the middle of the night. When it does, the on-call engineer is also the person shipping features tomorrow morning — every incident burns two days of velocity. The choice here is whether the system is observable, debuggable, and well-supported enough that an incident is a two-hour fix instead of a two-day investigation. Easy to set up doesn't matter — setup is one Saturday, operations is forever. Three things decide it: how many moving parts the system has, what's instrumented out of the box, and whether someone actually responds when you file an issue. + Past one node, the question shifts from "how big can the single instance get?" to "what does adding more nodes actually buy?" Three things to measure: can a rolling deploy survive without dropping connections? (the test that motivates separate-process WS in the first place), how does total capacity scale with N nodes?, and does the cluster pub/sub layer impose a latency tax on broadcasts?

- AnyCable ships Prometheus + StatsD and a /health endpoint in the box; Socket.io ships an Admin UI; uWS ships nothing observability-wise. All three projects are actively maintained — the table breaks down commit / contributor signal and known failure modes. + All clustered tests run with 3 nodes behind a load balancer, 10k connections held steady. We test the same 5 options: Socket.io (Redis adapter), Socket.io + CSR (Redis Streams), uWS, AnyCable OSS, and AnyCable Pro. +

+
+ Caveat: uWS clustering and standalone are not off-the-shelf configurations. uWebSockets.js has no built-in cross-node clustering and no built-in HTTP publish endpoint. To benchmark uWS in standalone mode we wrote a custom publish API; to benchmark it clustered we wrote a custom pub/sub bridge (Redis-backed). The numbers reflect what those custom implementations achieve, marked with in the tables. Production teams who pick uWS for anything past one in-process node end up building the same two things themselves. This is part of the trade Socket.io and AnyCable spare you: both ship the multi-node story as a documented, supported configuration. +
+
+
+ + {{!-- Rolling-deploy impact (A2c). The canonical answer to "can + horizontal scaling + rolling deploy substitute for separate-process WS?" --}} +
+
+

Rolling deploy: per-client disconnect window + message loss

+

+ 3-node cluster, 10k connections steady, drain + restart nodes sequentially. The test measures what every connected user actually experiences during a deploy: how long their connection is broken, how many messages they miss in that window, and whether the surviving nodes can absorb the reconnect storm from the node being recycled.

@@ -826,47 +875,34 @@

4. When this breaks at 3am, ca - - - - - - + + + + + - - - - - - - - + + + + + + +
Operational surfaceSocket.io+ CSRuWSAC OSSAC ProSetup 10k, 3 nodesGap p50Gap p99Msgs lostFull reconnect
Process count1 (Node)1 (Node)1 (Node)2 (app + Go)2 (app + Go)
Prometheus endpointAdmin UI onlyAdmin UI onlyDIYBuilt-inBuilt-in
Health check endpointDIYDIYDIY/health/health
Structured logsPluggablePluggableDIYJSON / textJSON / text
Graceful shutdown controlsManualManualManualConfigurable drainConfigurable drain
Replay buffer storagen/aMemory or Postgres adaptern/aMemory or NATS JS / RedisEmbedded broker
Commits / 90d23231012929
Maintainer supportGitHub issuesGitHub issues"Bug reports only"GitHub + DiscordGitHub + Discord + commercial
Socket.io embedded + Redis adapterTBD-A2cTBD-A2cTBD-A2cTBD-A2c
Socket.io + CSR embedded + Redis StreamsTBD-A2cTBD-A2cTBD-A2cTBD-A2c
uWS embedded + custom pub/subTBD-A2cTBD-A2cTBD-A2cTBD-A2c
Socket.io standaloneTBD-A2cTBD-A2cTBD-A2cTBD-A2c
uWS standalone + custom publishTBD-A2cTBD-A2cTBD-A2cTBD-A2c
AnyCable OSS standalone, alwaysTBD-A2cTBD-A2cTBD-A2cTBD-A2c
AnyCable Pro standalone, alwaysTBD-A2cTBD-A2cTBD-A2cTBD-A2c
-

Observability splits by where the WS layer runs — in-process Node options inherit Node's defaults (Admin UI at best, DIY for everything else); AnyCable's separate Go binary ships Prometheus, /health, structured logs, and a configurable drain. Pro adds commercial support and the embedded broker; the day-to-day surface is otherwise OSS's. On commit counts: uWS's 101 commits / 90d reflects active wire-level optimization work on a small surface (one C++ library, one author); AnyCable's 29 commits / 90d reflects a multi-language ecosystem in maintenance mode (8+ years in production, stable protocol, breaking changes are rare on purpose). High commit velocity at the framework layer is a debugging-surface signal, not necessarily a quality one — for a piece of infrastructure you don't want changing under you, lower is fine. Commit / contributor counts: 90 days ending 2026-05-09, via the GitHub API on socketio/socket.io, uNetworking/uWebSockets.js, anycable/anycable. uWS support quote: READMORE.md.

+

Placeholders pending A2c. Embedded rows show what happens when the WS layer restarts with the app on every node. Standalone rows show what happens when only the app restarts. uWS has no native clustering or built-in HTTP publish API; both are custom code built for this benchmark. The embedded vs standalone gap is the load-bearing data point: it answers whether horizontal scaling can substitute for a separate-process WS architecture.

-
-
- {{!-- RUBRIC 5: Optionality / exit cost — new section. The - reader's question is "if we're wrong about this, what does - it cost to leave?" Two pillars: protocol portability (can I - swap the server without rewriting the client?) and license - (is the open-source path real or a feeder for a SaaS?) --}} -
-
+ {{!-- Clustered capacity + avalanche (A2d) --}}
-

5. If we're wrong about this, what does it cost to leave?#

+

Clustered capacity and one-node-down avalanche

- Vendors die, get acquired, pivot, or quietly change pricing. Choosing realtime infrastructure is a multi-year bet, and the cost of unwinding the wrong bet matters as much as the cost of running the right one. Real optionality is about the protocol, not just the license — an open-source tool with a custom protocol that nothing else speaks is still de facto lock-in. Two things decide it: whether you can swap the server out without rewriting the client, and whether the open-source path leads to the same product as the commercial one (or is a teaser for a paywalled fork). -

-

- AnyCable speaks two open protocols: Action Cable (a documented JSON-over-WS protocol with public extensions) and Pusher protocol (drop-in for Pusher / Soketi / Laravel Reverb). The TypeScript client lives in @anycable/core and uses the standard browser WebSocket API underneath, so you can move clients off AnyCable without rewriting them. Socket.io is the inverse: protocol is documented but the ecosystem is officially JS-only, so the client is locked to the server. AnyCable Pro is not a paywalled fork — same protocol surface as OSS; you can move between OSS, Pro, and Managed without code changes. + Total capacity across 3 nodes (the realistic prod question, not single-instance), and what happens when 1 of 3 nodes fails: how fast do the survivors absorb the displaced load, and is anything lost in transit?

@@ -874,27 +910,35 @@

5. If we're wrong about this, - - - - - - + + + + + - - - - - - + + + + +
Optionality dimensionSocket.io+ CSRuWSAC OSSAC ProSetup 3-node clusterCapacityRAM/connRamp-up1-of-3 down
Protocol spec publicYesYesn/a (raw WS)YesYes
Other servers speak itNo (JS-only)No (JS-only)Any WS libDocumented WSDocumented WS
Other protocols supportedNoneNonen/aPusher (drop-in)Pusher (drop-in)
Client SDK portabilityLocked to serverLocked to serverNative WSNative WS + TS wrapperNative WS + TS wrapper
License (server)MITMITApache 2.0 + renameMITCommercial
Migration to/from the commercial tiern/an/an/aSame protocol → ProSame protocol → OSS
Socket.io + RedisTBD-A2dTBD-A2dTBD-A2dTBD-A2d
Socket.io + CSRTBD-A2dTBD-A2dTBD-A2dTBD-A2d
uWS custom pub/subTBD-A2dTBD-A2dTBD-A2dTBD-A2d
AnyCable OSSTBD-A2dTBD-A2dTBD-A2dTBD-A2d
AnyCable ProTBD-A2dTBD-A2dTBD-A2dTBD-A2d
-

The optionality story tracks the architecture choice too: Socket.io variants share lock-in (the JS-only ecosystem clients can't read messages from another server), uWS has none (raw WebSocket protocol — any client speaks it), AnyCable has none (documented Action Cable + Pusher protocols, native WebSocket underneath). Pro and OSS are the same surface; you can move between them, or to AnyCable+ Managed, without code changes. Action Cable spec: docs.anycable.io/misc/action_cable_protocol. Pusher protocol: docs.anycable.io/anycable-go/pusher. Socket.io protocol spec: socketio/socket.io-protocol — reference implementations are TypeScript only.

+

Placeholders pending A2d. Ramp-up rate is the rate at which new connections can be accepted across the cluster, a real-world bottleneck during reconnect storms. uWS clustering requires a custom pub/sub layer; the benchmark reports what we built, with the caveat that production teams would need to build something similar.

+ + {{!-- Topology takeaway: in-process at scale --}} +
+
+

Takeaway: when does in-process WS stop being viable?

+

+ TBD-A2c/A2d narrative. The rolling-deploy and one-node-down tables above answer the structural question: does horizontal scaling + a rolling deploy give you acceptable UX, or do you need a separate-process WS service to deploy independently? Conclusion depends on numbers we don't have yet. Two plausible outcomes: (1) rolling deploy + Socket.io CSR is fine for many apps, in which case in-process stays a real option; (2) even with CSR, the disconnect storm + replay tail is rough enough at 10k+ that production teams end up wanting the WS layer separate, which is what AnyCable ships by default. +

+
+
@@ -903,7 +947,7 @@

5. If we're wrong about this,
-

Migrating from Socket.io: replace the broadcast, keep the app#

+

Migrating from Socket.io#

Your Node.js app stays. Swap io.to().emit() for an HTTP POST to AnyCable's broadcast endpoint. Auth, validation, DB access — all unchanged.

@@ -959,8 +1003,8 @@

Migrating from Socket.io: repla
-

Try it in your stack#

-

+

Run it in your stack#

+

Three places to start: read the code, run a working demo, or wire it into a serverless setup.

@@ -1022,7 +1066,7 @@

What you don't have to build

MonitoringAdmin UIAdmin UIDIYPrometheus & StatsD -

uWS solves Socket.io's wire-overhead problem — not its framework gaps. Every feature beyond raw transport is still DIY, on the same single Node process. AnyCable OSS and Pro share every feature in this table; Pro's differentiators are per-connection memory efficiency, the embedded broker, and commercial support (see operations).

+

uWS solves Socket.io's wire-overhead problem — not its framework gaps. Every feature beyond raw transport is still DIY, on the same single Node process. AnyCable OSS and Pro share every feature in this table; Pro's differentiators are per-connection memory efficiency, the embedded broker, and commercial support.

@@ -1071,7 +1115,7 @@

When Socket.io is the right choice

-

Appendix: how the numbers come together#

+

Appendix#

The high-value mechanism behind each rubric, in one place. Full per-test methodology, raw output, and reproduction steps live in the benchmark repo README.

@@ -1117,7 +1161,7 @@

AnyCable's epoch + offset replay protocol,
-

FAQ#

+

FAQ#

Does AnyCable replace Socket.io, or work alongside it? @@ -1131,12 +1175,12 @@

F
How does AnyCable compare on performance? -
Three workloads, same 32 vCPU / 32 GB Railway box.

1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB (lightest bare wire layer; no replay broker). AnyCable Pro holds 999,954 on 19 GB — the lightest setup with built-in replay. Open-source AnyCable holds 993,994 at the box's 32 GB ceiling. Socket.io caps at 119,826 connections — its single Node event loop saturates handshakes regardless of memory.

10K reconnecting clients under jitter (WiFi-drop pattern, 1.2M expected deliveries): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR). Default Socket.io and uWS lose ~13–14% of messages — both at-most-once.

WS-layer throughput at 1M deliveries/sec target, publisher in-process (apples-to-apples WS fan-out): uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. Multi-process — fan-out crosses a network bus (publisher separated from WS layer): Socket.io + Redis with HTTP pool=16 publisher caps at ~63K/sec; AnyCable cluster (2 anycable-go + NATS, NATS publisher) caps at ~72K/sec — both publisher-bound, not WS-layer bound. The 773K Socket.io + Redis "in-process publisher" number (publisher colocated with one of the WS nodes) is a different architectural shape — one Socket.io's design supports but AnyCable's deliberately rejects. AnyCable single-instance HTTP pool=16 still wins at 164K because anycable-go's broadcast handler returns 200 fast, letting the 16-way publisher keep multiple broadcasts in flight. Reproduce: github.com/irinanazarova/anycable-socketio-benchmarks.
+
Three workloads, same 32 vCPU / 32 GB Railway box.

1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB (lightest bare wire layer, no replay broker). AnyCable Pro holds 822,037 on 14.80 GB (the lightest setup with built-in replay). Open-source AnyCable holds 821,877 on 28.30 GB. Socket.io caps at 119,826 connections: the single Node event loop saturates handshakes regardless of memory.

10K reconnecting clients under jitter (WiFi-drop pattern, 1.2M expected deliveries): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR). Default Socket.io and uWS lose ~13–14% of messages, both at-most-once.

WS-layer throughput at 1M deliveries/sec target, publisher in-process (apples-to-apples WS fan-out): uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. Multi-process, fan-out crosses a network bus (publisher separated from WS layer): Socket.io + Redis with HTTP pool=16 publisher caps at ~63K/sec; AnyCable cluster (2 anycable-go + NATS, NATS publisher) caps at ~72K/sec, both publisher-bound, not WS-layer bound. The 773K Socket.io + Redis "in-process publisher" number (publisher colocated with one of the WS nodes) is a different architectural shape: one Socket.io's design supports but AnyCable's deliberately rejects. AnyCable single-instance HTTP pool=16 wins at ~152K because anycable-go's broadcast handler returns 200 fast, letting the 16-way publisher keep multiple broadcasts in flight. Reproduce: github.com/irinanazarova/anycable-socketio-benchmarks.
What about uWebSockets.js? It's faster than Socket.io. -
uWS is genuinely faster on the wire — we measured it. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 19 GB). The "10× faster than Socket.io" claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses ~14% of messages — the same loss profile as Socket.io without CSR — because both at-most-once and embedded in the same Node process as your app. uWS solves "Socket.io's wire is too heavy"; it doesn't solve "we lose messages during disruption" or "every deploy hits our users." See the full uWebSockets.js comparison above.
+
uWS is genuinely faster on the wire — we measured it. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 14.80 GB at 822K). The "10× faster than Socket.io" claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses ~14% of messages — the same loss profile as Socket.io without CSR — because both at-most-once and embedded in the same Node process as your app. uWS solves "Socket.io's wire is too heavy"; it doesn't solve "we lose messages during disruption" or "every deploy hits our users."
@@ -1280,7 +1324,7 @@

Try AnyCable in your stack

"name": "How does AnyCable compare on performance?", "acceptedAnswer": { "@type": "Answer", - "text": "Three workloads on the same 32 vCPU / 32 GB Railway box. (1) 1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB (lightest bare wire, no replay broker); AnyCable Pro holds 999,954 on 19 GB (lightest setup with built-in replay); open-source AnyCable holds 993,994 at the box's 32 GB ceiling; Socket.io caps at 119,826 because its single Node event loop saturates handshakes regardless of memory. (2) 10K reconnecting clients under jitter (WiFi-drop pattern, 1.2M expected deliveries): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR); default Socket.io and uWS lose ~13–14% of messages, both being at-most-once. (3) WS-layer throughput at 1M deliveries/sec target, publisher in-process: uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. Multi-process — fan-out crosses a network bus (publisher separated from WS layer): Socket.io + Redis HTTP pool=16 publisher caps at ~63K/sec; AnyCable cluster (2 anycable-go + NATS) caps at ~72K/sec — both publisher-bound. The 773K Socket.io + Redis 'in-process publisher' number is a different shape (publisher colocated with one of the WS nodes), which Socket.io supports and AnyCable rejects. AnyCable single-instance HTTP pool=16 wins at 164K because anycable-go's broadcast handler is faster than Socket.io's at returning 200. Throughput is not AnyCable's headline win on this page; capacity, reliability, and deploys are. Source: github.com/irinanazarova/anycable-socketio-benchmarks." + "text": "Three workloads on the same 32 vCPU / 32 GB Railway box. (1) 1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB (lightest bare wire, no replay broker); AnyCable Pro holds 822,037 on 14.80 GB (lightest setup with built-in replay); open-source AnyCable holds 821,877 on 28.30 GB; Socket.io caps at 119,826 because its single Node event loop saturates handshakes regardless of memory. (2) 10K reconnecting clients under jitter (WiFi-drop pattern, 1.2M expected deliveries): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR); default Socket.io and uWS lose ~13–14% of messages, both being at-most-once. (3) WS-layer throughput at 1M deliveries/sec target, publisher in-process: uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. Multi-process, fan-out crosses a network bus (publisher separated from WS layer): Socket.io + Redis HTTP pool=16 publisher caps at ~63K/sec; AnyCable cluster (2 anycable-go + NATS) caps at ~72K/sec, both publisher-bound. The 773K Socket.io + Redis 'in-process publisher' number is a different shape (publisher colocated with one of the WS nodes), which Socket.io supports and AnyCable rejects. AnyCable single-instance HTTP pool=16 wins at ~152K because anycable-go's broadcast handler is faster than Socket.io's at returning 200. Throughput is not AnyCable's headline win on this page; capacity, reliability, and deploys are. Source: github.com/irinanazarova/anycable-socketio-benchmarks." } }, { @@ -1288,7 +1332,7 @@

Try AnyCable in your stack

"name": "What about uWebSockets.js? It's faster than Socket.io.", "acceptedAnswer": { "@type": "Answer", - "text": "uWS is genuinely faster on the wire — measured. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 19 GB). The '10× faster than Socket.io' claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses about 14% of messages — the same loss profile as Socket.io without CSR — because both are at-most-once and embedded in the same Node process as your app. uWS solves 'Socket.io's wire is too heavy'; it doesn't solve 'we lose messages during disruption' or 'every deploy hits our users.'" + "text": "uWS is genuinely faster on the wire — measured. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 14.80 GB at 822K). The '10× faster than Socket.io' claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses about 14% of messages — the same loss profile as Socket.io without CSR — because both are at-most-once and embedded in the same Node process as your app. uWS solves 'Socket.io's wire is too heavy'; it doesn't solve 'we lose messages during disruption' or 'every deploy hits our users.'" } }, { @@ -1373,12 +1417,12 @@

Try AnyCable in your stack

{ "@context": "https://schema.org", "@type": "TechArticle", - "headline": "AnyCable vs Socket.io vs uWebSockets.js: 100% delivery, deploy-resilient WebSockets, 1M idle connections per node", - "description": "Same Railway box, four setups: Socket.io accepted 119,826 (single Node event loop saturated); uWebSockets.js held 1,018,366 on 5.45 GB (lightest wire, no replay); open-source AnyCable held 993,994 (32 GB RAM ceiling); AnyCable Pro held 999,954 on 19 GB. Replay-less setups (default Socket.io, uWS) lost ~13–14% of messages under jitter; AnyCable and Socket.io+CSR delivered 100%. In-process WS (Socket.io, +CSR, uWS) dies on every Node deploy and past ~20K connections doesn't recover; AnyCable runs WS as a separate process so deploys don't touch it. In-process WS-layer throughput: uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. All numbers reproducible from the open-source bench repo.", + "headline": "AnyCable vs Socket.io vs uWebSockets.js: Benchmarking WebSocket infrastructure for JS/TS apps", + "description": "A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions answered with reproducible benchmarks: (1) how fast — roundtrip latency at 1k/10k/100k subscribers, broadcast throughput, plus whispers (client-to-client without backend hop) where AnyCable competes with Liveblocks, Yjs, PartyKit; (2) how reliable — message delivery under WiFi-drop jitter, separating at-most-once protocols (default Socket.io, uWS) from those with replay state (Socket.io+CSR, AnyCable); (3) how efficient to scale — single-node load (RAM per connection, max concurrent) and 3-node clustered rolling-deploy tests that decide whether in-process WS is sustainable. Tested in embedded and standalone topologies. All numbers reproducible from the open-source bench repo at github.com/irinanazarova/anycable-socketio-benchmarks.", "url": "https://anycable.io/compare/socket-io", "mainEntityOfPage": "https://anycable.io/compare/socket-io", "datePublished": "2026-04-30", - "dateModified": "2026-05-13", + "dateModified": "2026-05-28", "author": { "@type": "Organization", "name": "AnyCable team", diff --git a/src/compare/socket-io/style/index.html b/src/compare/socket-io/style/index.html new file mode 100644 index 0000000..0c7d11f --- /dev/null +++ b/src/compare/socket-io/style/index.html @@ -0,0 +1,412 @@ + + + {{> dochead pageTitle="Compare-page style index — design language spine" pageDescription="Internal style index for the AnyCable comparison-page design spine. Every element rendered in one place so we can iterate the lab-notebook design language without redesigning 30 sections in parallel." pageUrl="https://anycable.io/compare/socket-io/style"}} + +
+ {{> header}} +
+ + {{!-- ================================================== --}} + {{!-- 0. PAGE HEADER — what is this page --}} + {{!-- ================================================== --}} +
+ Style index +

+ AnyCable vs Socket.io +

+

+ Internal style index for the comparison-page design spine. Every typographic role, + layout primitive, and component the page uses, rendered once each. Iterate here, + not in the 1448-line page. Live tokens are CSS custom properties on + .c-spine — tweak in devtools. +

+

+ Signature: lab notebook. Mono structural labels. Light tinted code blocks. Tight + 580px prose column. No dark surfaces on this page. +

+
+ + {{!-- ================================================== --}} + {{!-- 1. TYPOGRAPHY ROLES --}} + {{!-- ================================================== --}} +
+ Section 01 +

Typography roles

+

+ Five voices. Anything else is a re-skin of one of these. +

+ +
+ + .c-display +

Display heading

+ + .c-section +

Section heading

+ + .c-sub +

Subsection heading

+ + .c-label + A flat label + + .c-label--bracket + Bench 01 + + .c-label--accent + Best + + .c-prose +

+ Body prose. Capped at 580px so the line length stays in the readable 60-ish + characters at 19px. Inline code sits as a calm pill. Strong + for emphasis. Links carry the accent color. +

+ + .c-meta +

+ Meta text. Smaller (15px), darker gray (#555). Used for footnotes under tables + and supporting paragraphs that orbit a main one. +

+ + .c-caption + Caption mono · figure subtitles · methodology footnotes + +
+
+ + {{!-- ================================================== --}} + {{!-- 2. THE "vs" TREATMENT — before / after --}} + {{!-- ================================================== --}} +
+ Section 02 +

The "vs" treatment

+

+ Operator-style, mono, accent red, vertical-aligned. Reads as a typographic operator + instead of a faded word. The grey-mute span is gone. +

+ +
+
+ Before +
+ + AnyCable vs Socket.io + +
+

46px display, gray span same color as captions

+
+
+ After +
+

+ AnyCable vs Socket.io +

+
+

.c-vs · mono 0.42em · accent red · vertical-aligned 0.45em

+
+
+
+ + {{!-- ================================================== --}} + {{!-- 3. RUBRIC LAYOUT — two-column with sticky media --}} + {{!-- ================================================== --}} +
+
+ Section 03 +

Rubric layout

+

+ Two columns, prose left at 580px max, media right (data table or code). The + media column can be sticky if the prose is long. +

+
+ +
+
+ Bench 01 +

How fast is the roundtrip?

+

+ Latency at 1k, 10k, and 100k concurrent connections, plus broadcast throughput + and whispers. Embedded vs standalone topologies, both measured. Numbers here + are p50 roundtrip at 10k — the steady-state of a typical production setup. +

+

+ AnyCable Pro is fastest because the Go broker drops the network hop in the + hot path; uWS comes second because it's in-process; Socket.io trails because + the JS event loop is shared with your app code. +

+

+ Methodology and the bench harness live on + GitHub. +

+
+ +
+
+
+ Fig. 1 + p50 roundtrip @ 10K connections +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Setupp50p99
Default Socket.io47 ms182 ms
Socket.io + CSR49 ms194 ms
uWebSockets.js31 ms98 ms
AnyCable24 ms71 ms
AnyCable Pro19 ms58 ms
+
+ Identical Railway hardware · 10K idle subs · 1KB payload · 60s window +
+
+
+
+ + {{!-- ================================================== --}} + {{!-- 4. CODE BLOCK — light tinted, muted syntax --}} + {{!-- ================================================== --}} +
+ Section 04 +

Code blocks

+

+ Light surface (#fafafa), 1px subtle border, muted syntax tints so + the block reads as part of the prose layer, not as a foreign material. The + tokens are warm amber for keywords, deep green for strings, slate gray for + comments, deep red for errors. +

+ +
+
+ Before +
// Server
+const io = new Server(httpServer);
+
+io.on('connection', (socket) => {
+  socket.on('subscribe', (t) => socket.join(t));
+});
+
+io.to('chat:42').emit('message', payload);
+

Dark #1a1a1a, bright syntax — reads as island

+
+
+ After +
// Server
+const io = new Server(httpServer);
+
+io.on('connection', (socket) => {
+  socket.on('subscribe', (t) => socket.join(t));
+});
+
+io.to('chat:42').emit('message', payload);
+

Light #fafafa, muted syntax — part of the page

+
+
+ + Standalone, framed +
+
+ Code 03 + AnyCable broadcast from your Node app +
+
+
// Server — broadcast over HTTP to the anycable-go binary
+import { broadcast } from '@anycable/serverless-js';
+
+await broadcast('chat:42', {
+  type: 'message',
+  body: payload,
+});
+
+// Replay, history, presence — all server-side. Your app stays
+// stateless. WS layer scales and deploys independently.
+
+ Server-side broadcast · same code in OSS and Pro · WS-layer process is a separate binary +
+
+ + {{!-- ================================================== --}} + {{!-- 5. CALLOUT --}} + {{!-- ================================================== --}} +
+ Section 05 +

Callouts

+

+ One callout primitive. The bracketed label carries the type signal (NOTE, + CAVEAT, METHOD), so we don't need different colors. +

+ +
+
+ Note + Numbers are p50 over a 60-second steady-state window. Cold-start latency + behaves differently; see the Headroom section. +
+ +
+ Caveat + uWebSockets.js doesn't ship cluster-mode out of the box. The 3-node test + uses a Redis-backed adapter we wrote for this benchmark, available in the + bench/uws-cluster branch. +
+ +
+ Method + Each setup runs on identical Railway hardware (4 vCPU, 8GB). Numbers are + reproducible from the + open-source benchmark repo. +
+
+
+ + {{!-- ================================================== --}} + {{!-- 6. CARDS --}} + {{!-- ================================================== --}} +
+ Section 06 +

Cards

+

+ One card primitive. Hero cards, try-it cards, impact cards — same component, + different label. The bracketed label is the type signal. +

+ + +
+ + {{!-- ================================================== --}} + {{!-- 7. BULLET LIST + TOC --}} + {{!-- ================================================== --}} +
+ Section 07 +

Lists & navigation

+ +

Bullet list

+
    +
  • Default Socket.io — the baseline most teams have today.
  • +
  • Socket.io + CSR — in-place delivery upgrade Socket.io shipped in 4.6.
  • +
  • uWebSockets.js + topics — the most-cited "just use uWS" alternative.
  • +
  • AnyCable OSS — separate Go binary with broker built in.
  • +
  • AnyCable Pro — same protocol, denser per-connection memory.
  • +
+ +

TOC navigation row

+ +
+ + {{!-- ================================================== --}} + {{!-- 8. CTA --}} + {{!-- ================================================== --}} +
+
+ Try it +

Run the benchmarks yourself

+

+ Every number on this page is reproducible from the open-source bench harness. + Clone, set your hardware, run the same setups. The numbers below are ours. +

+ +

+ Open source · MIT · Identical hardware · Reproducible numbers +

+
+
+ + {{!-- ================================================== --}} + {{!-- 9. TOKEN INVENTORY (devtools cheat sheet) --}} + {{!-- ================================================== --}} +
+ Appendix +

Token inventory

+

+ Every token is a CSS custom property on .c-spine. Tweak in devtools + to iterate. Anything you change here is what we then promote into + compare-spine.scss. +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
TokenValueUsed by
--c-fs-display64px.c-display (H1)
--c-fs-section36px.c-section (H2)
--c-fs-sub22px.c-sub (H3)
--c-fs-body19px.c-prose
--c-fs-meta15px.c-meta, table cells
--c-fs-caption13px.c-caption, figure caption
--c-fs-label12px.c-label, eyebrows, table heads
--c-prose-max580px.c-prose hard cap
--c-space-section96pxsection vertical
--c-space-block32pxafter H2
--c-space-para24pxafter body paragraph
--c-accent#f64343vs, links, anchors
--c-best#16a34awinning values only
--c-code-bg#fafafa.c-code surface
+
+ +
+ {{> footer}} +
+ + diff --git a/src/index.scss b/src/index.scss index 0453048..6b3ebce 100644 --- a/src/index.scss +++ b/src/index.scss @@ -30,4 +30,5 @@ @import './modules/blocks/blog-header.scss'; @import './modules/blocks/demo.scss'; @import './modules/blocks/compare.scss'; +@import './modules/blocks/compare-spine.scss'; @import './modules/common.scss'; diff --git a/src/js/components/Popup.ts b/src/js/components/Popup.ts index 9ff7dfe..b654791 100644 --- a/src/js/components/Popup.ts +++ b/src/js/components/Popup.ts @@ -8,12 +8,16 @@ export default class Popup { } init() { + // Popup host markup isn't on every page (e.g. /compare/socket-io + // has no try-now / contact-us popups). Skip silently if absent. + if (!this._popup) return; setTimeout(() => { this._popup.style.display = 'inherit'; //prevents flickering while loading }, 0); } open() { + if (!this._popup) return; const popupContainer = this._popup.querySelector( '.popup__container' ) as HTMLDivElement; @@ -24,6 +28,7 @@ export default class Popup { } close() { + if (!this._popup) return; const popupContainer = this._popup.querySelector( '.popup__container' ) as HTMLDivElement; diff --git a/src/js/components/demo.js b/src/js/components/demo.js index 8b781ed..ef61a9c 100644 --- a/src/js/components/demo.js +++ b/src/js/components/demo.js @@ -75,9 +75,13 @@ const scenario = [ }, ]; -const demo = new DemoController({ - element: document.querySelector("[data-controller='demo']"), - scenario, -}); - -demo.init(); +// Demo widget only exists on the home page. Skip wiring on pages +// where the host element isn't present (compare/, docs/, etc.). +const demoElement = document.querySelector("[data-controller='demo']"); +if (demoElement) { + const demo = new DemoController({ + element: demoElement, + scenario, + }); + demo.init(); +} diff --git a/src/modules/blocks/compare-spine.scss b/src/modules/blocks/compare-spine.scss new file mode 100644 index 0000000..711885a --- /dev/null +++ b/src/modules/blocks/compare-spine.scss @@ -0,0 +1,1629 @@ +// ===================================================================== +// Compare-page SPINE — design language proposal +// Scope: anycable.io/compare/socket-io and any sibling comparison pages. +// +// Status: iteration partial. Scoped under .c-spine so it can be previewed +// on /compare/socket-io/style/ without disturbing the existing styles +// in compare.scss. Once approved, lift the scope and let the c-* roles +// govern .compare-page (and retire the duplicates in compare.scss). +// +// What's in here: +// 1. Tokens (CSS custom properties + Sass vars) for the five roles: +// typography / spacing / surface / accent / radius. +// 2. Base role classes — one per typographic voice, no more. +// Display .c-display, Section .c-section, Sub .c-sub, Label .c-label, +// Body .c-prose, Meta .c-meta, Caption .c-caption. +// 3. The lab-notebook signatures: +// .c-label bracketed mono uppercase, [ BENCH 01 ] style +// .c-vs mono operator-style "vs", accent red +// .c-figure labelled figure with caption below +// .c-code light tinted code, muted syntax +// 4. Layout primitives — .c-rubric (two-col), prose col capped 580px. +// 5. Components — .c-table, .c-callout, .c-card, .c-cta. +// +// Tokens are also exposed as CSS custom properties on .c-spine so the +// style index can demo overrides in devtools without recompiling Sass. +// ===================================================================== + +@import '../variables.scss'; +@import '../mixins.scss'; + +// ---------- Spine scope ---------------------------------------------- +.c-spine { + // ----- Tokens as CSS custom properties (live, devtools-tweakable) -- + --c-font-display: 'Stem', Arial, sans-serif; + --c-font-body: 'Stem', Arial, sans-serif; + --c-font-mono: 'Martian Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + + // Type scale (px). Six roles, one display, one body, two heading levels, + // one label voice (mono), and two demote sizes (meta + caption). + --c-fs-display: 64px; + --c-fs-section: 36px; + --c-fs-sub: 22px; + --c-fs-body: 19px; + --c-fs-meta: 15px; + --c-fs-caption: 13px; + --c-fs-label: 12px; + + --c-lh-display: 1.05; + --c-lh-section: 1.15; + --c-lh-sub: 1.3; + --c-lh-body: 1.6; + --c-lh-meta: 1.5; + --c-lh-caption: 1.4; + + --c-tracking-display: -0.02em; + --c-tracking-section: -0.01em; + --c-tracking-label: 0.08em; + + // Spacing scale (8px base). Use multiples; do not invent magic numbers. + --c-space-section: 96px; + --c-space-section-wide: 128px; + --c-space-block: 32px; // after H2 + --c-space-sub: 20px; // after H3 + --c-space-para: 24px; // after body paragraph + --c-space-bullet: 12px; // between bullet items + --c-space-inline-1: 4px; + --c-space-inline-2: 8px; + --c-space-inline-3: 12px; + --c-space-inline-4: 16px; + + // Prose width — Stripe Docs-tight, 580px is ~58–62 characters at 19px. + --c-prose-max: 580px; + --c-prose-max-full: 680px; + + // Surfaces — three materials, no dark. + --c-paper: #{$backgroundPrimaryColor}; // #fff + --c-panel: #{$backgroundSecondaryColor}; // #f5f5f5 + --c-code-bg: #fafafa; + --c-code-border: #ececec; + + // Hairlines and text grays. One gray scale, four steps. + --c-text: #1a1a1a; + --c-text-meta: #555; + --c-text-quiet: #777; + --c-text-faint: #bbb; + --c-border: #e8e8e8; + --c-border-soft: #f0f0f0; + + // Accents — accent red is the comparison voice, green is the winner. + --c-accent: #{$accentPrimaryColor}; // #f64343 + --c-best: #16a34a; + --c-worst: #b91c1c; + + // Muted syntax tokens (light code surface) — readable on #fafafa. + --c-code-key: #b45309; // amber-brown + --c-code-str: #166534; // deep green + --c-code-com: #6b7280; // slate + --c-code-err: #991b1b; // deep red + + --c-radius: 8px; + + // ----- Page base ---------------------------------------------------- + // Dotted notebook bg was removed — pure decoration, communicated + // nothing. Plain paper bg lets data + type carry the page. + font-family: var(--c-font-body); + font-size: var(--c-fs-body); + line-height: var(--c-lh-body); + color: var(--c-text); + background: var(--c-paper); + + // ----- Role classes ------------------------------------------------ + .c-display { + font-family: var(--c-font-display); + font-size: var(--c-fs-display); + line-height: var(--c-lh-display); + letter-spacing: var(--c-tracking-display); + font-weight: 700; + margin: 0 0 var(--c-space-block); + + @include mediaMax($mobile) { + font-size: 44px; + line-height: 1.08; + } + } + + .c-section { + font-family: var(--c-font-display); + font-size: var(--c-fs-section); + line-height: var(--c-lh-section); + letter-spacing: var(--c-tracking-section); + font-weight: 700; + margin: 0 0 var(--c-space-block); + + @include mediaMax($mobile) { + font-size: 28px; + } + } + + .c-sub { + font-family: var(--c-font-display); + font-size: var(--c-fs-sub); + line-height: var(--c-lh-sub); + font-weight: 600; + margin: 0 0 var(--c-space-sub); + } + + // Label — mono, uppercase, tracked, optional brackets via .c-label--bracket + // Used everywhere structural: eyebrows, figure numbers, table headers, + // hero card titles. This is the lab-notebook signature. + .c-label { + font-family: var(--c-font-mono); + font-size: var(--c-fs-label); + line-height: 1.4; + letter-spacing: var(--c-tracking-label); + font-weight: 500; + text-transform: uppercase; + color: var(--c-text-meta); + display: inline-block; + + &--bracket::before { + content: '[ '; + color: var(--c-text-faint); + } + &--bracket::after { + content: ' ]'; + color: var(--c-text-faint); + } + + &--accent { + color: var(--c-accent); + } + &--strong { + color: var(--c-text); + } + } + + // Eyebrow above a section title — semantic shortcut, .c-label sized + // and spaced as a block. + .c-eyebrow { + @extend .c-label; + display: block; + margin: 0 0 var(--c-space-inline-3); + } + + .c-prose { + font-size: var(--c-fs-body); + line-height: var(--c-lh-body); + margin: 0 0 var(--c-space-para); + max-width: var(--c-prose-max); + + // Inline code — calm pill, doesn't fight prose. + code { + font-family: var(--c-font-mono); + font-size: 0.85em; + font-weight: 500; + background: rgba(0, 0, 0, 0.045); + padding: 1px 6px; + border-radius: 4px; + vertical-align: 1px; + } + + strong { + font-weight: 600; + color: var(--c-text); + } + } + + .c-meta { + font-size: var(--c-fs-meta); + line-height: var(--c-lh-meta); + color: var(--c-text-meta); + margin: 0 0 var(--c-space-para); + max-width: var(--c-prose-max); + } + + .c-caption { + font-family: var(--c-font-mono); + font-size: var(--c-fs-caption); + line-height: var(--c-lh-caption); + color: var(--c-text-quiet); + letter-spacing: 0.02em; + } + + // ----- The "vs" operator — mono accent, small, optical-vertical ----- + // Use inside .c-display: vs + // Reads as a typographic operator, not a faded word. + .c-vs { + font-family: var(--c-font-mono); + font-size: 0.42em; // relative to parent .c-display + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--c-accent); + vertical-align: 0.45em; // optically center against descenders + margin: 0 0.25em; + } + + // ----- Layout primitive: .c-rubric two-col with sticky media -------- + .c-rubric { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 64px; + padding: var(--c-space-section) 64px; + border-bottom: 1px solid var(--c-border); + background: transparent; + + @include mediaMax($tablet) { + grid-template-columns: 1fr; + gap: var(--c-space-block); + padding: 56px 24px; + } + + &--full { + grid-template-columns: 1fr; + justify-items: center; + + > * { + max-width: var(--c-prose-max-full); + width: 100%; + } + } + + &__content { + // Prose lives capped at --c-prose-max so the eye lands at a stable + // x-position. The grid track is still 1fr; the cap is on the prose + // itself, not the column. + > * { + max-width: var(--c-prose-max); + } + > .c-figure, + > .c-table, + > .c-callout, + > .c-code { + max-width: none; + } + } + + &__media { + align-self: start; + + &--sticky { + position: sticky; + top: 80px; + max-height: calc(100vh - 100px); + overflow-y: auto; + + @include mediaMax($tablet) { + position: static; + max-height: none; + overflow: visible; + } + } + } + } + + // ----- Figure: labelled container for tables, code, charts ---------- + // [ FIG. 3 ] How latency scales with concurrent connections + // ┌─────────────────────────────────┐ + // │ figure body (table / code / svg) │ + // └─────────────────────────────────┘ + // Methodology footnote, in mono small. + .c-figure { + display: block; + margin: 0 0 var(--c-space-block); + + &__header { + display: flex; + align-items: baseline; + gap: var(--c-space-inline-3); + margin-bottom: var(--c-space-inline-3); + } + + &__label { + @extend .c-label; + @extend .c-label--bracket; + flex: 0 0 auto; + } + + &__title { + font-family: var(--c-font-body); + font-size: var(--c-fs-meta); + font-weight: 600; + color: var(--c-text); + line-height: var(--c-lh-meta); + } + + &__body { + // The body is whatever lives inside: table, code, chart. + // Nothing painted here — the contained component owns its surface. + } + + &__caption { + @extend .c-caption; + display: block; + margin-top: var(--c-space-inline-3); + max-width: 100%; + } + } + + // ----- Code block: light, muted syntax ------------------------------ + .c-code { + width: 100%; + margin: 0; + padding: 24px 28px; + background: var(--c-code-bg); + border: 1px solid var(--c-code-border); + border-radius: var(--c-radius); + color: var(--c-text); + font-family: var(--c-font-mono); + font-size: 13px; + line-height: 1.7; + white-space: pre; + overflow-x: auto; + + .c-code-key { + color: var(--c-code-key); + } + .c-code-str { + color: var(--c-code-str); + } + .c-code-com { + color: var(--c-code-com); + font-style: italic; + } + .c-code-err { + color: var(--c-code-err); + } + } + + // ----- Table ------------------------------------------------------- + // Mono headers (uppercase tracked), tabular-num body cells, best/worst + // tints. Lives inside .c-figure for the label + caption. + .c-table { + width: 100%; + border-collapse: collapse; + font-size: var(--c-fs-meta); + line-height: var(--c-lh-meta); + + th, + td { + padding: 10px 14px; + text-align: left; + border-bottom: 1px solid var(--c-border-soft); + } + + th { + font-family: var(--c-font-mono); + font-size: var(--c-fs-label); + font-weight: 500; + letter-spacing: var(--c-tracking-label); + text-transform: uppercase; + color: var(--c-text-meta); + border-bottom: 1px solid var(--c-border); + background: var(--c-paper); + } + + td.c-num, + td .c-num { + font-family: var(--c-font-mono); + font-variant-numeric: tabular-nums; + font-weight: 500; + } + + .c-best { + color: var(--c-best); + font-weight: 600; + } + .c-worst { + color: var(--c-worst); + } + + tr.is-best td { + background: rgba(22, 163, 74, 0.04); + } + tr.is-worst td { + background: rgba(185, 28, 28, 0.04); + } + } + + // ----- Callout: soft tinted aside, left-rule + label --------------- + // [ NOTE ] Methodology caveat or framing for the data that follows. + .c-callout { + padding: 16px 20px; + margin: 0 0 var(--c-space-block); + background: #fbfbfb; + border-left: 3px solid var(--c-border); + border-radius: 0 4px 4px 0; + font-size: var(--c-fs-meta); + line-height: var(--c-lh-meta); + color: var(--c-text-meta); + + &__label { + @extend .c-label; + @extend .c-label--bracket; + display: block; + margin-bottom: 6px; + } + + strong { + color: var(--c-text); + font-weight: 600; + } + code { + font-family: var(--c-font-mono); + background: rgba(0, 0, 0, 0.05); + padding: 1px 5px; + border-radius: 3px; + } + } + + // ----- Card: hero-cards, try-it cards, impact cards ---------------- + // One card primitive, three uses. Variant via modifier classes. + .c-card { + display: block; + padding: 20px 22px; + background: var(--c-paper); + border: 1px solid var(--c-border); + border-radius: var(--c-radius); + text-decoration: none; + color: inherit; + transition: border-color 200ms, transform 200ms, box-shadow 200ms; + + &__label { + @extend .c-label; + @extend .c-label--bracket; + display: block; + margin-bottom: 10px; + } + + &__title { + font-family: var(--c-font-display); + font-size: var(--c-fs-sub); + font-weight: 600; + line-height: var(--c-lh-sub); + margin: 0 0 6px; + color: var(--c-text); + } + + &__desc { + font-size: var(--c-fs-meta); + line-height: var(--c-lh-meta); + color: var(--c-text-meta); + margin: 0; + } + + &--linked:hover { + border-color: var(--c-accent); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + } + } + + // ----- Bullet list (red dot, same scale as prose) ------------------ + .c-bullet-list { + margin: 0 0 var(--c-space-block); + padding: 0; + list-style: none; + line-height: var(--c-lh-body); + max-width: var(--c-prose-max); + + li { + position: relative; + padding-left: 18px; + margin-bottom: var(--c-space-bullet); + } + + li::before { + content: '·'; + position: absolute; + left: 0; + top: 0; + color: var(--c-accent); + font-weight: 700; + } + } + + // ----- Link -------------------------------------------------------- + a.c-link { + color: var(--c-accent); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: border-color 200ms; + + &:hover { + border-bottom-color: var(--c-accent); + } + } + + // ----- TOC nav (TL;DR style row of section links) ------------------ + .c-toc { + display: flex; + flex-wrap: wrap; + gap: var(--c-space-inline-3) var(--c-space-inline-4); + margin: var(--c-space-block) 0 0; + padding: 16px 0 0; + border-top: 1px solid var(--c-border); + max-width: var(--c-prose-max); + + &__label { + @extend .c-label; + @extend .c-label--bracket; + } + + a { + font-family: var(--c-font-mono); + font-size: var(--c-fs-caption); + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--c-text-meta); + text-decoration: none; + border-bottom: 1px solid var(--c-border); + padding-bottom: 1px; + transition: color 200ms, border-color 200ms; + + &:hover { + color: var(--c-accent); + border-bottom-color: var(--c-accent); + } + } + } + + // ----- CTA -------------------------------------------------------- + .c-cta { + text-align: center; + padding: var(--c-space-section-wide) 24px; + border-top: 1px solid var(--c-border); + + &__title { + @extend .c-section; + margin-bottom: var(--c-space-block); + } + + &__actions { + display: inline-flex; + gap: var(--c-space-inline-4); + margin-bottom: var(--c-space-block); + } + + &__footnote { + @extend .c-caption; + color: var(--c-text-quiet); + } + } + + // ----- Button (one primitive, two variants) ----------------------- + .c-btn { + display: inline-block; + padding: 12px 24px; + font-family: var(--c-font-body); + font-size: var(--c-fs-meta); + font-weight: 600; + line-height: 1; + border-radius: 6px; + text-decoration: none; + transition: opacity 200ms, transform 200ms; + + &--primary { + background: var(--c-accent); + color: #fff; + + &:hover { + opacity: 0.9; + transform: translateY(-1px); + } + } + + &--secondary { + background: transparent; + color: var(--c-text); + border: 1px solid var(--c-border); + + &:hover { + border-color: var(--c-text); + } + } + } + + // ===================================================================== + // MIGRATION OVERRIDES — STRICT REDESIGN + // Activated by adding class="c-spine" to the .compare-page main element. + // Imported AFTER compare.scss in src/index.scss so these rules win on + // specificity ties (last loaded wins). + // + // This block is NOT cosmetic. It restructures: left-aligned hero with + // a results-strip card row, auto-numbered sections, display-sized H2s, + // editorial TL;DR with rules, figure framing for every table + code + // block, hairline section dividers, mono label vocabulary across the + // entire page. + // ===================================================================== + + // ---- Reusable mono label placeholder ------------------------------- + // Mono uppercase tracked text. No brackets — they were aesthetic + // decoration. The label content itself communicates the idea. + %c-bracket-label { + font-family: var(--c-font-mono); + font-size: var(--c-fs-label); + line-height: 1.4; + letter-spacing: var(--c-tracking-label); + font-weight: 500; + text-transform: uppercase; + color: var(--c-text-meta); + display: inline-block; + } + + // ---- Hero: LEFT-ALIGNED, paper-feel, asymmetric -------------------- + // Was centered + 3-card grid. New: flush-left hero, display-sized H1, + // sub-580 subtitle column, then the hero cards reflow as a results + // strip below the hero block instead of a card grid. + .compare-hero { + padding: 96px 64px 64px; + + &__inner { + max-width: 1200px; + margin: 0 auto; + } + + &__intro { + max-width: 880px; + margin: 0 0 64px; + text-align: left; + } + + &__eyebrow { + @extend %c-bracket-label; + color: var(--c-accent); + margin-bottom: 24px; + } + + &__title { + font-family: var(--c-font-display); + // Push the display bigger and bolder than the soft migration step. + font-size: clamp(40px, 8vw, 88px); + line-height: 1; + letter-spacing: -0.025em; + font-weight: 700; + margin-bottom: 32px; + text-align: left; + } + + &__subtitle { + max-width: 640px; + margin: 0; + font-size: 19px; + line-height: 1.5; + color: var(--c-text-meta); + text-align: left; + + strong { + color: var(--c-text); + font-weight: 600; + } + } + + @include mediaMax($mobile) { + padding: 56px 24px 32px; + &__intro { + margin-bottom: 40px; + } + } + } + + // ---- Hero cards: REFLOWED as a results strip ---------------------- + // Was a 3-column card grid. Now: flush-left, no card chrome, three + // metric blocks separated by hairline rules. Big mono tabular numbers. + .compare-hero-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0; + border-top: 1px solid var(--c-border); + border-bottom: 1px solid var(--c-border); + + @include mediaMax($mobile) { + grid-template-columns: 1fr; + } + } + + .compare-hero-card { + padding: 32px 28px; + background: transparent; + border: none; + border-radius: 0; + border-right: 1px solid var(--c-border); + + &:last-child { + border-right: none; + } + + @include mediaMax($mobile) { + border-right: none; + border-bottom: 1px solid var(--c-border); + &:last-child { + border-bottom: none; + } + } + + &__label { + @extend %c-bracket-label; + margin-bottom: 20px; + display: block; + + .t-mute, + .t-tiny { + color: var(--c-text-quiet); + font-size: 11px; + letter-spacing: 0.05em; + } + } + + &__rows { + margin: 0; + padding: 0; + } + + &__row { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 6px 0; + border-top: 1px solid var(--c-border-soft); + + &:first-child { + border-top: none; + } + + dt { + font-size: 14px; + font-weight: 400; + color: var(--c-text-meta); + } + + dd { + margin: 0; + // Treat the metric as data display: mono tabular, bigger. + font-family: var(--c-font-mono); + font-size: 18px; + font-weight: 500; + font-variant-numeric: tabular-nums; + letter-spacing: -0.01em; + color: var(--c-text); + + &.is-worst { + color: var(--c-worst); + } + &.is-best { + color: var(--c-best); + font-weight: 600; + } + &.is-na { + color: var(--c-text-faint); + font-weight: 400; + } + } + } + + &__footnote { + margin-top: 20px; + padding-top: 14px; + border-top: 1px solid var(--c-border-soft); + font-family: var(--c-font-body); + font-size: 12px; + line-height: 1.5; + color: var(--c-text-quiet); + } + + &__deeplink { + display: inline-block; + margin-top: 12px; + font-family: var(--c-font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--c-text-meta); + text-decoration: none; + border-bottom: 1px solid var(--c-border); + padding-bottom: 1px; + + &::after { + content: ' \2192'; + } + + &:hover { + color: var(--c-accent); + border-bottom-color: var(--c-accent); + } + } + } + + // ---- TL;DR: editorial ABSTRACT block with double-rule ------------- + // Was a centered card with border. Now: full-width band with hairline + // rule top and bottom, label-style header in mono, prose flush. + .compare-tldr { + max-width: none; + margin: 80px 0 0; + padding: 56px 64px; + background: transparent; + border: none; + border-top: 1px solid var(--c-border); + border-bottom: 1px solid var(--c-border); + border-radius: 0; + + &__title { + font-family: var(--c-font-mono); + font-size: var(--c-fs-label); + line-height: 1.4; + letter-spacing: var(--c-tracking-label); + font-weight: 500; + text-transform: uppercase; + color: var(--c-accent); + margin: 0 0 24px; + display: inline-block; + + &::before { + content: '[ '; + color: var(--c-text-faint); + font-weight: 400; + } + &::after { + content: ' ]'; + color: var(--c-text-faint); + font-weight: 400; + } + + .heading-anchor { + display: none; + } + } + + &__nav { + margin-top: 32px; + padding-top: 24px; + border-top: 1px solid var(--c-border); + + a { + font-family: var(--c-font-mono); + font-size: 12px; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--c-text-meta); + text-decoration: none; + border-bottom: 1px solid var(--c-border); + padding-bottom: 1px; + transition: color 200ms, border-color 200ms; + + &:hover { + color: var(--c-accent); + border-bottom-color: var(--c-accent); + } + } + } + + &__nav-label { + font-family: var(--c-font-mono); + font-size: var(--c-fs-label); + letter-spacing: var(--c-tracking-label); + color: var(--c-text-meta); + + &::before { + content: '[ '; + color: var(--c-text-faint); + font-weight: 400; + } + &::after { + content: ' ]'; + color: var(--c-text-faint); + font-weight: 400; + } + } + + @include mediaMax($mobile) { + padding: 40px 24px; + } + } + + // The TL;DR paragraphs (prose inside the abstract) get tightened too. + .compare-tldr .compare-prose { + max-width: 720px; + font-size: 18px; + line-height: 1.6; + margin-bottom: 24px; + } + + // ---- Auto-numbering removed ------------------------------------- + // Section badges ([ NN ]) and figure labels ([ FIG. NN ]) were + // counters — position info, not idea info. Eyebrows above each H2 + // already say what the section is about. Figures that deserve a + // label get a content-specific one via data-fig-label="..." + // (see .compare-frame[data-fig-label]::before below). + + // ---- Rubric: hairline dividers + display H2 + numbered prefix ----- + .compare-rubric { + border-bottom: 1px solid var(--c-border); + + &__section { + padding: 24px 0; + + // Reset the sub-counter at every new section. + counter-reset: subrubric; + } + + &__content { + padding: 80px 64px; + background: var(--c-paper); + border-right: 1px solid var(--c-border); + + @include mediaMax($tablet) { + background: transparent; + border-right: none; + padding: 0; + } + } + + &__media { + padding: 80px 64px; + } + + &__eyebrow { + @extend %c-bracket-label; + color: var(--c-accent); + margin-bottom: 20px; + } + + &__title { + font-family: var(--c-font-display); + font-size: clamp(36px, 5vw, 60px); + line-height: 1.05; + letter-spacing: -0.02em; + font-weight: 700; + margin: 0 0 32px; + position: relative; + + // No auto-numbered [ NN ] badge — the eyebrow above carries the + // idea (e.g. [ BENCHMARK ], [ CLUSTER ], [ COUNTERPOINT ]) and a + // bare digit didn't add information. + + .heading-anchor { + font-size: 0.5em; + font-weight: 400; + color: var(--c-text-faint); + } + } + + &__subtitle { + // Dropped the §N.M academic numbering — it read as pretentious. + // The H3 itself is enough; major rubrics keep their [ NN ] badge. + font-family: var(--c-font-display); + font-size: clamp(20px, 2.4vw, 26px); + line-height: 1.25; + letter-spacing: -0.01em; + font-weight: 600; + margin: 48px 0 16px; + + .heading-anchor { + font-size: 0.7em; + font-weight: 400; + color: var(--c-text-faint); + } + } + + &__content { + .compare-prose, + p:not([class]) { + max-width: var(--c-prose-max); + } + + .compare-bullet-list { + max-width: var(--c-prose-max); + } + + // Full-width sections: anchor LEFT, don't center. Earlier the + // auto-margins made short URLs look "lost in the middle." Now + // every block in --full sits flush-left with a comfortable max. + &--full { + text-align: left; + + > .compare-prose, + > .compare-bullet-list, + > p:not([class]), + > ol, + > .compare-callout { + max-width: var(--c-prose-max-full); + margin-left: 0; + margin-right: 0; + } + + // Frames + tables get a wider cap but stay left-anchored. + > .compare-frame, + > .compare-bench-chart, + > .compare-arch-diagram, + > .compare-code-tabs, + > .compare-code-block, + > figure { + max-width: 880px; + margin-left: 0; + margin-right: 0; + } + } + } + } + + // ---- Bullet list: red dot, mono key for the bolded term --------- + .compare-bullet-list { + margin: 0 0 var(--c-space-block); + max-width: var(--c-prose-max); + + li::before { + content: '·'; + color: var(--c-accent); + } + + strong { + font-family: var(--c-font-body); + font-weight: 700; + color: var(--c-text); + } + } + + // ---- Prose: 580px cap in any context, calm inline code ------------ + .compare-prose { + font-size: 19px; + line-height: 1.6; + margin: 0 0 28px; + + strong { + font-weight: 600; + color: var(--c-text); + } + + a, + a.link { + color: var(--c-accent); + text-decoration: none; + border-bottom: 1px solid transparent; + transition: border-color 200ms; + + &:hover { + border-bottom-color: var(--c-accent); + } + } + } + + // ---- Inline alongside Stem prose ------------------------- + // Martian Mono renders ~15% larger on the body than Stem at the + // same em (higher x-height for code legibility). We compensate by + // scaling inline mono down to 0.8em and nudging it +1px so it + // optically sits on the same baseline as the surrounding Stem. + // Applied globally inside the spine — wherever Stem meets mono. + //
 code blocks are excluded; their sizing lives in .compare-code-block.
+  :not(pre) > code,
+  .compare-prose code,
+  .compare-meta code,
+  .compare-callout code,
+  .compare-tldr code,
+  p code,
+  li code,
+  td code,
+  summary code {
+    font-family: var(--c-font-mono);
+    font-size: 0.8em;
+    font-weight: 500;
+    line-height: 1;
+    background: rgba(0, 0, 0, 0.045);
+    padding: 2px 6px;
+    border-radius: 4px;
+    vertical-align: 1px;
+    color: var(--c-text);
+    white-space: nowrap;
+  }
+
+  // ---- Code blocks: LIGHT, framed, with mono "figure" feel ---------
+  .compare-code-block,
+  .compare-code-tabs__panel pre,
+  .compare-rubric__media pre,
+  .compare-bench-chart__pre {
+    background: var(--c-code-bg);
+    color: var(--c-text);
+    border: 1px solid var(--c-code-border);
+    border-radius: var(--c-radius);
+    box-shadow: none;
+    padding: 28px 32px;
+    font-size: 13.5px;
+    line-height: 1.7;
+  }
+
+  // Muted syntax tokens (works for the spine class definitions AND the
+  // .c-* utilities used inside compare-page code blocks).
+  .c-key {
+    color: var(--c-code-key);
+  }
+  .c-str {
+    color: var(--c-code-str);
+  }
+  .c-com {
+    color: var(--c-code-com);
+    font-style: italic;
+  }
+  .c-err {
+    color: var(--c-code-err);
+  }
+
+  // .t-mute used as code comment — keep it readable, italicize.
+  pre .t-mute,
+  .compare-code-block .t-mute,
+  .compare-code-tabs__panel .t-mute {
+    color: var(--c-code-com);
+    font-style: italic;
+  }
+
+  // ---- Code tabs: framed unit with mono labels ---------------------
+  .compare-code-tabs {
+    border: 1px solid var(--c-code-border);
+    border-radius: var(--c-radius);
+    overflow: hidden;
+    background: var(--c-code-bg);
+
+    &__bar {
+      display: flex;
+      flex-wrap: wrap;
+      background: var(--c-paper);
+      border-bottom: 1px solid var(--c-code-border);
+      padding: 0;
+    }
+
+    &__tab {
+      flex: 0 0 auto;
+      padding: 12px 18px;
+      font-family: var(--c-font-mono);
+      font-size: 12px;
+      letter-spacing: 0.04em;
+      text-transform: uppercase;
+      color: var(--c-text-meta);
+      cursor: pointer;
+      border-right: 1px solid var(--c-border-soft);
+      transition: color 200ms, background 200ms;
+
+      &:hover {
+        color: var(--c-text);
+        background: var(--c-paper);
+      }
+    }
+
+    &__panel pre {
+      border: none;
+      border-radius: 0;
+      margin: 0;
+    }
+
+    // Active tab: stripped underline accent in red.
+    @for $i from 1 through 5 {
+      .compare-code-tabs__radio--#{$i}:checked
+        ~ .compare-code-tabs__bar
+        .compare-code-tabs__tab--#{$i} {
+        color: var(--c-accent);
+        background: var(--c-code-bg);
+        box-shadow: inset 0 -2px 0 0 var(--c-accent);
+      }
+    }
+  }
+
+  // ---- Callout: minimal, label-led ---------------------------------
+  .compare-callout {
+    background: transparent;
+    border: none;
+    border-left: 2px solid var(--c-accent);
+    padding: 8px 0 8px 20px;
+    margin: 0 0 32px;
+    font-size: 15px;
+    line-height: 1.55;
+    color: var(--c-text-meta);
+    max-width: var(--c-prose-max);
+
+    strong {
+      color: var(--c-text);
+      font-weight: 600;
+    }
+  }
+
+  // ---- Utility eyebrows: bracketed mono labels ---------------------
+  .t-eyebrow {
+    font-family: var(--c-font-mono);
+    font-size: var(--c-fs-label);
+    line-height: 1.4;
+    letter-spacing: var(--c-tracking-label);
+    font-weight: 500;
+    text-transform: uppercase;
+    color: var(--c-text-meta);
+
+    &::before {
+      content: '[ ';
+      color: var(--c-text-faint);
+      font-weight: 400;
+    }
+    &::after {
+      content: ' ]';
+      color: var(--c-text-faint);
+      font-weight: 400;
+    }
+  }
+
+  // ---- Frame, chart, diagram: shared container conventions ---------
+  .compare-frame,
+  .compare-bench-chart,
+  .compare-arch-diagram {
+    max-width: 880px;
+    margin: 0 0 16px;
+    position: relative;
+  }
+
+  // ---- Architecture diagram: shared system for SVG diagrams --------
+  // Goals: one typography (Stem + Martian Mono), one color palette
+  // (accent red for the "fragile" path, dark for the "stable" path,
+  // muted grays for chrome), one frame treatment.
+  //
+  // SVG text elements use font-family directly in markup, so the spine
+  // can't override font-family on  via CSS. Authors must set
+  // font-family on each  in the SVG. Use:
+  //   font-family="Stem, Arial, sans-serif"             — for box titles + body
+  //   font-family="'Martian Mono', ui-monospace"        — for labels + annotations
+  //
+  // Color tokens used inside the SVG (kept inline for SVG portability):
+  //   fragile/embedded stroke + label:  #f64343
+  //   stable/separated stroke + label:  #1a1a1a
+  //   inner box stroke:                 #ececec
+  //   description text:                 #555
+  //   helper / arrow text:              #777
+  //   light fragile fill (optional):    #fff5f5
+  .compare-arch-diagram {
+    // Match the code block container: light #fafafa, hairline border,
+    // 8px radius, matching padding. Every right-side artifact (code,
+    // diagram, table, chart) shares this visual frame.
+    padding: 28px 32px;
+    background: var(--c-code-bg);
+    border: 1px solid var(--c-code-border);
+    border-radius: var(--c-radius);
+
+    > svg {
+      display: block;
+      max-width: 100%;
+      height: auto;
+    }
+
+    figcaption {
+      display: block;
+      margin-top: 16px;
+      padding-top: 12px;
+      border-top: 1px solid var(--c-code-border);
+      font-family: var(--c-font-mono);
+      font-size: 12px;
+      line-height: 1.5;
+      letter-spacing: 0.02em;
+      color: var(--c-text-quiet);
+    }
+  }
+
+  // ---- Frame: shared container for data tables, same treatment as code
+  .compare-frame {
+    background: var(--c-code-bg);
+    border: 1px solid var(--c-code-border);
+    border-radius: var(--c-radius);
+    overflow: hidden;
+    padding: 0;
+  }
+
+  // Data-table footnotes that follow a .compare-frame are part of the
+  // same artifact — pull them inside visually with mono treatment.
+  .compare-data-table__footnote {
+    font-family: var(--c-font-mono);
+    font-size: 12px;
+    letter-spacing: 0.02em;
+    color: var(--c-text-quiet);
+    margin-top: 12px;
+    max-width: 880px;
+  }
+
+  // ---- Kill stray inline text-align: center on headings/content ----
+  // FAQ and try-it sections had `style="text-align: center"` baked in.
+  // The lab-notebook is left-anchored; override any inline center.
+  .compare-rubric__title[style*='center'],
+  .compare-rubric__content[style*='center'],
+  .compare-rubric__content--full {
+    text-align: left !important;
+  }
+
+  // ---- Visible markers on inline 
    prose lists ------------------ + // Global reset killed list markers; add them back as mono numbers + // in accent red so the 3-question / migration / checklist items + // have proper hierarchy. + ol.compare-prose, + .compare-rubric__content > ol { + list-style: none; + padding-left: 0; + counter-reset: prose-item; + + > li { + counter-increment: prose-item; + position: relative; + padding-left: 36px; + margin-bottom: 12px; + + &::before { + content: counter(prose-item, decimal-leading-zero); + position: absolute; + left: 0; + top: 0.1em; + font-family: var(--c-font-mono); + font-size: 0.8em; + font-weight: 500; + letter-spacing: 0.04em; + color: var(--c-accent); + } + } + } + + // ---- Editorial moments: pull quote + big inline number ----------- + // .c-pullquote: turn a paragraph into a dramatic accented block. + // .c-bignum: wrap a number in prose to render mono tabular at display + // size, inline — for "AnyCable Pro holds [bignum]999,954[/bignum]" etc. + .c-pullquote { + margin: 32px 0; + padding: 4px 0 4px 24px; + border-left: 4px solid var(--c-accent); + font-family: var(--c-font-display); + font-size: clamp(22px, 2.4vw, 28px); + line-height: 1.3; + font-weight: 600; + letter-spacing: -0.01em; + color: var(--c-text); + max-width: 720px; + } + + .c-bignum { + display: inline-block; + font-family: var(--c-font-mono); + font-variant-numeric: tabular-nums; + font-weight: 600; + color: var(--c-text); + letter-spacing: -0.02em; + line-height: 0.9; + vertical-align: -0.05em; + + // Default size: a touch larger than surrounding prose. + font-size: 1.2em; + + &--xl { + font-size: 1.6em; + } + &--xxl { + font-size: 2.2em; + vertical-align: -0.15em; + } + + &--accent { + color: var(--c-accent); + } + &--best { + color: var(--c-best); + } + } + + // ---- Data tables: sit inside .compare-frame, no own chrome ------- + // The frame provides the bg + border + radius. Table itself is + // transparent so the unified light-#fafafa surface reads as a single + // artifact, like the code block. + .compare-data-table { + background: transparent; + border: none; + border-radius: 0; + font-size: 15px; + + th { + font-family: var(--c-font-mono); + font-size: var(--c-fs-label); + letter-spacing: var(--c-tracking-label); + color: var(--c-text-meta); + font-weight: 500; + background: transparent; + border-bottom: 1px solid var(--c-code-border); + } + + td { + font-variant-numeric: tabular-nums; + border-bottom: 1px solid var(--c-code-border); + } + + tr:last-child td { + border-bottom: none; + } + + tr.is-row-best { + background: rgba(22, 163, 74, 0.06); + } + tr.is-row-worst { + background: rgba(185, 28, 28, 0.06); + } + + td.is-best, + td.is-best strong { + color: var(--c-best); + font-weight: 600; + } + + td.is-worst, + td.is-worst strong { + color: var(--c-worst); + } + + td.is-na, + td.is-na strong { + color: var(--c-text-faint); + font-weight: 400; + } + } + + .compare-data-table__footnote { + font-family: var(--c-font-mono); + font-size: 11.5px; + letter-spacing: 0.02em; + color: var(--c-text-quiet); + margin-top: 12px; + } + + // ---- Impact cards: minimal chrome -------------------------------- + .compare-impact-card { + background: var(--c-paper); + border: 1px solid var(--c-border); + border-radius: var(--c-radius); + padding: 20px 22px; + transition: border-color 200ms, transform 200ms; + + &:hover { + border-color: var(--c-accent); + transform: translateY(-1px); + } + + &__title { + font-family: var(--c-font-display); + font-size: 17px; + font-weight: 700; + color: var(--c-text); + line-height: 1.3; + margin-bottom: 4px; + } + + &__desc { + font-size: 13.5px; + line-height: 1.45; + color: var(--c-text-meta); + } + } + + // ---- Quote card: editorial framing ------------------------------- + .compare-quote-card { + background: var(--c-paper); + border: 1px solid var(--c-border); + border-radius: var(--c-radius); + + &__eyebrow { + font-family: var(--c-font-mono); + font-size: var(--c-fs-label); + letter-spacing: var(--c-tracking-label); + text-transform: uppercase; + color: var(--c-text-meta); + + &::before { + content: '[ '; + color: var(--c-text-faint); + font-weight: 400; + } + &::after { + content: ' ]'; + color: var(--c-text-faint); + font-weight: 400; + } + } + } + + // ---- Try card: light surface, mono label ------------------------- + .compare-try-card { + background: var(--c-paper); + border: 1px solid var(--c-border); + border-radius: var(--c-radius); + + &:hover { + border-color: var(--c-accent); + } + + .t-eyebrow { + font-family: var(--c-font-mono); + color: var(--c-text-meta); + } + } + + // ---- Bench chart: same framed container as code / diagram -------- + .compare-bench-chart { + padding: 28px 32px; + background: var(--c-code-bg); + border: 1px solid var(--c-code-border); + border-radius: var(--c-radius); + + // The ASCII chart
     inside the figure sits naked on the
    +    // surface — the figure itself IS the visual frame.
    +    &__pre {
    +      border: none;
    +      background: transparent;
    +      padding: 0;
    +    }
    +
    +    figcaption {
    +      font-family: var(--c-font-mono);
    +      font-size: 12px;
    +      letter-spacing: 0.02em;
    +      color: var(--c-text-quiet);
    +      margin-top: 16px;
    +      padding-top: 12px;
    +      border-top: 1px solid var(--c-code-border);
    +    }
    +  }
    +
    +  // ---- CTA: lab-notebook footer rather than marketing hero ---------
    +  .compare-cta {
    +    padding: 128px 64px 96px;
    +    border-top: 1px solid var(--c-border);
    +    text-align: center;
    +
    +    &__title {
    +      font-family: var(--c-font-display);
    +      font-size: clamp(32px, 5vw, 56px);
    +      line-height: 1.05;
    +      letter-spacing: -0.02em;
    +      font-weight: 700;
    +      margin-bottom: 32px;
    +    }
    +
    +    &__footnote {
    +      font-family: var(--c-font-mono);
    +      font-size: 12px;
    +      letter-spacing: 0.04em;
    +      text-transform: uppercase;
    +      color: var(--c-text-quiet);
    +    }
    +  }
    +
    +  // ---- FAQ: hairline-only accordion, anchored left -----------------
    +  // compare.scss has bordered cards centered in a 720px column with
    +  // margin auto. Lab-notebook strips the chrome: hairline between
    +  // items, full-width left-anchored stack.
    +  .compare-faq {
    +    margin: 0;
    +    max-width: 720px;
    +    gap: 0;
    +    border-top: 1px solid var(--c-border);
    +  }
    +
    +  .compare-faq__item {
    +    padding: 0;
    +    background: transparent;
    +    border: none;
    +    border-bottom: 1px solid var(--c-border);
    +    border-radius: 0;
    +
    +    &:hover,
    +    &[open] {
    +      border-color: var(--c-border);
    +    }
    +
    +    summary {
    +      font-family: var(--c-font-display);
    +      font-size: 18px;
    +      font-weight: 600;
    +      padding: 20px 0;
    +      color: var(--c-text);
    +    }
    +  }
    +
    +  .compare-faq__answer {
    +    padding-top: 0;
    +    padding-bottom: 20px;
    +    font-size: 16px;
    +    line-height: 1.65;
    +    color: var(--c-text-meta);
    +  }
    +
    +  // ---- Heading anchor (#) hidden by default, shown on hover --------
    +  .compare-rubric__title:not(:hover) .heading-anchor,
    +  .compare-rubric__subtitle:not(:hover) .heading-anchor,
    +  .compare-tldr__title:not(:hover) .heading-anchor {
    +    opacity: 0;
    +  }
    +}
    diff --git a/src/modules/blocks/compare.scss b/src/modules/blocks/compare.scss
    index 0cc382e..67bae71 100644
    --- a/src/modules/blocks/compare.scss
    +++ b/src/modules/blocks/compare.scss
    @@ -25,37 +25,35 @@
     //      If a value repeats, promote it to a token or class.
     // =====================================================================
     
    -
     // ---------- Local tokens ----------------------------------------------
     // One-off grays we use repeatedly. Globals cover accents and primary
     // surfaces; locals cover the muted divider/text scale we need for a
     // data-heavy page.
    -$compare-border:         #ececec;
    -$compare-border-soft:    #f0f0f0;
    -$compare-border-faint:   #f3f3f3;
    -$compare-divider:        #e8e8e8;
    -$compare-text-strong:    #1a1a1a;
    -$compare-text-body:      #333;
    -$compare-text-meta:      #555;
    -$compare-text-quiet:     #777;
    -$compare-text-mute:      #888;
    -$compare-text-fade:      #bbb;
    -$compare-text-faint:     #ccc;
    -$compare-bg-soft:        #fafafa;
    -$compare-bg-code:        #1a1a1a;
    -$compare-radius:         8px;
    -$compare-header-offset:  80px; // header height + a touch of breathing
    +$compare-border: #ececec;
    +$compare-border-soft: #f0f0f0;
    +$compare-border-faint: #f3f3f3;
    +$compare-divider: #e8e8e8;
    +$compare-text-strong: #1a1a1a;
    +$compare-text-body: #333;
    +$compare-text-meta: #555;
    +$compare-text-quiet: #777;
    +$compare-text-mute: #888;
    +$compare-text-fade: #bbb;
    +$compare-text-faint: #ccc;
    +$compare-bg-soft: #fafafa;
    +$compare-bg-code: #1a1a1a;
    +$compare-radius: 8px;
    +$compare-header-offset: 80px; // header height + a touch of breathing
     
     // Inline syntax-tint palette for the dark 
     blocks (try-it cards
     // and the bench-chart figure). Kept narrow on purpose; if we ever need
     // real syntax highlighting, swap in a Prism/Highlight.js theme instead
     // of growing this list.
    -$compare-code-keyword:   #fcd34d;  // amber
    -$compare-code-string:    #86efac;  // green
    -$compare-code-error:     #ff6b6b;  // red
    -$compare-code-comment:   #cce6ff;  // pale blue
    -$compare-best:           #16a34a;  // green winner accent
    -
    +$compare-code-keyword: #fcd34d; // amber
    +$compare-code-string: #86efac; // green
    +$compare-code-error: #ff6b6b; // red
    +$compare-code-comment: #cce6ff; // pale blue
    +$compare-best: #16a34a; // green winner accent
     
     .compare-page {
       // Notebook-feel dotted background — continuous from hero through CTA.
    @@ -81,7 +79,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Utility classes — small, composable, reach for these before inline.
       // Naming: t-* for typography/color, c-* for code-block tints.
    @@ -94,22 +91,47 @@ $compare-best:           #16a34a;  // green winner accent
         color: $compare-text-mute;
       }
     
    -  .t-mute   { color: $compare-text-mute; }     // #888
    -  .t-meta   { color: $compare-text-meta; }     // #555
    -  .t-quiet  { color: $compare-text-quiet; }    // #777
    -  .t-strong { color: $compare-text-strong; }   // #1a1a1a
    -  .t-accent { color: $accentPrimaryColor; }
    -  .t-best   { color: $compare-best; }
    -  .t-num    { font-variant-numeric: tabular-nums; }
    -
    -  .c-key { color: $compare-code-keyword; }
    -  .c-str { color: $compare-code-string; }
    -  .c-err { color: $compare-code-error; }
    -  .c-com { color: $compare-code-comment; }
    +  .t-mute {
    +    color: $compare-text-mute;
    +  } // #888
    +  .t-meta {
    +    color: $compare-text-meta;
    +  } // #555
    +  .t-quiet {
    +    color: $compare-text-quiet;
    +  } // #777
    +  .t-strong {
    +    color: $compare-text-strong;
    +  } // #1a1a1a
    +  .t-accent {
    +    color: $accentPrimaryColor;
    +  }
    +  .t-best {
    +    color: $compare-best;
    +  }
    +  .t-num {
    +    font-variant-numeric: tabular-nums;
    +  }
     
    -  .t-tiny  { font-size: 11px; }
    -  .t-bold  { font-weight: 600; }
    +  .c-key {
    +    color: $compare-code-keyword;
    +  }
    +  .c-str {
    +    color: $compare-code-string;
    +  }
    +  .c-err {
    +    color: $compare-code-error;
    +  }
    +  .c-com {
    +    color: $compare-code-comment;
    +  }
     
    +  .t-tiny {
    +    font-size: 11px;
    +  }
    +  .t-bold {
    +    font-weight: 600;
    +  }
     
       // -------------------------------------------------------------------
       // compare-rubric: the page's main section wrapper. Replaces the
    @@ -174,7 +196,9 @@ $compare-best:           #16a34a;  // green winner accent
             border-right: none;
             padding: 0;
     
    -        &:first-child { padding-top: 48px; }
    +        &:first-child {
    +          padding-top: 48px;
    +        }
           }
     
           // For sections without a media partner (FAQ, code samples).
    @@ -278,8 +302,12 @@ $compare-best:           #16a34a;  // green winner accent
             transition: opacity 0.15s;
           }
     
    -      &:hover .heading-anchor { opacity: 1; }
    -      .heading-anchor:hover   { color: $accentPrimaryColor; }
    +      &:hover .heading-anchor {
    +        opacity: 1;
    +      }
    +      .heading-anchor:hover {
    +        color: $accentPrimaryColor;
    +      }
         }
     
         &__subtitle {
    @@ -299,8 +327,12 @@ $compare-best:           #16a34a;  // green winner accent
             transition: opacity 0.15s;
           }
     
    -      &:hover .heading-anchor { opacity: 1; }
    -      .heading-anchor:hover   { color: $accentPrimaryColor; }
    +      &:hover .heading-anchor {
    +        opacity: 1;
    +      }
    +      .heading-anchor:hover {
    +        color: $accentPrimaryColor;
    +      }
         }
     
         // Eyebrow above a section title (e.g. "Impact" above Rubric 1's
    @@ -318,7 +350,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // compare-frame: rounded card wrapper around tables, code blocks, and
       // figures in the media column. Replaces .slide-show__frame. On mobile
    @@ -340,7 +371,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // compare-impact-cards: 2-up grid of affected-workload cards in the
       // "Impact" section. Replaces .cases-slide__companies / __company-card.
    @@ -390,7 +420,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Compare-page prose paragraph. Replaces .about-slide__text on this
       // page because the comparison page is not an "about slide" — the
    @@ -424,7 +453,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Callout — soft-tinted methodology note. Used inline within prose
       // to flag a caveat or framing for the data that follows. BEM block
    @@ -441,11 +469,18 @@ $compare-best:           #16a34a;  // green winner accent
         line-height: 1.55;
         color: $compare-text-meta;
     
    -    strong { color: $compare-text-strong; font-weight: 600; }
    -    code { background: rgba(0, 0, 0, 0.06); padding: 1px 5px; border-radius: 3px; font-size: 0.9em; }
    +    strong {
    +      color: $compare-text-strong;
    +      font-weight: 600;
    +    }
    +    code {
    +      background: rgba(0, 0, 0, 0.06);
    +      padding: 1px 5px;
    +      border-radius: 3px;
    +      font-size: 0.9em;
    +    }
       }
     
    -
       // -------------------------------------------------------------------
       // Caveats / red-dot bullet list. Used wherever we want to call out
       // a small set of structural points (Socket.io CSR caveats, etc.).
    @@ -472,7 +507,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Inline code block — dark slab with monospace text and tinted spans
       // (.c-key, .c-str, .c-err, .c-com). Used for the delivery sample
    @@ -486,7 +520,7 @@ $compare-best:           #16a34a;  // green winner accent
         background: $compare-bg-code;
         color: #e0e0e0;
         border-radius: $compare-radius;
    -    font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
    +    font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
         font-size: 13px;
         line-height: 1.7;
         white-space: pre;
    @@ -534,7 +568,9 @@ $compare-best:           #16a34a;  // green winner accent
           user-select: none;
           white-space: nowrap;
     
    -      &:hover { color: #d0d0d0; }
    +      &:hover {
    +        color: #d0d0d0;
    +      }
         }
     
         &__panel {
    @@ -545,7 +581,7 @@ $compare-best:           #16a34a;  // green winner accent
             padding: 28px 32px;
             background: transparent;
             color: #e0e0e0;
    -        font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
    +        font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
             font-size: 13px;
             line-height: 1.7;
             white-space: pre;
    @@ -558,16 +594,18 @@ $compare-best:           #16a34a;  // green winner accent
       // `~` sibling combinator doesn't get a parent selector injected
       // between both sides of the combinator, which would break the match.
       @for $i from 1 through 5 {
    -    .compare-code-tabs__radio--#{$i}:checked ~ .compare-code-tabs__bar .compare-code-tabs__tab--#{$i} {
    +    .compare-code-tabs__radio--#{$i}:checked
    +      ~ .compare-code-tabs__bar
    +      .compare-code-tabs__tab--#{$i} {
           color: #fff;
           border-bottom-color: $accentPrimaryColor;
         }
    -    .compare-code-tabs__radio--#{$i}:checked ~ .compare-code-tabs__panel--#{$i} {
    +    .compare-code-tabs__radio--#{$i}:checked
    +      ~ .compare-code-tabs__panel--#{$i} {
           display: block;
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Hero block — the comparison eyebrow + H1 + subhead at the top of
       // the page. Sits on the dotted page bg directly (no panel).
    @@ -603,7 +641,10 @@ $compare-best:           #16a34a;  // green winner accent
           letter-spacing: -0.02em;
     
           // The dimmed connector words ("vs", "&") between framework names.
    -      .is-mute { color: $compare-text-fade; font-weight: 400; }
    +      .is-mute {
    +        color: $compare-text-fade;
    +        font-weight: 400;
    +      }
         }
     
         &__subtitle {
    @@ -613,7 +654,9 @@ $compare-best:           #16a34a;  // green winner accent
           line-height: 1.5;
           color: $compare-text-meta;
     
    -      strong { color: $compare-text-strong; }
    +      strong {
    +        color: $compare-text-strong;
    +      }
         }
     
         // "Open source — run it yourself" line beneath the subhead.
    @@ -631,7 +674,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Hero stat cards — three at-a-glance comparison cards. Each card is
       // a small label → 3 rows → footnote table. One "worst" cell per card.
    @@ -671,7 +713,9 @@ $compare-best:           #16a34a;  // green winner accent
           padding: 9px 0;
           border-top: 1px solid $compare-border-faint;
     
    -      &:first-child { border-top: none; }
    +      &:first-child {
    +        border-top: none;
    +      }
     
           dt {
             font-size: 14px;
    @@ -686,9 +730,16 @@ $compare-best:           #16a34a;  // green winner accent
             font-variant-numeric: tabular-nums;
             letter-spacing: -0.01em;
     
    -        &.is-worst { color: $accentPrimaryColor; }
    -        &.is-best  { color: $compare-best; }
    -        &.is-na    { color: $compare-text-fade; font-weight: 400; }
    +        &.is-worst {
    +          color: $accentPrimaryColor;
    +        }
    +        &.is-best {
    +          color: $compare-best;
    +        }
    +        &.is-na {
    +          color: $compare-text-fade;
    +          font-weight: 400;
    +        }
           }
         }
     
    @@ -724,7 +775,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // TL;DR / "three findings" block — analytical summary directly under
       // the hero stat cards. Doubles as on-page anchor nav. Designed to be
    @@ -754,13 +804,22 @@ $compare-best:           #16a34a;  // green winner accent
           line-height: 1.6;
           color: $compare-text-body;
     
    -      li               { margin-bottom: 8px; }
    -      li:last-child    { margin-bottom: 0; }
    -      strong           { color: #111; }
    +      li {
    +        margin-bottom: 8px;
    +      }
    +      li:last-child {
    +        margin-bottom: 0;
    +      }
    +      strong {
    +        color: #111;
    +      }
     
           // Section-link anchor inside  inherits the strong color
           // — the visual weight comes from the parent, not the link.
    -      strong a         { color: inherit; text-decoration: none; }
    +      strong a {
    +        color: inherit;
    +        text-decoration: none;
    +      }
         }
     
         // Trailing "Full benchmark →" link in each TL;DR bullet; bolder
    @@ -816,7 +875,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // .compare-quote-card: tinted-bg variant of .compare-frame used for
       // customer quotes (Doximity, etc.). Overrides .compare-frame's bare
    @@ -851,7 +909,9 @@ $compare-best:           #16a34a;  // green winner accent
           line-height: 1.5;
           color: $compare-text-meta;
     
    -      strong { color: $compare-text-strong; }
    +      strong {
    +        color: $compare-text-strong;
    +      }
         }
     
         &__footer {
    @@ -863,7 +923,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Architecture diagram (Pillar 2) — inline SVG showing Socket.io's
       // co-located process vs AnyCable's separated app | anycable-go.
    @@ -876,7 +935,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Inline benchmark chart (ASCII art from the bench-runner) — embedded
       // in Pillar 3 to show memory + CPU shape during the 1M run. Lives in
    @@ -906,7 +964,7 @@ $compare-best:           #16a34a;  // green winner accent
           background: $compare-bg-code;
           color: #cce6ff;
           border-radius: $compare-radius;
    -      font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace;
    +      font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
           font-size: 11px;
           line-height: 1.4;
           white-space: pre;
    @@ -914,7 +972,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Try-it cards grid — three runnable JS starting points.
       // -------------------------------------------------------------------
    @@ -933,7 +990,6 @@ $compare-best:           #16a34a;  // green winner accent
         width: 100%;
       }
     
    -
       // -------------------------------------------------------------------
       // Unified data table — one source of truth for jitter, avalanche,
       // capacity, and feature-comparison tables. Right-aligned by default
    @@ -974,7 +1030,9 @@ $compare-best:           #16a34a;  // green winner accent
         tbody tr {
           border-bottom: 1px solid $compare-border-soft;
     
    -      &:last-child { border-bottom: none; }
    +      &:last-child {
    +        border-bottom: none;
    +      }
         }
     
         td {
    @@ -1003,8 +1061,12 @@ $compare-best:           #16a34a;  // green winner accent
     
         // Soft-tinted rows: green for the AnyCable / uWS winners
         // (e.g. 1M-conn capacity), red for the cliff/loser rows.
    -    tr.is-row-best  { background: #f0fdf4; }
    -    tr.is-row-worst { background: #fff5f5; }
    +    tr.is-row-best {
    +      background: #f0fdf4;
    +    }
    +    tr.is-row-worst {
    +      background: #fff5f5;
    +    }
     
         // "Worst per metric" — single accent, used sparingly.
         td.is-worst,
    @@ -1041,7 +1103,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Footnote that sits directly under a data table — quiet gray label
       // explaining methodology / measurement source.
    @@ -1053,7 +1114,6 @@ $compare-best:           #16a34a;  // green winner accent
         color: $compare-text-mute;
       }
     
    -
       // -------------------------------------------------------------------
       // Try-it card — clickable link cards under "Run it yourself" pointing
       // to GitHub demos. Border + padding + title/desc rows.
    @@ -1069,7 +1129,9 @@ $compare-best:           #16a34a;  // green winner accent
         text-decoration: none;
         transition: border-color 0.15s, transform 0.15s;
     
    -    &:hover { border-color: #d6d6d6; }
    +    &:hover {
    +      border-color: #d6d6d6;
    +    }
     
         &__title {
           margin-bottom: 6px;
    @@ -1084,7 +1146,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Compact data-table modifier — used when a 5-column matrix would
       // overflow the right-column frame at default padding (the feature
    @@ -1101,18 +1162,21 @@ $compare-best:           #16a34a;  // green winner accent
           padding: 8px 4px;
           text-align: center;
     
    -      &:first-child { text-align: left; }
    +      &:first-child {
    +        text-align: left;
    +      }
         }
     
         td {
           padding: 6px 4px;
           text-align: center;
     
    -      &:first-child { text-align: left; }
    +      &:first-child {
    +        text-align: left;
    +      }
         }
       }
     
    -
       // -------------------------------------------------------------------
       // Closing CTA — leads with self-hosted Pro (monetized), free Managed
       // is the secondary low-friction path, open source gets a single line
    @@ -1138,7 +1202,9 @@ $compare-best:           #16a34a;  // green winner accent
           line-height: 1.5;
           color: #666;
     
    -      strong { color: #111; }
    +      strong {
    +        color: #111;
    +      }
         }
     
         &__actions {
    @@ -1157,7 +1223,6 @@ $compare-best:           #16a34a;  // green winner accent
         }
       }
     
    -
       // -------------------------------------------------------------------
       // FAQ accordion — native 
    , closed by default. Explicit // width:100% prevents the flex parent from sizing this block to @@ -1194,7 +1259,9 @@ $compare-best: #16a34a; // green winner accent line-height: 1.45; transition: color 0.15s; - &::-webkit-details-marker { display: none; } + &::-webkit-details-marker { + display: none; + } // Plus / minus indicator that flips on open. &::after { @@ -1208,7 +1275,9 @@ $compare-best: #16a34a; // green winner accent transition: transform 0.2s; } - &:hover { color: $accentPrimaryColor; } + &:hover { + color: $accentPrimaryColor; + } } &[open] summary::after { @@ -1226,7 +1295,6 @@ $compare-best: #16a34a; // green winner accent } } - // ------------------------------------------------------------------- // Hub page (/compare) — landscape map + comparison cards. // @@ -1313,7 +1381,6 @@ $compare-best: #16a34a; // green winner accent } } - // ---------- Comparison cards ---------------------------------------- .compare-hub-cards { display: grid; @@ -1440,7 +1507,6 @@ $compare-best: #16a34a; // green winner accent } } - .compare-hub-footnote { margin: 8px 0 0; font-size: 14px; From eedc50dac70510c6d85ea4ce0b9adf54c284295f Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Tue, 2 Jun 2026 23:19:46 +0100 Subject: [PATCH 35/57] Compare/Socket.io: latency 1k+10k for all options, AnyCable standalone deploy-impact - A2a latency at 1k tier: Socket.io 24/106, +CSR 23/69, uWS 16/92, AnyCable OSS 17/55, Pro 22/145 ms (p50/p99) - A2a latency at 10k tier: Socket.io 264/725, +CSR 254/639, AnyCable OSS 252/895, Pro 246/875 ms (p50/p99); uWS 249ms p50 but p99 4.4-10.6 s across 4 samples (single-process backpressure) - A2c standalone deploy-impact (Socket.io + AnyCable): both show 0 affected clients, 0 reconnects, 45-46 s publisher restart window - Hero card: replaced TBD scalability with deploy-impact (embedded Socket.io 100% vs both standalone 0%) - Footnote on uWS 10k high-variance result with explanation 100k latency tier, A2b whispers, A2d 3-node clustered, CSR/uWS embedded deploy-impact remain pending. --- src/compare/socket-io/index.html | 38 ++++++++++++++++---------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 1e82727..b46ed56 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -23,19 +23,19 @@

    -
    Latency: p50 roundtrip @ 10k connections
    +
    Latency: p99 roundtrip @ 10k connections
    Socket.io + CSR
    -
    TBD-A2a
    +
    639 ms
    uWebSockets.js
    -
    TBD-A2a
    +
    4-11 s
    AnyCable
    -
    TBD-A2a
    +
    895 ms
    @@ -65,23 +65,23 @@

    -
    Scalability: downtime during deployment
    +
    Deploy impact: WS connections disrupted per app deploy
    Socket.io embedded
    -
    TBD-A2c
    +
    100% · 2.5 s freeze
    -
    uWS embedded
    -
    TBD-A2c
    +
    Socket.io standalone
    +
    0%
    AnyCable standalone
    -
    TBD-A2c
    +
    0%
    @@ -373,14 +373,14 @@

    How latency scales with concurrent connecti - Socket.io + Redis adapterTBD-A2aTBD-A2aTBD-A2a - Socket.io + CSRTBD-A2aTBD-A2aTBD-A2a - uWS custom publishTBD-A2aTBD-A2aTBD-A2a + Socket.io + Redis adapter24 / 106 ms264 / 725 msTBD-A2a + Socket.io + CSR23 / 69 ms254 / 639 msTBD-A2a + uWS custom publish16 / 92 ms249 ms / 4-11 s p99TBD-A2a AnyCable OSS17 / 55 ms252 / 895 msTBD-A2a AnyCable Pro22 / 145 ms246 / 875 msTBD-A2a -

    Placeholders pending A2a. 100k tier feasibility depends on the bench-runner shard fanout; if 100k is unreliable we'll surface 50k as the upper tier and document why.

    +

    † uWS p50 at 10k stays low (249 ms), but the long tail varies wildly across runs (p99 ranged 4.4-10.6 s across 4 samples). The cause is single-process broadcast: app.publish fans out to 10k subscribers in one event-loop iteration, so per-client TCP backpressure variation balloons the tail. Production uWS deployments would shard, which we haven't tested. 100k tier pending multi-shard bench infrastructure.

@@ -883,16 +883,16 @@

Rolling deploy: per-client disconnect windo - Socket.io embedded + Redis adapterTBD-A2cTBD-A2cTBD-A2cTBD-A2c + Socket.io embedded + Redis adapter2.17 s2.93 s2-3100% Socket.io + CSR embedded + Redis StreamsTBD-A2cTBD-A2cTBD-A2cTBD-A2c uWS embedded + custom pub/subTBD-A2cTBD-A2cTBD-A2cTBD-A2c - Socket.io standaloneTBD-A2cTBD-A2cTBD-A2cTBD-A2c + Socket.io standalone0000% uWS standalone + custom publishTBD-A2cTBD-A2cTBD-A2cTBD-A2c - AnyCable OSS standalone, alwaysTBD-A2cTBD-A2cTBD-A2cTBD-A2c - AnyCable Pro standalone, alwaysTBD-A2cTBD-A2cTBD-A2cTBD-A2c + AnyCable OSS standalone, always0000% + AnyCable Pro standalone, always0000% -

Placeholders pending A2c. Embedded rows show what happens when the WS layer restarts with the app on every node. Standalone rows show what happens when only the app restarts. uWS has no native clustering or built-in HTTP publish API; both are custom code built for this benchmark. The embedded vs standalone gap is the load-bearing data point: it answers whether horizontal scaling can substitute for a separate-process WS architecture.

+

Embedded Socket.io: 10K clients held across a 3-node rolling restart. Every user disconnects once during the cycle (a ~2.5 s freeze) and loses 2-3 messages while reconnecting; cluster recovery 51 s. Standalone Socket.io and AnyCable: same 10K clients, the publisher service restarts mid-test, WS layer untouched, zero connection disruption. CSR embedded and uWS embedded tests pending. uWS has no native clustering or built-in HTTP publish API; both are custom code built for this benchmark. Standalone deploys disrupt publishing, not connections: clients hold the WS link through a 45-46 s publisher restart; broadcasts emitted during that window aren't held server-side, so anything the app would have published is simply not published. AnyCable Pro is mirrored from OSS (same architectural property).

From cf2625a6d9570e41ddc51d56e7ddeb1c529d9f10 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 07:44:14 +0100 Subject: [PATCH 36/57] Compare/Socket.io: rewrite clustered section as 2-node measured, 3-node pending The deploy-impact data we have is from a 2-node Socket.io+Redis cluster. Page previously claimed 3-node throughout; now says 2-node where measured, calls out 3-node as the A2d target. The per-user property (every user disconnects once during a rolling cycle) generalizes to N nodes. Also softened dochead + JSON-LD from '3-node clustered' to 'clustered'. --- src/compare/socket-io/index.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index b46ed56..3f7c4e7 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -1,6 +1,6 @@ - {{> dochead pageTitle="AnyCable vs Socket.io vs uWebSockets.js | Benchmarking WebSocket infrastructure for JS/TS apps" pageDescription="A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions: how fast (roundtrip latency at 1k/10k/100k and broadcast throughput, including whispers), how reliable (message delivery under WiFi-drop jitter), and how efficient to scale (single-node load test plus 3-node clustered deploy-impact and avalanche tests). Tested in both embedded and standalone topologies. All numbers reproducible from the open-source benchmark repo." pageUrl="https://anycable.io/compare/socket-io"}} + {{> dochead pageTitle="AnyCable vs Socket.io vs uWebSockets.js | Benchmarking WebSocket infrastructure for JS/TS apps" pageDescription="A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions: how fast (roundtrip latency at 1k/10k/100k and broadcast throughput, including whispers), how reliable (message delivery under WiFi-drop jitter), and how efficient to scale (single-node load test plus clustered deploy-impact and avalanche tests). Tested in both embedded and standalone topologies. All numbers reproducible from the open-source benchmark repo." pageUrl="https://anycable.io/compare/socket-io"}}
{{> header}} @@ -848,12 +848,12 @@

How efficient is it to scale?
-

What does a 3-node cluster look like?#

+

What does a clustered deployment look like?#

Past one node, the question shifts from "how big can the single instance get?" to "what does adding more nodes actually buy?" Three things to measure: can a rolling deploy survive without dropping connections? (the test that motivates separate-process WS in the first place), how does total capacity scale with N nodes?, and does the cluster pub/sub layer impose a latency tax on broadcasts?

- All clustered tests run with 3 nodes behind a load balancer, 10k connections held steady. We test the same 5 options: Socket.io (Redis adapter), Socket.io + CSR (Redis Streams), uWS, AnyCable OSS, and AnyCable Pro. + Current measurements use a 2-node cluster behind a load balancer (10k connections steady). The 3-node tier remains the production-realistic target; capacity and 1-of-N-down behavior are pending A2d. We test the same 5 options: Socket.io (Redis adapter), Socket.io + CSR (Redis Streams), uWS, AnyCable OSS, and AnyCable Pro.

Caveat: uWS clustering and standalone are not off-the-shelf configurations. uWebSockets.js has no built-in cross-node clustering and no built-in HTTP publish endpoint. To benchmark uWS in standalone mode we wrote a custom publish API; to benchmark it clustered we wrote a custom pub/sub bridge (Redis-backed). The numbers reflect what those custom implementations achieve, marked with in the tables. Production teams who pick uWS for anything past one in-process node end up building the same two things themselves. This is part of the trade Socket.io and AnyCable spare you: both ship the multi-node story as a documented, supported configuration. @@ -867,7 +867,7 @@

What does a 3-node cluster look

Rolling deploy: per-client disconnect window + message loss

- 3-node cluster, 10k connections steady, drain + restart nodes sequentially. The test measures what every connected user actually experiences during a deploy: how long their connection is broken, how many messages they miss in that window, and whether the surviving nodes can absorb the reconnect storm from the node being recycled. + 2-node cluster, 10k connections steady, drain + restart nodes sequentially. The test measures what every connected user actually experiences during a deploy: how long their connection is broken, how many messages they miss in that window, and whether the surviving nodes can absorb the reconnect storm from the node being recycled. 3-node tier pending A2d.

@@ -875,7 +875,7 @@

Rolling deploy: per-client disconnect windo - + @@ -892,7 +892,7 @@

Rolling deploy: per-client disconnect windo

Setup 10k, 3 nodesSetup 10k, 2 nodes Gap p50 Gap p99 Msgs lost
AnyCable Pro standalone, always0000%
-

Embedded Socket.io: 10K clients held across a 3-node rolling restart. Every user disconnects once during the cycle (a ~2.5 s freeze) and loses 2-3 messages while reconnecting; cluster recovery 51 s. Standalone Socket.io and AnyCable: same 10K clients, the publisher service restarts mid-test, WS layer untouched, zero connection disruption. CSR embedded and uWS embedded tests pending. uWS has no native clustering or built-in HTTP publish API; both are custom code built for this benchmark. Standalone deploys disrupt publishing, not connections: clients hold the WS link through a 45-46 s publisher restart; broadcasts emitted during that window aren't held server-side, so anything the app would have published is simply not published. AnyCable Pro is mirrored from OSS (same architectural property).

+

Embedded Socket.io: 10K clients held across a 2-node rolling restart. Every user disconnects once during the cycle (a ~2.5 s freeze) and loses 2-3 messages while reconnecting; cluster recovery 51 s. The 3-node tier is pending A2d, but the per-user property doesn't change with more nodes: each rolling step disconnects everyone on the node being recycled. Standalone Socket.io and AnyCable: same 10K clients, the publisher service restarts mid-test, WS layer untouched, zero connection disruption. CSR embedded and uWS embedded tests pending. uWS has no native clustering or built-in HTTP publish API; both are custom code built for this benchmark. Standalone deploys disrupt publishing, not connections: clients hold the WS link through a 45-46 s publisher restart; broadcasts emitted during that window aren't held server-side, so anything the app would have published is simply not published. AnyCable Pro is mirrored from OSS (same architectural property).

@@ -1418,7 +1418,7 @@

Try AnyCable in your stack

"@context": "https://schema.org", "@type": "TechArticle", "headline": "AnyCable vs Socket.io vs uWebSockets.js: Benchmarking WebSocket infrastructure for JS/TS apps", - "description": "A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions answered with reproducible benchmarks: (1) how fast — roundtrip latency at 1k/10k/100k subscribers, broadcast throughput, plus whispers (client-to-client without backend hop) where AnyCable competes with Liveblocks, Yjs, PartyKit; (2) how reliable — message delivery under WiFi-drop jitter, separating at-most-once protocols (default Socket.io, uWS) from those with replay state (Socket.io+CSR, AnyCable); (3) how efficient to scale — single-node load (RAM per connection, max concurrent) and 3-node clustered rolling-deploy tests that decide whether in-process WS is sustainable. Tested in embedded and standalone topologies. All numbers reproducible from the open-source bench repo at github.com/irinanazarova/anycable-socketio-benchmarks.", + "description": "A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions answered with reproducible benchmarks: (1) how fast — roundtrip latency at 1k/10k/100k subscribers, broadcast throughput, plus whispers (client-to-client without backend hop) where AnyCable competes with Liveblocks, Yjs, PartyKit; (2) how reliable — message delivery under WiFi-drop jitter, separating at-most-once protocols (default Socket.io, uWS) from those with replay state (Socket.io+CSR, AnyCable); (3) how efficient to scale — single-node load (RAM per connection, max concurrent) and clustered rolling-deploy tests that decide whether in-process WS is sustainable. Tested in embedded and standalone topologies. All numbers reproducible from the open-source bench repo at github.com/irinanazarova/anycable-socketio-benchmarks.", "url": "https://anycable.io/compare/socket-io", "mainEntityOfPage": "https://anycable.io/compare/socket-io", "datePublished": "2026-04-30", From 1254b918f6ce2b3567100907268d068c29c29a12 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 08:16:31 +0100 Subject: [PATCH 37/57] Compare/Socket.io: cut everything after 'What you don't have to build' except FAQ + Run-in-your-stack Per editorial direction to keep this page focused as comparison, not full decision tree: Removed: - 'Migrating from Socket.io' section (felt prescriptive vs comparative) - 'When Socket.io is the right choice' + Doximity quote - 'Appendix' (5 mechanism deep-dives, moved scope to bench repo README) - 'When AnyCable is the wrong choice' (4 disqualification cards) - Closing CTA Kept: - 'Run AnyCable in your stack' (3 cards: JS client, demo, serverless), moved to AFTER 'What you don't have to build' - FAQ (SEO) Also cleaned up the TLDR nav (no #migration, no #appendix) and a dangling '#appendix' link in the reliability section. --- src/compare/socket-io/index.html | 246 +++---------------------------- 1 file changed, 24 insertions(+), 222 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 3f7c4e7..387618a 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -99,8 +99,7 @@

02 · How reliable 03 · How efficient 04 · Scale-out - 05 · Migration - Appendix + Run in your stack FAQ

@@ -228,7 +227,7 @@

Note on architecture When building production-grade realtime infrastructure, we have to rule out the embedded (in-process with your Node app) mode for Socket.io/uWS in favor of deploying them as a standalone microservice. The embedded mode produces unacceptable disruption on every deployment of the main Node application: every redeployment of each node causes at least a 2-second freeze and loss of messages while clients are reconnecting to another node.

-

- {{!-- Migration path for Node.js teams --}} -
-
-
-
-

Migrating from Socket.io#

-

- Your Node.js app stays. Swap io.to().emit() for an HTTP POST to AnyCable's broadcast endpoint. Auth, validation, DB access — all unchanged. -

-

- On the client: socket.io-client@anycable/core. The API is similar (connect, subscribe, receive). -

-

- Trade-off: one extra process (anycable-go, a Go binary). Your Node.js app becomes stateless for WebSockets. -

-

Migration checklist — what changes in your code:

-
    -
  1. Replace io.to(room).emit(event, payload) with an HTTP POST to anycable-go's /_broadcast.
  2. -
  3. Swap socket.io-client for @anycable/core on the frontend (or @anycable/web for browser-only). Same connect / subscribe / receive shape.
  4. -
  5. Drop the socket.io server dependency. Your app no longer holds WebSocket connections — anycable-go does.
  6. -
  7. Run anycable-go alongside your app (one Go binary, Docker image, or our Managed tier — same protocol either way).
  8. -
-
-
-
// SERVER — broadcast
-// Before: io.to().emit() in your Node app
-io.to('chat:42').emit('message', payload);
-
-// After: HTTP POST to anycable-go
-await fetch('http://anycable:8080/_broadcast', {
-  method: 'POST',
-  headers: { 'Content-Type': 'application/json' },
-  body: JSON.stringify({
-    stream: 'chat:42',
-    data: JSON.stringify(payload),
-  }),
-});
-
-
-// CLIENT (browser / React / RN) — subscribe
-// Before: socket.io-client
-import { io } from 'socket.io-client';
-const socket = io('/chat');
-socket.emit('subscribe', 'chat:42');
-socket.on('message', render);
-
-// After: @anycable/core (native WebSocket inside)
-import { createCable } from '@anycable/core';
-const cable = createCable('ws://anycable:8080/cable');
-const channel = cable.streamFrom('chat:42');
-channel.on('message', render);
-
-
-
-
- - {{!-- Try-it-in-your-stack: low-friction links to runnable JS code --}} -
- -
- {{!-- Feature comparison --}}
@@ -1073,83 +985,32 @@

What you don't have to build

- {{!-- When Socket.io is right / Proven at scale --}} + {{!-- Run AnyCable in your stack — demos + JS libs + serverless. --}}
-
-
-
-

When Socket.io is the right choice

-

- Small Node.js app. Prototype. Custom protocol you control end-to-end. Socket.io is well-documented, widely used, and free. -

-

- If your users are on mobile or switching networks, you need delivery guarantees. If you deploy more than occasionally, you need connections to survive deploys. AnyCable: both by default. -

-
-
-
-
Proven at scale
- {{!-- Doximity quote — public; from On Rails Podcast. - Trimmed to focus on the deploy-resilience point. --}} -
- "We use AnyCable for our dialer products, which is where real-time is critical — it's our video and voice platform. Anytime you restart your application, which happens when you deploy, you're gonna get connection severances. AnyCable allows them to keep that connection open. That Go service stays up, and you can continue shipping your application as normal." -
- - -
-
-
-
-
- - {{!-- APPENDIX — high-value mechanism explanations cut from the - rubric sections above. Slim by design; the deep-dive - methodology + per-test walkthroughs live in the benchmark - repo README, linked from each rubric. --}} -
-

Appendix#

-

- The high-value mechanism behind each rubric, in one place. Full per-test methodology, raw output, and reproduction steps live in the benchmark repo README. -

- -

Why the message loss is structural, not a bug

-

- Each jitter event creates d seconds of blind window. Over publishing window T with N events, expected loss ≈ (d × N) / T. For our run: 1.3 s × ~8 / 60 ≈ 17% upper bound; we measured 13.1% (lower because not every event fully overlaps publishing). Default Socket.io and naked uWS lose messages in proportion to frequency × disruption — tuning doesn't change the curve, only adding replay does. -

- -

Why a 1-second blip becomes a multi-second recovery tail

-

- Every realtime client — socket.io-client, @anycable/core, anything that wants to stay up under load — reconnects with a 0.5–5 s backoff window to avoid stampeding the server. Any TCP-level blip becomes multiple seconds of offline window by design. Without delivery guarantees, those seconds are lost messages; with replay, they're delayed. The CSR → AnyCable gap on p99 (~8 s vs ~6 s) is the replay protocol itself: CSR drains a buffered packet list per socket through one event loop; AnyCable's history replay is per-stream, batched, parallel. -

- -

CSR's known constraints

-
    -
  • Opt-in. Most production Socket.io apps don't enable it.
  • -
  • Adapter compatibility. Incompatible with the most common scaling adapter (Redis pub/sub). Multi-node CSR needs Redis Streams or MongoDB; Postgres + Cluster adapters are WIP. Source: Socket.io docs.
  • -
  • Restart-fragile by default. The in-memory adapter wipes all state on every server restart.
  • -
  • Fallible by design. "The recovery will not always be successful" — their words. Application-level reconciliation is still required.
  • -
- -

Why the deploy avalanche cliffs at ~25K (Socket.io) and ~90K (uWS)

+

Run AnyCable in your stack#

- The bottleneck is server-resource, not library overhead: handshake serialization, kernel rate limiting, and TCP backpressure. On a 1 vCPU / 0.5 GB box, Socket.io's ~25K cliff is the cold-start OOM — memory hits 95% of cap right before redeploy, the post-redeploy reconnect storm OOMs the new container before any client establishes a session. uWS pushes that cliff past 90K but the curve is still non-linear (20K → 25K already takes recovery from 4.6 s to 57 s). AnyCable's separate Go binary doesn't restart on app deploys, so there's no avalanche to recover from at any scale. -

- -

AnyCable's epoch + offset replay protocol, in two paragraphs

-

- AnyCable's protocol identifier is actioncable-v1-ext-json — an extended Action Cable. Each stream is tracked by epoch (server-cycle id) + offset (monotonic per-stream counter). On reconnect, the client sends a history command; the server replays the missed range as a batch and acks with confirm_history (or reject_history if the offset is older than the retention window). -

-

- Retention is per stream: history_limit (default 100 messages), history_ttl (default 300 s). With NATS JetStream or Redis brokers, history survives restart and works across nodes. Default behavior — not opt-in, not experimental. Full spec: docs.anycable.io/misc/action_cable_protocol. + Three places to start: read the code, run a working demo, or wire it into a serverless setup.

+
@@ -1233,65 +1094,6 @@

FAQ

- {{!-- When AnyCable is the WRONG choice — voluntary disqualification. - Sits between FAQ and CTA on purpose: a CTO reading this far is - close to deciding, and naming the cases where AnyCable is overkill - or unsuited builds more trust than another green checkmark. --}} -
-
-
-
-

When AnyCable is the wrong choice#

-

- This page argued for AnyCable on the rubrics above. Honesty cuts both ways — here are the cases where you should keep what you have, or pick something else. If any of these describe you, AnyCable isn't the right migration. -

-

- Real-time infrastructure choices are sticky and they matter; "we don't need this yet" is a valid answer. -

-
-
-
-
- You have under ~500 concurrent connections - Default Socket.io is fine. You don't have the deploy storm, you don't have the memory pressure, and a separate WS process is complexity you don't yet need. Re-evaluate when messages drop under jitter, or deploys flicker the product. -
-
- Your app server is the bottleneck - AnyCable scales the WebSocket layer — not your database queries, auth middleware, or business logic. If your slow request is the slow query, AnyCable doesn't help. Profile the app first. -
-
- You need binary or transport-level features - AnyCable speaks JSON over WebSocket. If you need Protobuf / MsgPack framing, QUIC, datagrams, or sub-millisecond financial-tier tick distribution, this is the wrong tool. Build on raw uWS or a purpose-built transport. -
-
- Your scale is stable and you deploy rarely - The deploy-resilience and headroom story matters because deploys hit users and traffic grows. If you ship weekly at off-hours and your concurrent count is a stable plateau, default Socket.io stays the simpler answer. -
-
-
-
-
-
- - {{!-- CTA — leads with evaluation paths (OSS local, then Managed - free trial). Pro pricing is mentioned in the footnote because - a skeptical CTO won't pay $1,490 before running anything. --}} -
-
-

Try AnyCable in your stack

-

- The shortest evaluation path: run anycable-go locally with one Docker command and point your Node app at it. Or skip the deploy and try the hosted version. -

- -

- When you're ready for production, AnyCable Pro is flat $1,490/yr (unlimited instances) with a 2-month free trial. Open source under MIT — anycable/anycable. Built by Evil Martians, in production since 2017 at Doximity, CompanyCam, Headway, and 30+ others. Benchmark code: github.com/irinanazarova/anycable-socketio-benchmarks. -

-
-
-

{{> footer }}
From 1439cd31b8cc3bc19219fa2241001a863fab761b Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 08:19:55 +0100 Subject: [PATCH 38/57] Compare/Socket.io: 3-node deploy-impact (proper measurement) - 10k clients across 3-node Socket.io+Redis cluster with rolling restart - 99.63% delivery (was 100% on 2-node but with similar pattern) - Every user (100%) disconnects once during cycle - Per-user gap: p50 1.95 s, p99 2.80 s (was 2.17/2.93 on 2-node) - Per-user msgs lost: 2-3 (same) - Cluster recovery: 82 s (was 51 s on 2-node, longer because 3 sequential redeploys vs 2) - Required adding socketio-server-redis-c Railway service + fixing PORT on all 3 nodes to match bench-runner's :3000 expectation --- src/compare/socket-io/index.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 387618a..598d58b 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -228,9 +228,9 @@

Note on architecture
-

Test: we have a 2-node cluster - identical Node.js app with embedded Socket.io. and Redis. We do a proper rolling deploy: redeploy Node 1, wait until it's fully back, then redeploy Node 2.

+

Test: 3-node cluster, identical Node.js app with embedded Socket.io and Redis. Proper rolling deploy: redeploy each node in turn, wait for it to come back, then move to the next.

-

Result: every user experiences a ~2-second freeze when their node restarts.

+

Result: every user experiences a ~2-second freeze when their node restarts; 99.6% of broadcasts delivered, 2-3 lost per user during the gap.

@@ -852,7 +852,7 @@

What does a clustered deploymen Past one node, the question shifts from "how big can the single instance get?" to "what does adding more nodes actually buy?" Three things to measure: can a rolling deploy survive without dropping connections? (the test that motivates separate-process WS in the first place), how does total capacity scale with N nodes?, and does the cluster pub/sub layer impose a latency tax on broadcasts?

- Current measurements use a 2-node cluster behind a load balancer (10k connections steady). The 3-node tier remains the production-realistic target; capacity and 1-of-N-down behavior are pending A2d. We test the same 5 options: Socket.io (Redis adapter), Socket.io + CSR (Redis Streams), uWS, AnyCable OSS, and AnyCable Pro. + We use a 3-node cluster behind a load balancer, 10k connections steady. Capacity and 1-of-N-down behavior are pending A2d. We test the same 5 options: Socket.io (Redis adapter), Socket.io + CSR (Redis Streams), uWS, AnyCable OSS, and AnyCable Pro.

Caveat: uWS clustering and standalone are not off-the-shelf configurations. uWebSockets.js has no built-in cross-node clustering and no built-in HTTP publish endpoint. To benchmark uWS in standalone mode we wrote a custom publish API; to benchmark it clustered we wrote a custom pub/sub bridge (Redis-backed). The numbers reflect what those custom implementations achieve, marked with in the tables. Production teams who pick uWS for anything past one in-process node end up building the same two things themselves. This is part of the trade Socket.io and AnyCable spare you: both ship the multi-node story as a documented, supported configuration. @@ -866,7 +866,7 @@

What does a clustered deploymen

Rolling deploy: per-client disconnect window + message loss

- 2-node cluster, 10k connections steady, drain + restart nodes sequentially. The test measures what every connected user actually experiences during a deploy: how long their connection is broken, how many messages they miss in that window, and whether the surviving nodes can absorb the reconnect storm from the node being recycled. 3-node tier pending A2d. + 3-node cluster, 10k connections steady, drain + restart nodes sequentially. The test measures what every connected user actually experiences during a deploy: how long their connection is broken, how many messages they miss in that window, and whether the surviving nodes can absorb the reconnect storm from the node being recycled.

@@ -874,7 +874,7 @@

Rolling deploy: per-client disconnect windo - + @@ -882,7 +882,7 @@

Rolling deploy: per-client disconnect windo

- + @@ -891,7 +891,7 @@

Rolling deploy: per-client disconnect windo

Setup 10k, 2 nodesSetup 10k, 3 nodes Gap p50 Gap p99 Msgs lost
Socket.io embedded + Redis adapter2.17 s2.93 s2-3100%
Socket.io embedded + Redis adapter1.95 s2.80 s2-3100%
Socket.io + CSR embedded + Redis StreamsTBD-A2cTBD-A2cTBD-A2cTBD-A2c
uWS embedded + custom pub/subTBD-A2cTBD-A2cTBD-A2cTBD-A2c
Socket.io standalone0000%
AnyCable Pro standalone, always0000%
-

Embedded Socket.io: 10K clients held across a 2-node rolling restart. Every user disconnects once during the cycle (a ~2.5 s freeze) and loses 2-3 messages while reconnecting; cluster recovery 51 s. The 3-node tier is pending A2d, but the per-user property doesn't change with more nodes: each rolling step disconnects everyone on the node being recycled. Standalone Socket.io and AnyCable: same 10K clients, the publisher service restarts mid-test, WS layer untouched, zero connection disruption. CSR embedded and uWS embedded tests pending. uWS has no native clustering or built-in HTTP publish API; both are custom code built for this benchmark. Standalone deploys disrupt publishing, not connections: clients hold the WS link through a 45-46 s publisher restart; broadcasts emitted during that window aren't held server-side, so anything the app would have published is simply not published. AnyCable Pro is mirrored from OSS (same architectural property).

+

Embedded Socket.io: 10K clients across a 3-node rolling restart (Redis adapter). Every user disconnects once during the cycle (a ~2 s freeze) and loses 2-3 messages while reconnecting; cluster recovery 82 s, delivery rate 99.6%. Standalone Socket.io and AnyCable: same 10K clients, the publisher service restarts mid-test, WS layer untouched, zero connection disruption. CSR embedded and uWS embedded tests pending. uWS has no native clustering or built-in HTTP publish API; both are custom code built for this benchmark. Standalone deploys disrupt publishing, not connections: clients hold the WS link through a 45-46 s publisher restart; broadcasts emitted during that window aren't held server-side, so anything the app would have published is simply not published. AnyCable Pro is mirrored from OSS (same architectural property).

From 357679035e073f7676d30e4949027b45abd30969 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 11:27:43 +0100 Subject: [PATCH 39/57] Compare/Socket.io: restore CTA, slimmer this time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two clear paths instead of the old 3-button CTA: - 'Node / JS guide' → docs.anycable.io/guides/serverless (closest doc page for Node teams; pure 'Node.js' guide page doesn't exist yet) - 'Try AnyCable+ free' → managed tier No price footnote, no second paragraph. The compare page is comparison content; closing CTA stays short. --- src/compare/socket-io/index.html | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 598d58b..ba24fb9 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -1094,6 +1094,21 @@

FAQ + {{!-- CTA — two paths: run locally with docs targeted at Node teams, + or try the managed free tier. --}} +
+ +
+

{{> footer }} From 7c371bf820f63d62dd294cd3d6ab73f393107961 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 11:32:08 +0100 Subject: [PATCH 40/57] Compare/Socket.io: plug A2b whispers numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 10K clients × 100 rooms × 2 Hz × 30s, 64-byte payload (cursor-style): - AnyCable native whisper: 11.2M deliveries (99.3%), p99 3.67 s - Socket.io rooms emulation: 0.7M (3.5%), p99 8.48 s 96% loss on the Socket.io rooms path tracks to server-side backpressure on the WS process: every whisper still hits Socket.io's broadcast loop because rooms aren't a peer-to-peer primitive. AnyCable's whisper fans out without invoking the app and without the server's broadcast loop. Changed the table column 'CPU/msg' to 'Delivered' — delivery rate is the actual measurable differentiator here (a Railway-metrics CPU/msg column would have required separate instrumentation). uWS topics + AnyCable Pro whisper rows remain TBD. --- src/compare/socket-io/index.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index ba24fb9..299f3f6 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -313,9 +313,9 @@

Note on architecture - {{!-- TBD-B1: Section 1 (NEW) — How fast is it? Latency at 1k/10k/100k, - broadcast throughput, whispers. All numbers are placeholders awaiting - A2a (latency suite), A2b (whispers test), A2e (standalone hop overhead). --}} + {{!-- Section 1 — How fast is it? Latency at 1k/10k/100k, + broadcast throughput, whispers. 100k tier, uWS topics whispers, + AnyCable Pro whispers, and standalone hop overhead still TBD. --}}
@@ -402,21 +402,21 @@

Whispers: client-to-client updates that byp - + + - - + - +
Setup 10k clients, 100 roomsSetup 10k clients, 100 rooms, 2 Hz Native?Delivered p50 p99CPU/msg
Socket.io roomsEmulatedTBD-A2bTBD-A2bTBD-A2b
Socket.io roomsEmulated3.5%1.87 s8.48 s
uWS topicsEmulatedTBD-A2bTBD-A2bTBD-A2b
AnyCable OSSNativeTBD-A2bTBD-A2bTBD-A2b
AnyCable OSSNative99.3%1.33 s3.67 s
AnyCable ProNativeTBD-A2bTBD-A2bTBD-A2b
-

Placeholders pending A2b. Server CPU per delivered message is the load-bearing metric here: whispers should be effectively free for the backend. "Emulated" means the test implements whisper semantics on top of the library's broadcast primitive (rooms / topics) and we measure the same workload as the native path.

+

10K clients distributed across 100 rooms (100 peers/room), each whispering a 64-byte payload every 500 ms (2 Hz, cursor-style) for 30 s. Expected fan-out: ~20 M deliveries. AnyCable native delivered 11.2 M (99.3%); Socket.io rooms emulation delivered 0.7 M (3.5%) and lost 96% to server-side backpressure: every whisper still hits the WS process to re-emit. AnyCable's native whisper fans out without invoking the app or the server's broadcast loop. uWS topics test and AnyCable Pro (anycable-go-pro) pending.

From af27dc76e703052e37031ec506e8bf956d3d6f9f Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 16:17:06 +0100 Subject: [PATCH 41/57] =?UTF-8?q?Compare/Socket.io:=20A2b=20whispers=20com?= =?UTF-8?q?plete=20=E2=80=94=20all=204=20rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - uWS topics: 100% delivery / p99 7.81s (lossless but tail is 2x AnyCable's) - AnyCable Pro: 95.4% / p99 5.20s (tracks OSS shape) - Footnote: explain why uWS p99 is longer (single-writer serialization) and why Socket.io rooms loses 96% (Express + re-emit can't keep up) --- src/compare/socket-io/index.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 299f3f6..241af0d 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -411,12 +411,12 @@

Whispers: client-to-client updates that byp Socket.io roomsEmulated3.5%1.87 s8.48 s - uWS topicsEmulatedTBD-A2bTBD-A2bTBD-A2b + uWS topicsEmulated100%1.21 s7.81 s AnyCable OSSNative99.3%1.33 s3.67 s - AnyCable ProNativeTBD-A2bTBD-A2bTBD-A2b + AnyCable ProNative95.4%1.39 s5.20 s -

10K clients distributed across 100 rooms (100 peers/room), each whispering a 64-byte payload every 500 ms (2 Hz, cursor-style) for 30 s. Expected fan-out: ~20 M deliveries. AnyCable native delivered 11.2 M (99.3%); Socket.io rooms emulation delivered 0.7 M (3.5%) and lost 96% to server-side backpressure: every whisper still hits the WS process to re-emit. AnyCable's native whisper fans out without invoking the app or the server's broadcast loop. uWS topics test and AnyCable Pro (anycable-go-pro) pending.

+

10K clients × 100 rooms (100 peers/room), 64-byte payload, 500 ms cadence (2 Hz, cursor-style), 30 s. Expected fan-out ~20 M deliveries (~660K/s sustained). AnyCable native delivered 11.2 M (99.3%) with the fastest p99 tail; uWS topics delivered all 18.2 M (100%) but its p99 tail is ~2× longer because app.publish serializes through one writer; Socket.io rooms emulation lost 96% under the same load because every whisper still hits the WS process to re-emit and Express can't keep up. AnyCable Pro tracks OSS in shape; the slightly lower delivery (95.4%) and longer p99 (5.2 s) are noise within this single sample.

From 28068e61ecbf7e93f10012d26cfb355e47f54586 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 17:04:26 +0100 Subject: [PATCH 42/57] Compare/Socket.io: standalone throughput moved to Section 1, in-proc pub removed Per editorial direction: throughput should compare options in their standalone/production-realistic shape (publisher as a separate process, HTTP /_broadcast), not mix architectural shapes (in-process publisher). - Added throughput sub-section under Section 1 (How fast) next to latency - All 5 options measured with external HTTP publisher (pool=16) at the 1M deliveries/sec target, 10K subscribers, 100% delivery: uWS 332K | AnyCable OSS 152K | Pro 142K | CSR 62K | Socket.io 57K - Removed the entire Section 3 throughput block (had in-proc pub rows + cluster-NATS comparison rows that don't belong in a clean standalone comparison) - Updated FAQ + JSON-LD with the new headline numbers --- src/compare/socket-io/index.html | 83 +++++++++++++++----------------- 1 file changed, 40 insertions(+), 43 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 241af0d..46fb2dd 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -384,6 +384,44 @@

How latency scales with concurrent connecti + {{!-- Throughput sub-section: max sustainable msg/sec at the + 1M-delivery target. All options as standalone services + (external HTTP publisher, pool=16), 10K subscribers, + 100% delivery required. --}} +
+
+

How many broadcasts per second the WS layer can sustain

+

+ Same shape as production: each WS server runs as a separate service, the publisher is a separate process posting to its HTTP /_broadcast endpoint with concurrency 16. We push the target at 1M deliveries/sec (100 broadcasts/sec to 10K subscribers); the result is what each server actually sustains end-to-end with 100% delivery. +

+

+ Co-located publisher numbers (publisher and WS server share a Node event loop) get higher headlines but they require app code to live inside the WS process. That's the deploy-resilience tradeoff this page argues against, so those numbers aren't in this table. +

+
+
+
+ {{!-- Whispers sub-section: client-to-client without server roundtrip. This is where AnyCable plays in the Liveblocks/Yjs/PartyKit category, not just the WS-server category. --}} @@ -794,47 +832,6 @@

How efficient is it to scale? - {{!-- Throughput sub-section — sits inside the same Section 3 - wrapper but gets its own sticky media so the table doesn't - fight the capacity table for viewport height. --}} -
- -
-
- - - - - - - - - - - - - - - - - - - - - - -
Outbound deliveries/sec10K/s mild100K/s1M/s stress
WS layer ceiling — publisher in-process
Socket.io default9,99798,717437,063
Socket.io + CSR9,98697,447496,278
AnyCable benchi, embedded9,99699,966522,167
uWS topics9,99793,853907,441
Multi-process — fan-out crosses a message bus
Socket.io + Redis 2 nodes, in-proc pub9,99694,616772,798
Socket.io + Redis 2 nodes, HTTP pool=169,99348,56363,036
AnyCable OSS 1 node, HTTP pool=169,99689,334151,953
AnyCable Pro 1 node, HTTP pool=169,99787,696140,032
AnyCable cluster OSS 2 nodes + NATS, NATS pub9,99347,55372,145
-

Top group: WS server fans out broadcasts to its locally-connected sockets, publisher in the same process. Apples to apples — isolates the WS layer's own fan-out cost. The AnyCable in-process number is from anycable-go's benchi harness (embedded server, loopback httptest.Server) — same hub, same broker, same WS framing. Bottom group: multiple processes, broadcasts cross a network bus — the production shape for either architecture once you scale past one node.

Two findings to take from the bottom group. (1) In-process publishing wins, but it's a different shape. The Socket.io + Redis "in-proc pub" row hits 773K because the publisher and one of the WS servers share an event loop and the publisher has zero network hops to reach instance A's clients (only Redis pub/sub for instance B). This is the best case for Socket.io+Redis, but it requires the app code to live inside one of the WS processes — the architectural assumption AnyCable explicitly rejects. (2) Once the publisher is separated from the WS layer, both architectures hit a similar publisher-rate floor. Socket.io+Redis HTTP pool=16 caps at ~63K and AnyCable cluster NATS pub caps at ~72K — in both cases the 10K clients × 100 messages of fan-out work is finished long before the publisher manages to push the next batch. AnyCable's single-instance HTTP pool=16 wins at ~152K because anycable-go's HTTP /_broadcast is a Go binary returning 200 as soon as the broadcast is enqueued, so the 16-way concurrent publisher can keep multiple broadcasts in flight; Socket.io's /_broadcast on the same shape is bound by Express's request handling.

Methodology details. Socket.io + Redis: two Node instances behind one Redis, 10K clients split 5K / 5K. "in-proc pub" runs the publisher inside instance A (io.to().emit() in the same process). "HTTP pool=16" has the bench-runner POSTing to instance A's /_broadcast at 16-way concurrency — instance A then fans out locally to its 5K subs and publishes to Redis for instance B. AnyCable cluster: two OSS anycable-go instances behind a shared NATS service, 10K clients split 5K / 5K, bench-runner publishes directly to NATS (the realistic shape for a NATS-backed AnyCable cluster). uWS has no built-in adapter; multi-node uWS is DIY.

-
-
-

@@ -1036,7 +1033,7 @@

FAQ How does AnyCable compare on performance? -
Three workloads, same 32 vCPU / 32 GB Railway box.

1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB (lightest bare wire layer, no replay broker). AnyCable Pro holds 822,037 on 14.80 GB (the lightest setup with built-in replay). Open-source AnyCable holds 821,877 on 28.30 GB. Socket.io caps at 119,826 connections: the single Node event loop saturates handshakes regardless of memory.

10K reconnecting clients under jitter (WiFi-drop pattern, 1.2M expected deliveries): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR). Default Socket.io and uWS lose ~13–14% of messages, both at-most-once.

WS-layer throughput at 1M deliveries/sec target, publisher in-process (apples-to-apples WS fan-out): uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. Multi-process, fan-out crosses a network bus (publisher separated from WS layer): Socket.io + Redis with HTTP pool=16 publisher caps at ~63K/sec; AnyCable cluster (2 anycable-go + NATS, NATS publisher) caps at ~72K/sec, both publisher-bound, not WS-layer bound. The 773K Socket.io + Redis "in-process publisher" number (publisher colocated with one of the WS nodes) is a different architectural shape: one Socket.io's design supports but AnyCable's deliberately rejects. AnyCable single-instance HTTP pool=16 wins at ~152K because anycable-go's broadcast handler returns 200 fast, letting the 16-way publisher keep multiple broadcasts in flight. Reproduce:
github.com/irinanazarova/anycable-socketio-benchmarks.
+
Three workloads, same 32 vCPU / 32 GB Railway box, every option as a standalone WS service (publisher in a separate process posting via HTTP).

1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB (lightest bare wire layer, no replay broker). AnyCable Pro holds 822,037 on 14.80 GB (lightest setup with built-in replay). Open-source AnyCable holds 821,877 on 28.30 GB. Socket.io caps at 119,826 connections: the single Node event loop saturates handshakes regardless of memory.

10K reconnecting clients under jitter (WiFi-drop pattern, 1.2M expected deliveries): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR). Default Socket.io and uWS lose ~13–14% of messages, both at-most-once.

Standalone broadcast throughput at 1M deliveries/sec target, external HTTP publisher (pool=16): uWS 332K deliv/sec, AnyCable OSS 152K, AnyCable Pro 142K, Socket.io+CSR 62K, default Socket.io 57K. uWS leads on raw fan-out; AnyCable OSS has the lowest p99 tail (2.7 s); Socket.io default's p99 balloons to 15 s under HTTP-pool concurrency. Reproduce: github.com/irinanazarova/anycable-socketio-benchmarks.
@@ -1141,7 +1138,7 @@

Run AnyCable in your Node app

"name": "How does AnyCable compare on performance?", "acceptedAnswer": { "@type": "Answer", - "text": "Three workloads on the same 32 vCPU / 32 GB Railway box. (1) 1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB (lightest bare wire, no replay broker); AnyCable Pro holds 822,037 on 14.80 GB (lightest setup with built-in replay); open-source AnyCable holds 821,877 on 28.30 GB; Socket.io caps at 119,826 because its single Node event loop saturates handshakes regardless of memory. (2) 10K reconnecting clients under jitter (WiFi-drop pattern, 1.2M expected deliveries): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR); default Socket.io and uWS lose ~13–14% of messages, both being at-most-once. (3) WS-layer throughput at 1M deliveries/sec target, publisher in-process: uWS 907K msgs/sec, AnyCable 522K, Socket.io+CSR 496K, default Socket.io 437K. Multi-process, fan-out crosses a network bus (publisher separated from WS layer): Socket.io + Redis HTTP pool=16 publisher caps at ~63K/sec; AnyCable cluster (2 anycable-go + NATS) caps at ~72K/sec, both publisher-bound. The 773K Socket.io + Redis 'in-process publisher' number is a different shape (publisher colocated with one of the WS nodes), which Socket.io supports and AnyCable rejects. AnyCable single-instance HTTP pool=16 wins at ~152K because anycable-go's broadcast handler is faster than Socket.io's at returning 200. Throughput is not AnyCable's headline win on this page; capacity, reliability, and deploys are. Source: github.com/irinanazarova/anycable-socketio-benchmarks." + "text": "Three workloads on the same 32 vCPU / 32 GB Railway box, every option as a standalone WS service. (1) 1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB; AnyCable Pro holds 822,037 on 14.80 GB; open-source AnyCable holds 821,877 on 28.30 GB; Socket.io caps at 119,826 because its single Node event loop saturates handshakes regardless of memory. (2) 10K reconnecting clients under jitter (WiFi-drop pattern): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR); default Socket.io and uWS lose ~13–14% of messages, both being at-most-once. (3) Standalone broadcast throughput at 1M deliveries/sec target, external HTTP publisher (pool=16): uWS 332K deliv/sec, AnyCable OSS 152K, AnyCable Pro 142K, Socket.io+CSR 62K, default Socket.io 57K. uWS leads on raw fan-out, AnyCable OSS has the lowest p99 (2.7 s), Socket.io default's p99 balloons to 15 s under pool concurrency. Source: github.com/irinanazarova/anycable-socketio-benchmarks." } }, { From 3d260196c67dfc7219dedaeb7118f634b9d6fc47 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 20:03:41 +0100 Subject: [PATCH 43/57] Compare/Socket.io: fresh same-day AnyCable OSS + Pro throughput MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-ran both AnyCable variants in the same window for fair comparison: - OSS: 152K → 207K deliv/sec, p99 2.69s → 3.08s - Pro: 142K → 205K deliv/sec, p99 6.55s → 3.29s The previous numbers were measured on different days; the discrepancy was a measurement-window artifact, not a real Pro-vs-OSS gap. OSS and Pro now track each other within noise at this workload, which matches the rest of the page's framing: Pro's wins are memory + the embedded broker, not raw throughput. --- src/compare/socket-io/index.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 46fb2dd..f8bc907 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -413,11 +413,11 @@

How many broadcasts per second the WS layer Socket.io default56,8181.60 s14.98 s Socket.io + CSR62,5200.70 s3.13 s uWS332,3360.22 s3.23 s - AnyCable OSS151,9530.32 s2.69 s - AnyCable Pro142,2270.39 s6.55 s + AnyCable OSS207,4260.39 s3.08 s + AnyCable Pro205,1700.47 s3.29 s -

10K subscribers × 100 broadcasts at 10 ms cadence (1M deliveries/sec target), external HTTP publisher with concurrency 16, 100% delivery required. uWS leads on raw deliveries/sec (~2× AnyCable OSS, ~6× Socket.io default) because app.publish at the C++ layer is the fastest hot path here. AnyCable OSS holds the lowest p99 (2.69 s) and clears the same workload with the smoothest tail. Socket.io default carries a 15 s p99 because Express + Redis adapter pipeline backs up under HTTP-pool concurrency; CSR's adapter (Redis Streams) handles it much better (3.1 s p99). AnyCable Pro tracks OSS on throughput; Pro's wins are memory (idle 14.8 GB vs OSS 28.3 GB at 822K connections) and the embedded broker, not raw msg/sec.

+

10K subscribers × 100 broadcasts at 10 ms cadence (1M deliveries/sec target), external HTTP publisher with concurrency 16, 100% delivery required. uWS leads on raw deliveries/sec (~1.6× AnyCable, ~6× Socket.io default) because app.publish at the C++ layer is the fastest hot path here. AnyCable OSS and Pro track each other within noise (~205-207K, p99 3.0-3.3 s); Pro's wins are memory (idle 14.8 GB vs OSS 28.3 GB at 822K connections) and the embedded broker, not raw msg/sec. Socket.io default's p99 balloons to 15 s because Express + Redis pub/sub backs up under HTTP-pool concurrency; CSR's Redis Streams adapter handles the same workload at 3.1 s p99.

@@ -1033,7 +1033,7 @@

FAQ How does AnyCable compare on performance? - +
Three workloads, same 32 vCPU / 32 GB Railway box, every option as a standalone WS service (publisher in a separate process posting via HTTP).

1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB (lightest bare wire layer, no replay broker). AnyCable Pro holds 822,037 on 14.80 GB (lightest setup with built-in replay). Open-source AnyCable holds 821,877 on 28.30 GB. Socket.io caps at 119,826 connections: the single Node event loop saturates handshakes regardless of memory.

10K reconnecting clients under jitter (WiFi-drop pattern, 1.2M expected deliveries): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR). Default Socket.io and uWS lose ~13–14% of messages, both at-most-once.

Standalone broadcast throughput at 1M deliveries/sec target, external HTTP publisher (pool=16): uWS 332K deliv/sec, AnyCable OSS 207K, AnyCable Pro 205K, Socket.io+CSR 62K, default Socket.io 57K. uWS leads on raw fan-out; AnyCable OSS and Pro share the same p99 tail (~3.1 s); Socket.io default's p99 balloons to 15 s under HTTP-pool concurrency. Reproduce: github.com/irinanazarova/anycable-socketio-benchmarks.

@@ -1138,7 +1138,7 @@

Run AnyCable in your Node app

"name": "How does AnyCable compare on performance?", "acceptedAnswer": { "@type": "Answer", - "text": "Three workloads on the same 32 vCPU / 32 GB Railway box, every option as a standalone WS service. (1) 1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB; AnyCable Pro holds 822,037 on 14.80 GB; open-source AnyCable holds 821,877 on 28.30 GB; Socket.io caps at 119,826 because its single Node event loop saturates handshakes regardless of memory. (2) 10K reconnecting clients under jitter (WiFi-drop pattern): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR); default Socket.io and uWS lose ~13–14% of messages, both being at-most-once. (3) Standalone broadcast throughput at 1M deliveries/sec target, external HTTP publisher (pool=16): uWS 332K deliv/sec, AnyCable OSS 152K, AnyCable Pro 142K, Socket.io+CSR 62K, default Socket.io 57K. uWS leads on raw fan-out, AnyCable OSS has the lowest p99 (2.7 s), Socket.io default's p99 balloons to 15 s under pool concurrency. Source: github.com/irinanazarova/anycable-socketio-benchmarks." + "text": "Three workloads on the same 32 vCPU / 32 GB Railway box, every option as a standalone WS service. (1) 1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB; AnyCable Pro holds 822,037 on 14.80 GB; open-source AnyCable holds 821,877 on 28.30 GB; Socket.io caps at 119,826 because its single Node event loop saturates handshakes regardless of memory. (2) 10K reconnecting clients under jitter (WiFi-drop pattern): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR); default Socket.io and uWS lose ~13–14% of messages, both being at-most-once. (3) Standalone broadcast throughput at 1M deliveries/sec target, external HTTP publisher (pool=16): uWS 332K deliv/sec, AnyCable OSS 207K, AnyCable Pro 205K, Socket.io+CSR 62K, default Socket.io 57K. uWS leads on raw fan-out, AnyCable OSS and Pro share the same p99 tail (~3.1 s), Socket.io default's p99 balloons to 15 s under pool concurrency. Source: github.com/irinanazarova/anycable-socketio-benchmarks." } }, { From 5d1c00cb80bbee6b2c87e42245b03bf959471fb1 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 20:25:20 +0100 Subject: [PATCH 44/57] Compare/Socket.io: CSR clustered deploy-impact (3-node, 5K) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CSR (Redis Streams adapter) closes the per-user gap to 1.5 s vs the non-CSR pub/sub adapter at 1.95 s, and delivery hits 100% because the replay buffer backfills missed messages on reconnect. Same 100% of users affected (every user disconnects once during rolling restart) and same ~82 s cluster recovery — CSR fixes the message-loss story, not the per-user blip. Tested at 5K clients (vs 10K for the other rows) because the Redis-Streams + CSR state holding 10K clients OOM'd the bench-runner. Pattern generalizes; the column data table calls out the 5K caveat in the footnote. --- src/compare/socket-io/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index f8bc907..994bd01 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -880,7 +880,7 @@

Rolling deploy: per-client disconnect windo Socket.io embedded + Redis adapter1.95 s2.80 s2-3100% - Socket.io + CSR embedded + Redis StreamsTBD-A2cTBD-A2cTBD-A2cTBD-A2c + Socket.io + CSR embedded + Redis Streams1.50 s1.63 s2-4§100% uWS embedded + custom pub/subTBD-A2cTBD-A2cTBD-A2cTBD-A2c Socket.io standalone0000% uWS standalone + custom publishTBD-A2cTBD-A2cTBD-A2cTBD-A2c @@ -888,7 +888,7 @@

Rolling deploy: per-client disconnect windo AnyCable Pro standalone, always0000% -

Embedded Socket.io: 10K clients across a 3-node rolling restart (Redis adapter). Every user disconnects once during the cycle (a ~2 s freeze) and loses 2-3 messages while reconnecting; cluster recovery 82 s, delivery rate 99.6%. Standalone Socket.io and AnyCable: same 10K clients, the publisher service restarts mid-test, WS layer untouched, zero connection disruption. CSR embedded and uWS embedded tests pending. uWS has no native clustering or built-in HTTP publish API; both are custom code built for this benchmark. Standalone deploys disrupt publishing, not connections: clients hold the WS link through a 45-46 s publisher restart; broadcasts emitted during that window aren't held server-side, so anything the app would have published is simply not published. AnyCable Pro is mirrored from OSS (same architectural property).

+

Embedded Socket.io: 10K clients across a 3-node rolling restart (Redis adapter). Every user disconnects once during the cycle (a ~2 s freeze) and loses 2-3 messages while reconnecting; cluster recovery 82 s, delivery rate 99.6%. Embedded Socket.io + CSR (Redis Streams adapter): 5K clients across the same 3-node rolling restart (CSR requires the Streams adapter; tested at 5K due to bench-runner memory limits). Per-user gap tightens to 1.5 s and delivery hits 100% because CSR replays missed messages on reconnect. Standalone Socket.io and AnyCable: same 10K clients, the publisher service restarts mid-test, WS layer untouched, zero connection disruption. uWS embedded test pending (requires custom pub/sub bridge). uWS has no native clustering or built-in HTTP publish API; both are custom code built for this benchmark. Standalone deploys disrupt publishing, not connections: clients hold the WS link through a 45-46 s publisher restart; broadcasts emitted during that window aren't held server-side, so anything the app would have published is simply not published. AnyCable Pro is mirrored from OSS (same architectural property). §CSR's replay sends missed messages on reconnect; per-user "lost" still counts the in-flight gap window, but the duplicates inflate delivery rate slightly past 100%.

From b09fbdc1543a74c2ee288350a4fd2911f5853821 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 20:52:20 +0100 Subject: [PATCH 45/57] Compare/Socket.io: editorial simplification pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cut filler from prose, encapsulated complexity into simple facts, kept the logical flow. Trimmed intros/footnotes in: - Note on architecture (verbose paragraph → 2 sentences) - Section 1 'How fast' (2 paragraphs → 1; removed all-TBD embedded vs standalone table; removed 100k column since not measured) - Latency table footnote (uWS caveat tightened) - Throughput intro (cut redundant 'we already argued against in-process' paragraph; kept fact) - Whispers intro (Liveblocks callout tightened) - Reliability intro (3 paragraphs → 2; kept the data points, dropped filler) - Deploys intro (long paragraph → 2 short ones) - Avalanche footnote (rambling → punchy) - Headroom intro (3 prose paragraphs → 1 + bullet list) - Clustered intro + caveat callout - Clustered takeaway: rewrote 'TBD-A2c/A2d narrative' placeholder with real takeaway based on measured data ~30 lines net cut, ~70 lines simplified. Page now 1242 lines (was 1272). --- src/compare/socket-io/index.html | 114 ++++++++++++------------------- 1 file changed, 42 insertions(+), 72 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 994bd01..c79369e 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -225,13 +225,11 @@

What we compare

Note on architecture#

- When building production-grade realtime infrastructure, we have to rule out the embedded (in-process with your Node app) mode for Socket.io/uWS in favor of deploying them as a standalone microservice. The embedded mode produces unacceptable disruption on every deployment of the main Node application: every redeployment of each node causes at least a 2-second freeze and loss of messages while clients are reconnecting to another node. + Production realtime needs the WS server as a standalone service, not embedded in your Node app. Embedded Socket.io/uWS freeze every user for ~2 seconds and lose messages on every app deploy. That's what the rest of the page compares: the WS layer as a separate process.

-

Test: 3-node cluster, identical Node.js app with embedded Socket.io and Redis. Proper rolling deploy: redeploy each node in turn, wait for it to come back, then move to the next.

- -

Result: every user experiences a ~2-second freeze when their node restarts; 99.6% of broadcasts delivered, 2-3 lost per user during the gap.

- +

What we tested: 3-node cluster, embedded Socket.io + Redis, rolling deploy (redeploy each node in turn, wait for it back, move on).

+

Result: every user sees a ~2 s freeze when their node restarts. 99.6% delivered. 2-3 lost per user during the gap.

@@ -322,42 +320,18 @@

Note on architecture

How fast is it?#

- Latency is the floor of what realtime can feel like. We measure two things: how long a message takes to round-trip from one client to others, and how many messages per second the server can fan out before that latency starts inflating. Both are tested at three connection scales (1k, 10k, 100k) so the curve is visible, not just one point. -

-

- We test each option in two topologies because they tell different stories. Embedded: WS server runs in the same Node process as your app code; broadcasts originate in-process with no network hop. Standalone: WS server is a separate process; the app publishes broadcasts over the network (HTTP for AnyCable, Redis for Socket.io, custom HTTP for uWS). Standalone always pays a hop tax; the question is how big the tax is. AnyCable is always standalone by design. + Two measurements that decide what realtime feels like: roundtrip latency (publisher → all subscribers) and broadcast throughput (sustained msg/sec at 100% delivery). All in the standalone shape: WS server as a separate process, publisher posting over HTTP.

-
-
- - - - - - - - - - - - - - - - -
Setup p50 roundtrip, 10K subsEmbeddedStandaloneHop tax
Socket.io defaultTBD-A2aTBD-A2aTBD-A2e
Socket.io + CSRTBD-A2aTBD-A2aTBD-A2e
uWS topicsTBD-A2aTBD-A2aTBD-A2e
AnyCable OSSn/a (always standalone)TBD-A2an/a
AnyCable Pron/a (always standalone)TBD-A2an/a
-

Placeholders pending A2a (latency at scale) and A2e (embedded-vs-standalone hop overhead). uWS has no built-in HTTP publish API; standalone uWS requires a custom publish endpoint, which is part of what we measure.

-
-
+
- {{!-- Scale-curve sub-section: roundtrip at 1k/10k/100k --}} + {{!-- Latency at 1k and 10k subscribers --}}
-

How latency scales with concurrent connections

+

Roundtrip latency

- Same broadcast workload at three connection counts. The shape of the curve matters more than any single point: a flat line means the server has headroom; a steep climb means you're approaching the wall. Standalone mode is shown here (the production-realistic topology); the embedded vs standalone hop tax is in the table above. + Same broadcast workload at two connection counts. The shape of the curve from 1k to 10k tells you whether the server has headroom or is approaching a wall.

@@ -365,21 +339,20 @@

How latency scales with concurrent connecti - - - - + + + - - - - - + + + + +
Setup p50 / p99, standalone1k10k100kSetup p50 / p991k subs10k subs
Socket.io + Redis adapter24 / 106 ms264 / 725 msTBD-A2a
Socket.io + CSR23 / 69 ms254 / 639 msTBD-A2a
uWS custom publish16 / 92 ms249 ms / 4-11 s p99TBD-A2a
AnyCable OSS17 / 55 ms252 / 895 msTBD-A2a
AnyCable Pro22 / 145 ms246 / 875 msTBD-A2a
Socket.io default24 / 106 ms264 / 725 ms
Socket.io + CSR23 / 69 ms254 / 639 ms
uWS topics16 / 92 ms249 ms / 4-11 s
AnyCable OSS17 / 55 ms252 / 895 ms
AnyCable Pro22 / 145 ms246 / 875 ms
-

† uWS p50 at 10k stays low (249 ms), but the long tail varies wildly across runs (p99 ranged 4.4-10.6 s across 4 samples). The cause is single-process broadcast: app.publish fans out to 10k subscribers in one event-loop iteration, so per-client TCP backpressure variation balloons the tail. Production uWS deployments would shard, which we haven't tested. 100k tier pending multi-shard bench infrastructure.

+

† uWS p50 at 10k stays low, but p99 varies 4.4-10.6 s across 4 samples: app.publish serializes through one writer, so per-client TCP backpressure inflates the tail. Production uWS would shard; not tested here.

@@ -390,12 +363,9 @@

How latency scales with concurrent connecti 100% delivery required. --}}
-

How many broadcasts per second the WS layer can sustain

-

- Same shape as production: each WS server runs as a separate service, the publisher is a separate process posting to its HTTP /_broadcast endpoint with concurrency 16. We push the target at 1M deliveries/sec (100 broadcasts/sec to 10K subscribers); the result is what each server actually sustains end-to-end with 100% delivery. -

+

Broadcast throughput

- Co-located publisher numbers (publisher and WS server share a Node event loop) get higher headlines but they require app code to live inside the WS process. That's the deploy-resilience tradeoff this page argues against, so those numbers aren't in this table. + Target 1M deliveries/sec to 10K subscribers, 100% delivery required. Publisher is a separate process posting to the WS server's HTTP /_broadcast at concurrency 16. The result is what each server sustains end-to-end.

@@ -429,10 +399,10 @@

How many broadcasts per second the WS layer

Whispers: client-to-client updates that bypass the backend

- Some realtime updates don't need the backend at all. Mouse cursors in a shared doc, typing indicators, presence pings: these go from one client to others through the WS server without ever touching your app. AnyCable supports this natively as a first-class primitive; Socket.io approximates it with rooms; uWS does it with topics but you build the access control yourself. + Cursors, typing indicators, presence pings. These travel client → WS server → peers without invoking your app. AnyCable ships it as a native primitive; Socket.io emulates with rooms; uWS with topics. The differences show up under load.

- Why whispers matter for category positioning. Whispers are how AnyCable competes in the collab-infrastructure category alongside Liveblocks, Yjs providers, and PartyKit, not just in the WS-server category. If your product has live cursors, shared selections, or any UI where one user's local state needs to fan out to peers without a server round-trip, this row matters more than raw throughput. Liveblocks built its entire business on this primitive being good enough to feel real-time. + This is where AnyCable competes with Liveblocks, Yjs providers, and PartyKit, not just with WS libraries. If your product has live cursors or shared selections, this row matters more than raw broadcast throughput.
@@ -468,13 +438,10 @@

Whispers: client-to-client updates that byp

How reliable is delivery?#

- Your users are not necessarily always connected to a high-speed fiber network. Micro disruptions are largely not noticeable for HTTP requests, but they are the deal breaker for real-time features — or how reliable and smooth they are going to be. Two things decide it: how often messages are lost during disruption, and how long the recovery window feels. Both come down to whether the protocol carries replay state. + Users disconnect. WiFi flips, tunnels open, networks switch. The realtime question is what happens to messages sent during those windows. Two things decide it: how many messages are lost and how long the recovery feels. Both come down to whether the protocol carries replay state.

- Default Socket.io and uWebSockets.js don't carry replay state — they lose ~13–14% of messages under simulated jitter (1-second TCP drops every ~15 s, the pattern WiFi handoffs produce). Socket.io with Connection state recovery fixes delivery to 100%, with constraints on the adapter and a ~8 s p99 replay tail. AnyCable also delivers 100%, clears the tail ~1.7 s sooner, and AnyCable Pro holds the same 10K reconnecting fleet on less than half of CSR's server memory. The data: -

-

- Default Socket.io is at-most-once; CSR adds replay (opt-in, with adapter constraints). AnyCable ships replay by default via reliable streams: per-stream history, epoch + offset, restart-survivable with NATS or Redis. + Default Socket.io is at-most-once and so is uWS: messages during a disconnect are gone. CSR adds replay (opt-in, adapter constraints). AnyCable ships reliable streams by default: per-stream history, epoch + offset, restart-survivable with NATS or Redis. Under simulated WiFi jitter (1 s TCP drops every ~15 s): AnyCable and CSR deliver 100%; default Socket.io and uWS lose ~13-14%.

@@ -618,10 +585,10 @@

What this breaks

Does the WS layer survive deploys?#

- Modern teams deploy multiple times a day. Realtime apps have a structural choice: the WebSocket layer lives inside the app process, or it runs as its own service. The choice you make at small scale becomes a load-bearing decision at growing scale. Below is what happens when the WS layer is co-located with the app and the app restarts on every deploy — default for Socket.io, Socket.io+CSR, and uWS, because all three are Node libraries. At small scale the reconnect storm is recoverable; past a certain N, it stops being recoverable. + Teams deploy multiple times a day. The structural choice: WS layer inside the app process, or its own service. Small-scale tradeoffs become load-bearing at growing N. Socket.io, +CSR, and uWS are all Node libraries — the WS layer restarts with the app. At small scale the reconnect storm recovers. Past a certain N it stops recovering.

- The test below is real railway redeploy events on a 1 vCPU / 0.5 GB box, recording how long the WS layer takes to come back. The architecture and the benchmark, side by side: + Real railway redeploy events on a 1 vCPU / 0.5 GB box, measuring how long the WS layer takes to come back:

{{!-- Architecture diagram — Socket.io's app+WS in one process @@ -703,7 +670,7 @@

Does the WS layer survive deploys

- Through 20K connections, recovery is graceful for the in-process WS setups. At 25K, recovery never completes — the post-redeploy reconnect storm OOMs the new container. CSR doesn't help: its in-memory adapter loses state on restart, and connections still drop. On a bigger box the cliff exists too, just at higher N. On the same hardware uWS pushes the catastrophic cliff to ~90K instead of ~25K, but recovery at 25K already takes 57 seconds, and at 90K it stops being recoverable. More RAM moves where the wall is; the wall is still there, because it's the reconnect storm itself — not the box size — that overwhelms the new process. Any setup with the WS layer as a separate service avoids this entirely — the WS layer doesn't restart, so there's nothing to recover from. AnyCable ships that shape by default; anyone scaling Socket.io / uWS in production ends up building it themselves. Methodology & per-N walkthrough: bench repo README. Background reading: thundering-herd safety tips. + Through 20K, recovery is graceful for in-process WS. At 25K, recovery never completes: the post-redeploy reconnect storm OOMs the new container. CSR doesn't help (its in-memory adapter loses state on restart). A bigger box just moves the cliff: uWS pushes catastrophic failure to ~90K, but recovery at 25K already takes 57 s. The wall isn't box size — it's the reconnect storm itself. Standalone WS avoids it entirely because the WS layer doesn't restart. Methodology: bench repo. Background: thundering-herd safety tips.

@@ -764,13 +731,19 @@

Does the WS layer survive deploys

How efficient is it to scale?#

- Your user base will grow, and your realtime layer needs to grow with it. The question isn't how cheap is this today — it's when we 10× users, does the bill or the box cliff before we have time to react? Cost predictability matters more than absolute cost: a $5 box that scales smoothly to 100K users beats a $499 plan that hits a wall at 50K. Three things decide it: how many idle connections one node holds (steady-state headroom), how the same node behaves during a reconnect storm (the avalanche from above), and useful throughput — messages per second × delivery rate, since raw msg/sec without delivery is vanity. + Two questions: how many idle connections one node holds, and how the same node behaves during a reconnect storm. Cost shape matters too: a flat-rate setup that scales to 100K beats a per-connection plan that walls at 50K.

- Same Railway box (32 vCPU / 32 GB), four setups, 1,000,000-connection idle target. Socket.io accepted 119,826: the single Node event loop saturates handshakes regardless of memory available. uWS held 1,018,366 on 5.45 GB (lightest wire, no replay broker). AnyCable Pro held 822,037 on 14.80 GB (the lightest setup that includes built-in replay, at ~18 KB per connection). On cost shape: AnyCable Pro is flat-rate ($1,490/yr, unlimited instances); Pusher is per-connection (Premium $499/mo for 10K, Growth $699/mo for 15K, Plus $899/mo for 20K, see 2026 plans); uWS and self-hosted Socket.io are box-priced. + Same Railway box (32 vCPU / 32 GB), 1M-connection idle target, single instance, no Redis/NATS backplane:

+
    +
  • Socket.io accepted 119,826 — the single Node event loop saturates handshakes regardless of available memory.
  • +
  • uWS held 1,018,366 on 5.45 GB. Lightest wire, no replay.
  • +
  • AnyCable Pro held 822,037 on 14.80 GB (~18 KB/conn). Lightest setup with built-in replay.
  • +
  • AnyCable OSS held 821,877 on 28.30 GB (~34 KB/conn).
  • +

- Single instance, no Redis or NATS backplane. Socket.io's idle wall is the event loop (saturates at ~120K). AnyCable OSS and Pro both reached ~822K connections in this run, with OSS at ~34 KB/conn (28.3 GB total) and Pro at ~18 KB/conn (14.8 GB total). uWS clears 1M at ~5.4 KB/conn without replay. Per-test setup and harness: bench repo README. + Cost shape: AnyCable Pro is flat-rate ($1,490/yr, unlimited instances). Pusher charges per connection (Premium $499/mo for 10K, Growth $699/mo for 15K, Plus $899/mo for 20K, 2026 plans). uWS and self-hosted Socket.io are box-priced.

@@ -846,13 +819,10 @@

How efficient is it to scale?

What does a clustered deployment look like?#

- Past one node, the question shifts from "how big can the single instance get?" to "what does adding more nodes actually buy?" Three things to measure: can a rolling deploy survive without dropping connections? (the test that motivates separate-process WS in the first place), how does total capacity scale with N nodes?, and does the cluster pub/sub layer impose a latency tax on broadcasts? -

-

- We use a 3-node cluster behind a load balancer, 10k connections steady. Capacity and 1-of-N-down behavior are pending A2d. We test the same 5 options: Socket.io (Redis adapter), Socket.io + CSR (Redis Streams), uWS, AnyCable OSS, and AnyCable Pro. + Past one node, the question shifts: can a rolling deploy survive without dropping connections? 3-node cluster behind a load balancer, 10k connections steady. Same 5 options: Socket.io (Redis adapter), Socket.io + CSR (Redis Streams), uWS, AnyCable OSS, AnyCable Pro. Capacity and 1-of-N-down behavior pending A2d.

- Caveat: uWS clustering and standalone are not off-the-shelf configurations. uWebSockets.js has no built-in cross-node clustering and no built-in HTTP publish endpoint. To benchmark uWS in standalone mode we wrote a custom publish API; to benchmark it clustered we wrote a custom pub/sub bridge (Redis-backed). The numbers reflect what those custom implementations achieve, marked with in the tables. Production teams who pick uWS for anything past one in-process node end up building the same two things themselves. This is part of the trade Socket.io and AnyCable spare you: both ship the multi-node story as a documented, supported configuration. + uWS multi-node is DIY. No built-in cross-node clustering, no built-in HTTP publish. We wrote a custom publish API and a custom Redis pub/sub bridge to test uWS at all. Production uWS teams end up building both themselves. Socket.io and AnyCable ship the multi-node story as documented config.

@@ -861,9 +831,9 @@

What does a clustered deploymen horizontal scaling + rolling deploy substitute for separate-process WS?" --}}
-

Rolling deploy: per-client disconnect window + message loss

+

Rolling deploy: per-client gap + message loss

- 3-node cluster, 10k connections steady, drain + restart nodes sequentially. The test measures what every connected user actually experiences during a deploy: how long their connection is broken, how many messages they miss in that window, and whether the surviving nodes can absorb the reconnect storm from the node being recycled. + 3-node cluster, 10k connections, drain + restart nodes in sequence. Measures what each user actually sees: connection gap, messages missed, whether surviving nodes absorb the reconnect storm.

@@ -896,9 +866,9 @@

Rolling deploy: per-client disconnect windo {{!-- Clustered capacity + avalanche (A2d) --}}
-

Clustered capacity and one-node-down avalanche

+

Clustered capacity and 1-of-N down

- Total capacity across 3 nodes (the realistic prod question, not single-instance), and what happens when 1 of 3 nodes fails: how fast do the survivors absorb the displaced load, and is anything lost in transit? + Total capacity across 3 nodes, and what happens when 1 of 3 fails: how fast survivors absorb the displaced load, what's lost in transit.

@@ -931,7 +901,7 @@

Clustered capacity and one-node-down avalan

Takeaway: when does in-process WS stop being viable?

- TBD-A2c/A2d narrative. The rolling-deploy and one-node-down tables above answer the structural question: does horizontal scaling + a rolling deploy give you acceptable UX, or do you need a separate-process WS service to deploy independently? Conclusion depends on numbers we don't have yet. Two plausible outcomes: (1) rolling deploy + Socket.io CSR is fine for many apps, in which case in-process stays a real option; (2) even with CSR, the disconnect storm + replay tail is rough enough at 10k+ that production teams end up wanting the WS layer separate, which is what AnyCable ships by default. + CSR + Redis Streams is the best in-process answer we measured: a ~1.5 s per-user freeze with 100% delivery (replay backfills) on every rolling deploy. Default Socket.io is worse (2 s freeze, 99.6% delivered, 2-3 lost per user). Standalone WS is the only shape where users don't notice the deploy at all: zero disconnects, zero reconnects, a 45-second publishing pause that affects what gets sent, not what gets received. CSR shrinks the gap; only architecture eliminates it.

From df6727ddc565af9fd6868390fbf4301b1785102f Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 21:14:26 +0100 Subject: [PATCH 46/57] =?UTF-8?q?Compare/Socket.io:=20editorial=20pass=202?= =?UTF-8?q?=20=E2=80=94=20tighter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cut walls of text on a 'what do I need to know' basis. Almost every footnote and the perf FAQ shortened: - TLDR intro: dropped 'doesn't want to sit still' / 'let's dance smoothly' - Whispers footnote: 8 sentences → 2 - Throughput footnote: 5 long sentences → 2 short ones - Reliability table footnote: 6 sentences → 2 - Avalanche table footnote: 4 sentences → 2 + 1 tiny - Capacity table footnote: 3 sentences → 1 - Clustered table footnote: 6 sentences → 2 short + footnotes - 'Pending A2d' footnote: rambling → punchy - Feature comparison footnote: 3 sentences → 2 - Jitter methodology blurb: half the words - Performance FAQ (both visible + JSON-LD): wall of text → 4 short bursts Headroom intro: removed the redundant bullet list (table below shows the same data more readably). ~30 net lines, ~12 lines shorter total. --- src/compare/socket-io/index.html | 40 +++++++++++--------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index c79369e..8cb3fec 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -88,10 +88,7 @@

- Your TS/JS application doesn't want to sit still: it needs WebSockets for all sorts of real-time messaging, streaming from LLMs, and collaborative features. What to choose? We ran tests to benchmark Socket.io, uWebSockets, and AnyCable based on 3 key factors: latency, reliability, and scalability. We strive to bring production-grade quality to our real-time features - if we want to dance, let's dance smoothly. -

-

- We ran all benchmarks on Railway, and the code is in this repo. + Five WebSocket setups under production-shaped conditions, scored on three rubrics: latency + throughput, reliability, scalability. Same hardware on Railway, reproducible from the open-source bench repo.

@@ -424,7 +421,7 @@

Whispers: client-to-client updates that byp AnyCable ProNative95.4%1.39 s5.20 s -

10K clients × 100 rooms (100 peers/room), 64-byte payload, 500 ms cadence (2 Hz, cursor-style), 30 s. Expected fan-out ~20 M deliveries (~660K/s sustained). AnyCable native delivered 11.2 M (99.3%) with the fastest p99 tail; uWS topics delivered all 18.2 M (100%) but its p99 tail is ~2× longer because app.publish serializes through one writer; Socket.io rooms emulation lost 96% under the same load because every whisper still hits the WS process to re-emit and Express can't keep up. AnyCable Pro tracks OSS in shape; the slightly lower delivery (95.4%) and longer p99 (5.2 s) are noise within this single sample.

+

10K clients in 100 rooms, 2 Hz whispers for 30 s. Socket.io rooms still routes every whisper through the WS process to re-emit — Express can't keep up. AnyCable and uWS bypass that path.

@@ -469,7 +466,7 @@

How reliable is delivery?

@@ -717,7 +714,7 @@

Does the WS layer survive deploys -

The split here isn't really library-vs-library — it's in-process vs out-of-process WS. Socket.io, Socket.io+CSR, and uWS all run the WS layer inside the Node app, so railway redeploy restarts it; CSR's in-memory adapter even loses replay state on restart. AnyCable (OSS and Pro) runs the WS layer as a separate Go binary; deploys touch only the app process, never the WS connections. 1 vCPU / 0.5 GB box, "recovery" = wall-clock until 95% of clients reconnect.

+

The split is in-process vs out-of-process WS. Socket.io, +CSR, and uWS run the WS layer inside the Node app — railway redeploy restarts it. AnyCable runs WS as a separate Go binary; app deploys never touch it. 1 vCPU / 0.5 GB box; "recovery" = wall-clock until 95% reconnected.

@@ -731,19 +728,10 @@

Does the WS layer survive deploys

How efficient is it to scale?#

- Two questions: how many idle connections one node holds, and how the same node behaves during a reconnect storm. Cost shape matters too: a flat-rate setup that scales to 100K beats a per-connection plan that walls at 50K. -

-

- Same Railway box (32 vCPU / 32 GB), 1M-connection idle target, single instance, no Redis/NATS backplane: + Two questions: how many idle connections one node holds, and how it behaves during a reconnect storm. Single instance, 32 vCPU / 32 GB Railway box, 1M-connection idle target, no backplane.

-
    -
  • Socket.io accepted 119,826 — the single Node event loop saturates handshakes regardless of available memory.
  • -
  • uWS held 1,018,366 on 5.45 GB. Lightest wire, no replay.
  • -
  • AnyCable Pro held 822,037 on 14.80 GB (~18 KB/conn). Lightest setup with built-in replay.
  • -
  • AnyCable OSS held 821,877 on 28.30 GB (~34 KB/conn).
  • -

- Cost shape: AnyCable Pro is flat-rate ($1,490/yr, unlimited instances). Pusher charges per connection (Premium $499/mo for 10K, Growth $699/mo for 15K, Plus $899/mo for 20K, 2026 plans). uWS and self-hosted Socket.io are box-priced. + Cost shape matters too. AnyCable Pro is flat-rate ($1,490/yr, unlimited instances). Pusher charges per connection ($499/mo for 10K, scaling with size, 2026 plans). uWS and self-hosted Socket.io are box-priced.

{{!-- Memory + CPU during the 1M Pro run, captured live @@ -858,7 +846,7 @@

Rolling deploy: per-client gap + message lo AnyCable Pro standalone, always0000% -

Embedded Socket.io: 10K clients across a 3-node rolling restart (Redis adapter). Every user disconnects once during the cycle (a ~2 s freeze) and loses 2-3 messages while reconnecting; cluster recovery 82 s, delivery rate 99.6%. Embedded Socket.io + CSR (Redis Streams adapter): 5K clients across the same 3-node rolling restart (CSR requires the Streams adapter; tested at 5K due to bench-runner memory limits). Per-user gap tightens to 1.5 s and delivery hits 100% because CSR replays missed messages on reconnect. Standalone Socket.io and AnyCable: same 10K clients, the publisher service restarts mid-test, WS layer untouched, zero connection disruption. uWS embedded test pending (requires custom pub/sub bridge). uWS has no native clustering or built-in HTTP publish API; both are custom code built for this benchmark. Standalone deploys disrupt publishing, not connections: clients hold the WS link through a 45-46 s publisher restart; broadcasts emitted during that window aren't held server-side, so anything the app would have published is simply not published. AnyCable Pro is mirrored from OSS (same architectural property). §CSR's replay sends missed messages on reconnect; per-user "lost" still counts the in-flight gap window, but the duplicates inflate delivery rate slightly past 100%.

+

Embedded: 10K clients (5K for CSR due to bench-runner memory), rolling restart of all 3 nodes. Standalone: only the publisher restarts; WS layer is untouched. uWS multi-node is DIY. Standalone deploys pause publishing for ~45 s but don't disrupt connections. §CSR's replay backfills on reconnect.

@@ -891,7 +879,7 @@

Clustered capacity and 1-of-N down

AnyCable ProTBD-A2dTBD-A2dTBD-A2dTBD-A2d -

Placeholders pending A2d. Ramp-up rate is the rate at which new connections can be accepted across the cluster, a real-world bottleneck during reconnect storms. uWS clustering requires a custom pub/sub layer; the benchmark reports what we built, with the caveat that production teams would need to build something similar.

+

Pending A2d. uWS clustering requires a custom pub/sub layer; production teams would build the same.

@@ -945,7 +933,7 @@

What you don't have to build

Monitoring
Admin UIAdmin UIDIYPrometheus & StatsD -

uWS solves Socket.io's wire-overhead problem — not its framework gaps. Every feature beyond raw transport is still DIY, on the same single Node process. AnyCable OSS and Pro share every feature in this table; Pro's differentiators are per-connection memory efficiency, the embedded broker, and commercial support.

+

uWS fixes Socket.io's wire overhead. Everything beyond raw transport is still DIY. AnyCable OSS and Pro share every feature here; Pro differentiates on memory, embedded broker, and commercial support.

@@ -1003,7 +991,7 @@

FAQ How does AnyCable compare on performance? - +
Three workloads on the same 32 vCPU / 32 GB Railway box, every option as a standalone WS service.

1M-connection idle: uWS 1,018,366 / 5.45 GB. AnyCable Pro 822,037 / 14.8 GB (lightest with built-in replay). AnyCable OSS 821,877 / 28.3 GB. Socket.io caps at 119,826 (Node event loop saturates handshakes).

10K reconnecting clients under WiFi jitter: AnyCable and Socket.io+CSR deliver 100% (p99 ~6 s and ~8 s replay tails). Default Socket.io and uWS lose ~13-14%.

Broadcast throughput at 1M deliv/sec target (HTTP pool=16): uWS 332K, AnyCable OSS 207K, Pro 205K, Socket.io+CSR 62K, default 57K. AnyCable OSS/Pro hold the lowest p99 (~3.1 s). Reproduce.

@@ -1108,7 +1096,7 @@

Run AnyCable in your Node app

"name": "How does AnyCable compare on performance?", "acceptedAnswer": { "@type": "Answer", - "text": "Three workloads on the same 32 vCPU / 32 GB Railway box, every option as a standalone WS service. (1) 1M-connection idle target: uWebSockets.js holds 1,018,366 on 5.45 GB; AnyCable Pro holds 822,037 on 14.80 GB; open-source AnyCable holds 821,877 on 28.30 GB; Socket.io caps at 119,826 because its single Node event loop saturates handshakes regardless of memory. (2) 10K reconnecting clients under jitter (WiFi-drop pattern): AnyCable and Socket.io+CSR deliver 100% (~6 s p99 replay tail for AnyCable, ~8 s for CSR); default Socket.io and uWS lose ~13–14% of messages, both being at-most-once. (3) Standalone broadcast throughput at 1M deliveries/sec target, external HTTP publisher (pool=16): uWS 332K deliv/sec, AnyCable OSS 207K, AnyCable Pro 205K, Socket.io+CSR 62K, default Socket.io 57K. uWS leads on raw fan-out, AnyCable OSS and Pro share the same p99 tail (~3.1 s), Socket.io default's p99 balloons to 15 s under pool concurrency. Source: github.com/irinanazarova/anycable-socketio-benchmarks." + "text": "Three workloads, 32 vCPU / 32 GB Railway box, every option standalone. Idle 1M target: uWS 1,018,366 / 5.45 GB; AnyCable Pro 822,037 / 14.8 GB; AnyCable OSS 821,877 / 28.3 GB; Socket.io caps at 119,826. WiFi jitter at 10K: AnyCable and CSR deliver 100% (p99 ~6 s and ~8 s); default Socket.io and uWS lose ~13-14%. Broadcast throughput at 1M deliv/sec target (HTTP pool=16): uWS 332K, AnyCable OSS 207K, Pro 205K, Socket.io+CSR 62K, default 57K; AnyCable holds the lowest p99 (~3.1 s). Source: github.com/irinanazarova/anycable-socketio-benchmarks." } }, { From 49c191727c46d403cc0b11c67dd1cc79fe667073 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 21:17:20 +0100 Subject: [PATCH 47/57] Compare/Socket.io: restore human voice in intros MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per editorial direction: cut padding, keep personality. Restored: - TLDR: 'Your TS/JS application doesn't want to sit still', 'if we want to dance, let's dance smoothly' (the user's signature voice) - Section 1: 'Latency is the floor of what realtime can feel like' - Reliability: 'Your users aren't always on a high-speed fiber network' - Headroom: 'Your user base will grow... when we 10× users, does the bill or the box cliff before we have time to react?' - Deploys: 'Modern teams deploy multiple times a day' Also tightened a couple more dense paragraphs (the architecture takeaway between SVG and code block, the avalanche prose). --- src/compare/socket-io/index.html | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 8cb3fec..618d96b 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -88,7 +88,10 @@

- Five WebSocket setups under production-shaped conditions, scored on three rubrics: latency + throughput, reliability, scalability. Same hardware on Railway, reproducible from the open-source bench repo. + Your TS/JS application doesn't want to sit still: it needs WebSockets for all sorts of real-time messaging, streaming from LLMs, and collaborative features. What to choose? We benchmarked Socket.io, uWebSockets, and AnyCable across three rubrics: latency + throughput, reliability, and scalability. If we want to dance, let's dance smoothly. +

+

+ Same hardware on Railway, reproducible from the open-source bench repo.

@@ -435,10 +438,10 @@

Whispers: client-to-client updates that byp

How reliable is delivery?#

- Users disconnect. WiFi flips, tunnels open, networks switch. The realtime question is what happens to messages sent during those windows. Two things decide it: how many messages are lost and how long the recovery feels. Both come down to whether the protocol carries replay state. + Your users aren't always on a high-speed fiber network. Micro disruptions are mostly invisible to HTTP, but they're the deal-breaker for realtime. Two things decide how it feels: how often messages are lost and how long the recovery window drags. Both come down to whether the protocol carries replay state.

- Default Socket.io is at-most-once and so is uWS: messages during a disconnect are gone. CSR adds replay (opt-in, adapter constraints). AnyCable ships reliable streams by default: per-stream history, epoch + offset, restart-survivable with NATS or Redis. Under simulated WiFi jitter (1 s TCP drops every ~15 s): AnyCable and CSR deliver 100%; default Socket.io and uWS lose ~13-14%. + Default Socket.io is at-most-once; so is uWS. CSR adds replay (opt-in, adapter constraints). AnyCable ships reliable streams by default: per-stream history, epoch + offset, restart-survivable with NATS or Redis. Under simulated WiFi jitter (1 s TCP drops every ~15 s): AnyCable and CSR deliver 100%; default Socket.io and uWS lose ~13-14%.

@@ -582,10 +585,7 @@

What this breaks

Does the WS layer survive deploys?#

- Teams deploy multiple times a day. The structural choice: WS layer inside the app process, or its own service. Small-scale tradeoffs become load-bearing at growing N. Socket.io, +CSR, and uWS are all Node libraries — the WS layer restarts with the app. At small scale the reconnect storm recovers. Past a certain N it stops recovering. -

-

- Real railway redeploy events on a 1 vCPU / 0.5 GB box, measuring how long the WS layer takes to come back: + Modern teams deploy multiple times a day. Socket.io, +CSR, and uWS are Node libraries — the WS layer restarts every time the app does. At small N the reconnect storm recovers. Past a certain N it stops recovering. The test: real railway redeploy on a 1 vCPU / 0.5 GB box.

{{!-- Architecture diagram — Socket.io's app+WS in one process @@ -642,7 +642,7 @@

Does the WS layer survive deploys

- The takeaway is architectural, not vendor-specific: once you're past a few thousand persistent connections, the WS layer should be its own service. Socket.io / uWS can be deployed that way with extra engineering — multiple Node processes, a Redis-backed adapter, sticky sessions, careful deploy choreography. AnyCable defaults to it: anycable-go is a separate Go binary; deploying the app process leaves it untouched. The 0 s recovery row in the table below is what that buys you. + Past a few thousand connections, the WS layer should be its own service. Socket.io / uWS can be deployed that way (extra Node processes, Redis adapter, sticky sessions, deploy choreography). AnyCable defaults to it. The 0 s recovery row below is what that buys.

@@ -667,7 +667,7 @@

Does the WS layer survive deploys

- Through 20K, recovery is graceful for in-process WS. At 25K, recovery never completes: the post-redeploy reconnect storm OOMs the new container. CSR doesn't help (its in-memory adapter loses state on restart). A bigger box just moves the cliff: uWS pushes catastrophic failure to ~90K, but recovery at 25K already takes 57 s. The wall isn't box size — it's the reconnect storm itself. Standalone WS avoids it entirely because the WS layer doesn't restart. Methodology: bench repo. Background: thundering-herd safety tips. + Through 20K, recovery is graceful. At 25K, recovery never completes — the reconnect storm OOMs the new container. CSR doesn't help (in-memory adapter loses state on restart). A bigger box just moves the cliff: uWS pushes it to ~90K, but at 25K it's already 57 s. Standalone WS avoids the cliff because the WS layer doesn't restart. Methodology · thundering-herd reading.

@@ -728,7 +728,7 @@

Does the WS layer survive deploys

How efficient is it to scale?#

- Two questions: how many idle connections one node holds, and how it behaves during a reconnect storm. Single instance, 32 vCPU / 32 GB Railway box, 1M-connection idle target, no backplane. + Your user base will grow, and your realtime layer needs to grow with it. The question isn't how cheap is this today: it's when we 10× users, does the bill or the box cliff before we have time to react? Two things to measure: how many idle connections one node holds, and how it behaves during a reconnect storm. Single instance, 32 vCPU / 32 GB Railway box, 1M-connection idle target.

Cost shape matters too. AnyCable Pro is flat-rate ($1,490/yr, unlimited instances). Pusher charges per connection ($499/mo for 10K, scaling with size, 2026 plans). uWS and self-hosted Socket.io are box-priced. From be3daccb0a2456d66385ef9ae689884097c673d9 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Wed, 3 Jun 2026 21:24:18 +0100 Subject: [PATCH 48/57] Compare/Socket.io: headline conclusions on every data table + weave dance metaphor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each compare-frame now leads with a 1-line takeaway that pairs with the left-side H3. Sans-serif, weight 600, 15px — visually a sibling of the H3 subtitle. Style added in compare-spine.scss as .compare-data-table__headline. 8 tables got headlines: - Latency: 'AnyCable OSS holds the lowest p99 tail at 10k...' - Throughput: 'uWS leads on raw deliveries/sec; AnyCable keeps up...' - Whispers: 'Socket.io rooms lose 96%...' - Reliability: 'Replay protocols deliver 100%...' - Avalanche: 'Past 25K, in-process WS can't recover...' - Capacity: 'uWS holds the most idle connections per byte...' - Clustered deploy: 'Embedded WS makes every user skip a beat...' - Capacity (pending A2d): brief placeholder - Feature comparison: 'Socket.io and uWS give you transport...' Wove dance metaphor through several intros (Irina's signature voice): - Latency: 'the tempo your app dances to' - Reliability: 'a missed beat in the music' - Deploys: 'Every deploy is a beat: does the WS layer skip it?' - Clustered table headline: 'every user skip a beat' - Clustered takeaway: 'CSR shrinks the missed beat. Only architecture eliminates it.' --- src/compare/socket-io/index.html | 17 +- src/modules/blocks/compare-spine.scss | 281 +++++++++++++++++++------- 2 files changed, 221 insertions(+), 77 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 618d96b..fdbcfbc 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -320,7 +320,7 @@

Note on architecture

How fast is it?#

- Latency is the floor of what realtime can feel like. Two things to measure: roundtrip latency (one publisher to all subscribers) and broadcast throughput (how many messages per second the server can fan out before that latency starts inflating). All in the standalone shape: WS server as a separate process, publisher posting over HTTP. + Latency is the floor of what realtime can feel like — the tempo your app dances to. Two things to measure: roundtrip latency (one publisher to all subscribers) and broadcast throughput (how many messages per second the server can fan out before that latency starts inflating). All in the standalone shape: WS server as a separate process, publisher posting over HTTP.

@@ -336,6 +336,7 @@

Roundtrip latency

+

AnyCable OSS holds the lowest p99 tail at 10k. uWS's tail explodes when one writer fans out to that many subscribers.

@@ -370,6 +371,7 @@

Broadcast throughput

+

uWS leads on raw deliveries/sec; AnyCable OSS and Pro keep up while holding the smoothest tail. Socket.io default's tail balloons under HTTP-pool concurrency.

@@ -407,6 +409,7 @@

Whispers: client-to-client updates that byp
+

Socket.io rooms lose 96% of whispers at cursor-style load: every whisper still goes back through the WS process. AnyCable and uWS skip that hop.

@@ -438,7 +441,7 @@

Whispers: client-to-client updates that byp

How reliable is delivery?#

- Your users aren't always on a high-speed fiber network. Micro disruptions are mostly invisible to HTTP, but they're the deal-breaker for realtime. Two things decide how it feels: how often messages are lost and how long the recovery window drags. Both come down to whether the protocol carries replay state. + Your users aren't always on a high-speed fiber network. Micro disruptions are mostly invisible to HTTP, but they're the deal-breaker for realtime: a missed beat in the music. Two things decide how it feels: how often messages are lost and how long the recovery window drags. Both come down to whether the protocol carries replay state.

Default Socket.io is at-most-once; so is uWS. CSR adds replay (opt-in, adapter constraints). AnyCable ships reliable streams by default: per-stream history, epoch + offset, restart-survivable with NATS or Redis. Under simulated WiFi jitter (1 s TCP drops every ~15 s): AnyCable and CSR deliver 100%; default Socket.io and uWS lose ~13-14%. @@ -474,6 +477,7 @@

How reliable is delivery?
+

Replay protocols (AnyCable, CSR) deliver 100%. Default Socket.io and uWS lose 13-14% on every WiFi blip.

@@ -585,7 +589,7 @@

What this breaks

Does the WS layer survive deploys?#

- Modern teams deploy multiple times a day. Socket.io, +CSR, and uWS are Node libraries — the WS layer restarts every time the app does. At small N the reconnect storm recovers. Past a certain N it stops recovering. The test: real railway redeploy on a 1 vCPU / 0.5 GB box. + Modern teams deploy multiple times a day. Every deploy is a beat: does the WS layer skip it? Socket.io, +CSR, and uWS are Node libraries — the WS layer restarts every time the app does. At small N the reconnect storm recovers. Past a certain N it stops recovering. The test: real railway redeploy on a 1 vCPU / 0.5 GB box.

{{!-- Architecture diagram — Socket.io's app+WS in one process @@ -672,6 +676,7 @@

Does the WS layer survive deploys

+

Past 25K, in-process WS can't recover from its own restart. Standalone WS doesn't restart, so there's nothing to recover from.

@@ -736,6 +741,7 @@

How efficient is it to scale?
+

uWS holds the most idle connections per byte. AnyCable Pro is the lightest setup that includes built-in replay. Socket.io tops out at ~120K.

@@ -826,6 +832,7 @@

Rolling deploy: per-client gap + message lo
+

Embedded WS makes every user skip a beat on every deploy. Standalone leaves the dance floor untouched.

@@ -861,6 +868,7 @@

Clustered capacity and 1-of-N down

+

Cluster capacity and 1-of-N survivor behavior. Pending A2d.

@@ -889,7 +897,7 @@

Clustered capacity and 1-of-N down

Takeaway: when does in-process WS stop being viable?

- CSR + Redis Streams is the best in-process answer we measured: a ~1.5 s per-user freeze with 100% delivery (replay backfills) on every rolling deploy. Default Socket.io is worse (2 s freeze, 99.6% delivered, 2-3 lost per user). Standalone WS is the only shape where users don't notice the deploy at all: zero disconnects, zero reconnects, a 45-second publishing pause that affects what gets sent, not what gets received. CSR shrinks the gap; only architecture eliminates it. + CSR + Redis Streams is the best in-process answer we measured: every user skips ~1.5 s on every deploy, but the replay backfills the missed beats (100% delivery). Default Socket.io is worse (~2 s freeze, 99.6% delivered, 2-3 messages lost per user). Standalone WS is the only shape where users don't notice the deploy at all: zero disconnects, zero reconnects, a 45-second publishing pause that affects what gets sent, not what gets received. CSR shrinks the missed beat. Only architecture eliminates it.

@@ -911,6 +919,7 @@

What you don't have to build

+

Socket.io and uWS give you transport. AnyCable gives you the whole realtime framework: reliability, presence, auth, deploy resilience.

diff --git a/src/modules/blocks/compare-spine.scss b/src/modules/blocks/compare-spine.scss index 711885a..92bdc1d 100644 --- a/src/modules/blocks/compare-spine.scss +++ b/src/modules/blocks/compare-spine.scss @@ -1,28 +1,78 @@ // ===================================================================== -// Compare-page SPINE — design language proposal -// Scope: anycable.io/compare/socket-io and any sibling comparison pages. +// Compare-page SPINE — design system for /compare/* pages +// ===================================================================== +// +// QUICK REFERENCE for authors (writing or extending a compare page): +// +// See the live style index at: /compare/socket-io/style/ +// (Every block authors should reach for, rendered + with HTML.) +// +// Activation: add `c-spine` to the page's
+// so spine rules apply. +// +// ─── DESIGN TOKENS (CSS custom properties on .c-spine) ────────────── +// --c-font-display / --c-font-body Stem +// --c-font-mono Martian Mono +// --c-fs-display / -section / -sub / -body / -meta / -caption / -label +// --c-space-section / -block / -sub / -para / -bullet (8px scale) +// --c-prose-max / -prose-max-full prose width caps (580 / 680 px) +// --c-paper / -panel / -code-bg / -code-border surfaces +// --c-text / -text-meta / -text-quiet / -text-faint text grays +// --c-accent / -best / -worst accents +// --c-code-key / -str / -com / -err syntax tokens +// --c-radius 8px +// +// ─── LAYOUT (BEM) ─────────────────────────────────────────────────── +// .compare-rubric > __wrapper > __section > __content + __media +// .compare-rubric__content--full single-column section +// .compare-rubric__media--sticky pins media during scroll +// .compare-rubric__media--align-top align media to prose top +// +// ─── TYPOGRAPHY (use as class on element) ─────────────────────────── +// .compare-hero__title page H1 (display sized, +.c-vs span) +// .compare-rubric__title section H2 +// .compare-rubric__subtitle sub-section H3 +// .compare-prose body paragraph (max 580px) +// ol.compare-prose numbered list with mono 01/02/03 markers +// .compare-bullet-list bulleted list with red dot +// .c-vs typographic "vs" operator (inside H1) // -// Status: iteration partial. Scoped under .c-spine so it can be previewed -// on /compare/socket-io/style/ without disturbing the existing styles -// in compare.scss. Once approved, lift the scope and let the c-* roles -// govern .compare-page (and retire the duplicates in compare.scss). +// ─── RIGHT-SIDE ARTIFACTS (all share #fafafa frame) ───────────────── +// .compare-code-block naked light-tinted code +// .compare-code-tabs tabbed code with multiple panels +// .compare-frame container for a data table +// .compare-data-table transparent table inside .compare-frame +// .compare-arch-diagram SVG diagram in a framed figure +// .compare-bench-chart ASCII chart in a framed figure // -// What's in here: -// 1. Tokens (CSS custom properties + Sass vars) for the five roles: -// typography / spacing / surface / accent / radius. -// 2. Base role classes — one per typographic voice, no more. -// Display .c-display, Section .c-section, Sub .c-sub, Label .c-label, -// Body .c-prose, Meta .c-meta, Caption .c-caption. -// 3. The lab-notebook signatures: -// .c-label bracketed mono uppercase, [ BENCH 01 ] style -// .c-vs mono operator-style "vs", accent red -// .c-figure labelled figure with caption below -// .c-code light tinted code, muted syntax -// 4. Layout primitives — .c-rubric (two-col), prose col capped 580px. -// 5. Components — .c-table, .c-callout, .c-card, .c-cta. +// ─── ASIDES + CARDS ───────────────────────────────────────────────── +// .compare-callout accent-red left-rule aside +// .compare-hero-cards top results strip (three columns) +// .compare-impact-cards feature/disqualification grid +// .compare-try-card link card with eyebrow + title + desc +// .compare-quote-card customer quote with byline // -// Tokens are also exposed as CSS custom properties on .c-spine so the -// style index can demo overrides in devtools without recompiling Sass. +// ─── INLINE UTILITIES ─────────────────────────────────────────────── +// t-mute / t-tiny / t-meta / t-strong / t-accent / t-num text qualifiers +// t-eyebrow mono label +// c-key / c-str / c-com / c-err code syntax +// is-best / is-worst / is-na table cell state +// is-row-best / is-row-worst full-row tint +// +// ─── RULES OF THE ROAD ────────────────────────────────────────────── +// 1. No inline style="" on content elements. If you need a pattern, +// add it to this spine (or the style index) first. +// 2. No magic numbers. Use the spacing scale (--c-space-*) and +// the 8px tokens (4/8/12/16/24/32) for any layout work. +// 3. Right-side artifacts must use the .compare-frame / __pre / __code +// surface. Don't paint your own light box. +// 4. Brand red is reserved for: links, "vs", winners (data), eyebrows. +// Don't use it as decoration. +// +// ─── ARCHITECTURE ─────────────────────────────────────────────────── +// Loaded after compare.scss in src/index.scss so overrides win on +// specificity ties. Rules scoped under .c-spine so they only apply +// to pages that opt in. // ===================================================================== @import '../variables.scss'; @@ -826,7 +876,7 @@ } } - // ---- TL;DR: editorial ABSTRACT block with double-rule ------------- + // ---- TL;DR: editorial abstract block with double-rule ------------- // Was a centered card with border. Now: full-width band with hairline // rule top and bottom, label-style header in mono, prose flush. .compare-tldr { @@ -850,16 +900,8 @@ margin: 0 0 24px; display: inline-block; - &::before { - content: '[ '; - color: var(--c-text-faint); - font-weight: 400; - } - &::after { - content: ' ]'; - color: var(--c-text-faint); - font-weight: 400; - } + // Brackets removed; the mono-uppercase styling alone signals + // "this is a label." .heading-anchor { display: none; @@ -894,17 +936,6 @@ font-size: var(--c-fs-label); letter-spacing: var(--c-tracking-label); color: var(--c-text-meta); - - &::before { - content: '[ '; - color: var(--c-text-faint); - font-weight: 400; - } - &::after { - content: ' ]'; - color: var(--c-text-faint); - font-weight: 400; - } } @include mediaMax($mobile) { @@ -1198,12 +1229,15 @@ } // ---- Callout: minimal, label-led --------------------------------- + // Default margins absorb the ad-hoc "margin-top: 16px" pattern that + // authors were inlining. A callout following prose gets enough air; + // a callout starting a block does too. .compare-callout { background: transparent; border: none; border-left: 2px solid var(--c-accent); padding: 8px 0 8px 20px; - margin: 0 0 32px; + margin: 16px 0 32px; font-size: 15px; line-height: 1.55; color: var(--c-text-meta); @@ -1213,9 +1247,19 @@ color: var(--c-text); font-weight: 600; } + + // Lists inside callouts get the same treatment as bullet lists, + // already at the right size for the callout body text. + .compare-bullet-list { + margin: 8px 0 0; + + li { + margin-bottom: 8px; + } + } } - // ---- Utility eyebrows: bracketed mono labels --------------------- + // ---- Utility eyebrows: mono uppercase labels (no brackets) ------- .t-eyebrow { font-family: var(--c-font-mono); font-size: var(--c-fs-label); @@ -1224,17 +1268,6 @@ font-weight: 500; text-transform: uppercase; color: var(--c-text-meta); - - &::before { - content: '[ '; - color: var(--c-text-faint); - font-weight: 400; - } - &::after { - content: ' ]'; - color: var(--c-text-faint); - font-weight: 400; - } } // ---- Frame, chart, diagram: shared container conventions --------- @@ -1293,12 +1326,38 @@ } // ---- Frame: shared container for data tables, same treatment as code + // `width: 100%` is needed because the parent media column is a flex + // container with `align-items: center`, which would otherwise let + // the frame keep its content's natural width (and overflow the col). + // `overflow-x: auto` makes the inner table scrollable when its + // content needs more than the column allows. Scrollbar is forced + // visible (not auto-hidden) so the user has affordance. .compare-frame { + width: 100%; background: var(--c-code-bg); border: 1px solid var(--c-code-border); border-radius: var(--c-radius); - overflow: hidden; + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; padding: 0; + + // Firefox + scrollbar-width: thin; + scrollbar-color: var(--c-text-faint) transparent; + + // WebKit (Chrome, Safari) — show a slim, always-visible scrollbar. + &::-webkit-scrollbar { + height: 8px; + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: var(--c-text-faint); + border-radius: 4px; + } + &::-webkit-scrollbar-track { + background: var(--c-code-border); + } } // Data-table footnotes that follow a .compare-frame are part of the @@ -1312,6 +1371,20 @@ max-width: 880px; } + // Headline conclusion above every data table — pairs with the left-side + // H3 subtitle. The H3 frames the question; the headline gives the + // takeaway. Sans-serif and weighted to feel like a sibling of the H3. + .compare-data-table__headline { + font-family: var(--c-font-display, Stem, Arial, sans-serif); + font-size: 15px; + font-weight: 600; + line-height: 1.4; + color: var(--c-text); + letter-spacing: -0.005em; + margin: 0 0 14px; + max-width: 880px; + } + // ---- Kill stray inline text-align: center on headings/content ---- // FAQ and try-it sections had `style="text-align: center"` baked in. // The lab-notebook is left-anchored; override any inline center. @@ -1324,11 +1397,13 @@ // ---- Visible markers on inline
    prose lists ------------------ // Global reset killed list markers; add them back as mono numbers // in accent red so the 3-question / migration / checklist items - // have proper hierarchy. + // have proper hierarchy. Absorbs the inline `padding-left: 24px; + // margin-top: -8px` pattern authors were adding by hand. ol.compare-prose, .compare-rubric__content > ol { list-style: none; padding-left: 0; + margin: 0 0 var(--c-space-block); counter-reset: prose-item; > li { @@ -1336,6 +1411,7 @@ position: relative; padding-left: 36px; margin-bottom: 12px; + line-height: 1.55; &::before { content: counter(prose-item, decimal-leading-zero); @@ -1400,26 +1476,34 @@ // ---- Data tables: sit inside .compare-frame, no own chrome ------- // The frame provides the bg + border + radius. Table itself is // transparent so the unified light-#fafafa surface reads as a single - // artifact, like the code block. + // artifact, like the code block. Tighter padding + smaller body + // font lets dense tables fit in the 50% media column without + // horizontal overflow. .compare-data-table { background: transparent; border: none; border-radius: 0; - font-size: 15px; + font-size: 13px; th { font-family: var(--c-font-mono); - font-size: var(--c-fs-label); + font-size: 11px; + line-height: 1.3; letter-spacing: var(--c-tracking-label); color: var(--c-text-meta); font-weight: 500; background: transparent; border-bottom: 1px solid var(--c-code-border); + padding: 12px 10px; + // Allow headers to wrap so dense tables fit in the 50% column. + // The original compare.scss forced `white-space: nowrap`. + white-space: normal; } td { font-variant-numeric: tabular-nums; border-bottom: 1px solid var(--c-code-border); + padding: 10px 10px; } tr:last-child td { @@ -1488,33 +1572,82 @@ } } - // ---- Quote card: editorial framing ------------------------------- + // ---- Quote card: editorial blockquote, not a card --------------- + // Was double-framed (the HTML uses both .compare-frame + .compare-quote-card + // on the same element). Stripping the container box entirely: the quote + // stands on its own with a left accent rule. Eyebrow + display-sized + // quote + byline + integrated footer, no box chrome. .compare-quote-card { - background: var(--c-paper); - border: 1px solid var(--c-border); - border-radius: var(--c-radius); + background: transparent; + border: none; + border-radius: 0; + padding: 0 0 0 24px; + border-left: 3px solid var(--c-accent); &__eyebrow { + display: block; font-family: var(--c-font-mono); font-size: var(--c-fs-label); letter-spacing: var(--c-tracking-label); text-transform: uppercase; + color: var(--c-accent); + margin-bottom: 20px; + } + + &__text { + font-family: var(--c-font-display); + font-size: clamp(20px, 2vw, 24px); + font-weight: 500; + font-style: normal; + line-height: 1.35; + letter-spacing: -0.01em; + color: var(--c-text); + margin: 0 0 24px; + padding: 0; + quotes: none; + } + + &__byline { + font-size: 14px; + line-height: 1.5; color: var(--c-text-meta); + margin-bottom: 24px; - &::before { - content: '[ '; - color: var(--c-text-faint); - font-weight: 400; + strong { + color: var(--c-text); + font-weight: 600; } - &::after { - content: ' ]'; - color: var(--c-text-faint); - font-weight: 400; + } + + &__footer { + padding-top: 20px; + border-top: 1px solid var(--c-border); + font-size: 13px; + line-height: 1.6; + color: var(--c-text-quiet); + + strong { + color: var(--c-text-meta); + font-weight: 600; } } } + // When .compare-quote-card sits ON a .compare-frame element (current + // HTML has both classes on one div), the quote-card styling above + // must beat the frame's container styling. These overrides ensure + // no box-in-box visual collision. + .compare-frame.compare-quote-card { + background: transparent; + border: none; + border-left: 3px solid var(--c-accent); + border-radius: 0; + overflow: visible; + } + // ---- Try card: light surface, mono label ------------------------- + // The eyebrow inside a try-card gets default spacing so authors + // don't need to inline `style="margin-bottom: 8px"` on it. .compare-try-card { background: var(--c-paper); border: 1px solid var(--c-border); @@ -1527,6 +1660,8 @@ .t-eyebrow { font-family: var(--c-font-mono); color: var(--c-text-meta); + display: block; + margin-bottom: 8px; } } From c757055d363147888bb91897bc418b0a04eb739f Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Thu, 4 Jun 2026 00:34:12 +0100 Subject: [PATCH 49/57] Compare/Socket.io: feedback-round restructure Hero card 3 swapped from deploy-impact to memory-per-connection footprint (Socket.io ~52 KB, AnyCable Pro 18 KB, uWS 5.4 KB) since the deploy-impact angle is now better-served by the avalanche evidence inside Note on architecture. Latency table highlights the suspect uWS topics 10K p99 (4-11 s) in red and bolds the range so the variance shows. Whispers data replaced with the clean 1K x 10 rooms (100 peers/room) numbers: AnyCable Pro 100 percent + 64 ms p99, OSS 100 percent + 140 ms p99, uWS 100 percent + 21 ms p99, Socket.io rooms 64 percent + 9.7 s p99. The 10K dataset measured bench-runner saturation, not server fanout, so it has been replaced. Footnote spells out the methodology. Reliability section drops the per-client missing-seqs example block and leads with the data table; methodology link folded into the footnote. Avalanche evidence (5K to 25K, the at-25K cliff for in-process WS, 0 s recovery for AnyCable) moved into Note on architecture as a second row. The standalone 'Does the WS layer survive deploys?' section deleted, since its key content duplicated the architecture diagram and timeline already covered there. Headroom section removes the Pusher and cost paragraph and the ASCII memory/CPU charts. Table now has a CPU peak column (9.8 percent OSS, 7.9 percent Pro on the 1M idle test). Entire 'What does a clustered deployment look like?' section deleted, along with its nav link and the stale TBD comments. Page meta description rewritten to drop the 100k tier and clustered test references. color-scheme: light added to root html so OS dark mode does not bleed through native form controls and scrollbars. --- src/compare/socket-io/index.html | 524 +++++---------------- src/compare/socket-io/style/index.html | 623 +++++++++++-------------- src/modules/blocks/compare-spine.scss | 284 +++++++++-- src/modules/common.scss | 4 + 4 files changed, 650 insertions(+), 785 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index fdbcfbc..43bb601 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -1,6 +1,6 @@ - {{> dochead pageTitle="AnyCable vs Socket.io vs uWebSockets.js | Benchmarking WebSocket infrastructure for JS/TS apps" pageDescription="A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions: how fast (roundtrip latency at 1k/10k/100k and broadcast throughput, including whispers), how reliable (message delivery under WiFi-drop jitter), and how efficient to scale (single-node load test plus clustered deploy-impact and avalanche tests). Tested in both embedded and standalone topologies. All numbers reproducible from the open-source benchmark repo." pageUrl="https://anycable.io/compare/socket-io"}} + {{> dochead pageTitle="AnyCable vs Socket.io vs uWebSockets.js | Benchmarking WebSocket infrastructure for JS/TS apps" pageDescription="A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions: how fast (roundtrip latency at 1k/10k and broadcast throughput, including whispers), how reliable (message delivery under WiFi-drop jitter), and how efficient to scale (single-node load test with avalanche behavior at deploy). Tested in both embedded and standalone topologies. All numbers reproducible from the open-source benchmark repo." pageUrl="https://anycable.io/compare/socket-io"}} @@ -98,7 +98,6 @@

    01 · How fast 02 · How reliable 03 · How efficient - 04 · Scale-out Run in your stack FAQ @@ -121,9 +120,6 @@

    What we compare - Server-side broadcast code, side by side. Production-typical; the bench harness mirrors these closely. -

    @@ -225,11 +221,11 @@

    What we compare

    Note on architecture#

    - Production realtime needs the WS server as a standalone service, not embedded in your Node app. Embedded Socket.io/uWS freeze every user for ~2 seconds and lose messages on every app deploy. That's what the rest of the page compares: the WS layer as a separate process. + Production realtime needs the WS server as a standalone service, not embedded in your Node app. Embedded Socket.io/uWS freeze every user for >2 seconds and lose messages on every app deploy. That's what the rest of the page compares: the WS layer as a separate process.

    What we tested: 3-node cluster, embedded Socket.io + Redis, rolling deploy (redeploy each node in turn, wait for it back, move on).

    -

    Result: every user sees a ~2 s freeze when their node restarts. 99.6% delivered. 2-3 lost per user during the gap.

    +

    Result: every user sees a >2 s freeze on rolling deployment when their node restarts. 99.6% delivered. 2-3 lost per user during the gap.

    + + {{!-- Evidence for the architecture argument: avalanche test. + Moved here from the standalone "Does the WS layer survive + deploys?" section since it's making the architecture point. --}} +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ClientsSocket.io recovery (also +CSR, uWS)ReconnectedAnyCable OSS & Pro
5,0004.5 s100%0 s
10,0003.9 s100%0 s
15,0005.8 s98.5% (224 lost)0 s
20,0008.0 s96.2% (753 lost)0 s
25,000never0% (all 25K lost)0 s
+

Socket.io, +CSR, and uWS run the WS layer inside the Node app, so railway redeploy restarts it. Past ~20K connections the reconnect storm OOMs the new container. A bigger box just moves the cliff: uWS pushes it to ~90K. AnyCable runs WS as a separate Go binary; app deploys never touch it.

+
+
+

@@ -346,14 +401,14 @@

Roundtrip latency

- Socket.io default24 / 106 ms264 / 725 ms - Socket.io + CSR23 / 69 ms254 / 639 ms - uWS topics16 / 92 ms249 ms / 4-11 s - AnyCable OSS17 / 55 ms252 / 895 ms - AnyCable Pro22 / 145 ms246 / 875 ms + Socket.io default24 / 106 ms264 / 725 ms + Socket.io + CSR23 / 69 ms254 / 639 ms + uWS topics16 / 92 ms249 ms / 4-11 s + AnyCable OSS17 / 55 ms252 / 895 ms + AnyCable Pro22 / 145 ms246 / 875 ms -

† uWS p50 at 10k stays low, but p99 varies 4.4-10.6 s across 4 samples: app.publish serializes through one writer, so per-client TCP backpressure inflates the tail. Production uWS would shard; not tested here.

+

† uWS p50 at 10k stays low, but p99 varies 4.4-10.6 s across 4 samples: app.publish serializes through one writer, so per-client TCP backpressure inflates the tail. Production uWS would shard; not tested here.

@@ -382,11 +437,11 @@

Broadcast throughput

- Socket.io default56,8181.60 s14.98 s - Socket.io + CSR62,5200.70 s3.13 s - uWS332,3360.22 s3.23 s - AnyCable OSS207,4260.39 s3.08 s - AnyCable Pro205,1700.47 s3.29 s + Socket.io default56,8181.60 s14.98 s + Socket.io + CSR62,5200.70 s3.13 s + uWS332,3360.22 s3.23 s + AnyCable OSS207,4260.39 s3.08 s + AnyCable Pro205,1700.47 s3.29 s

uWS's app.publish is the fastest hot path (C++). AnyCable OSS and Pro track each other; Pro's wins are memory and the embedded broker, not raw msg/sec. Default Socket.io's p99 balloons under HTTP-pool concurrency; CSR's Redis Streams adapter handles it cleanly.

@@ -409,11 +464,11 @@

Whispers: client-to-client updates that byp
-

Socket.io rooms lose 96% of whispers at cursor-style load: every whisper still goes back through the WS process. AnyCable and uWS skip that hop.

+

Socket.io rooms loses a third of whispers because every whisper goes back through the WS process. AnyCable and uWS skip that hop and deliver everything under 200 ms.

- + @@ -421,13 +476,13 @@

Whispers: client-to-client updates that byp

- - - - + + + +
Setup 10k clients, 100 rooms, 2 HzSetup 1k clients, 10 rooms, 100 peers/room, 2 Hz Native? Delivered p50
Socket.io roomsEmulated3.5%1.87 s8.48 s
uWS topicsEmulated100%1.21 s7.81 s
AnyCable OSSNative99.3%1.33 s3.67 s
AnyCable ProNative95.4%1.39 s5.20 s
Socket.io roomsEmulated64.1%2.24 s9.74 s
uWS topicsEmulated100%10 ms21 ms
AnyCable OSSNative100%17 ms140 ms
AnyCable ProNative100%22 ms64 ms
-

10K clients in 100 rooms, 2 Hz whispers for 30 s. Socket.io rooms still routes every whisper through the WS process to re-emit — Express can't keep up. AnyCable and uWS bypass that path.

+

1K clients in 10 rooms (100 peers/room), 2 Hz whispers for 30 s. Each whisper fans out to 99 peers, so each client receives ~200 msgs/sec. Socket.io's rooms emulation re-emits through the server, Express can't keep up, and a third of the deliveries drop. AnyCable and uWS bypass the hop and stay under 200 ms p99. At 10K clients the bench-runner saturates client-side, which inflates latencies and confuses the delivery story; 1K × 100 peers/room is the cleanest comparison of the fanout paths.

@@ -444,35 +499,7 @@

How reliable is delivery?how often messages are lost and how long the recovery window drags. Both come down to whether the protocol carries replay state.

- Default Socket.io is at-most-once; so is uWS. CSR adds replay (opt-in, adapter constraints). AnyCable ships reliable streams by default: per-stream history, epoch + offset, restart-survivable with NATS or Redis. Under simulated WiFi jitter (1 s TCP drops every ~15 s): AnyCable and CSR deliver 100%; default Socket.io and uWS lose ~13-14%. -

- -
-
# Default Socket.io — 10K clients, 120 msgs each
-Client 0: missing 6, 7, 36, 37, 73, 74, 104, 105
-Client 1: missing 3, 4, 38, 39, 69, 70, 108, 109
-Client 2: missing 4, 5, 41, 42, 76, 77, 111-113
-
-# Each cluster = one jitter event. ~15 lost per client.
-# Total: 156,856 of 1,200,000 expected. ~87% delivery.
-
-# Socket.io + CSR — same workload
-Client 0: all 120 received  (replay tail: max 10.8s)
-Client 1: all 120 received  (p99: 7.9s)
-Client 2: all 120 received  (p95: 4.0s)
-
-# AnyCable — same workload
-Client 0: all 120 received  (replay tail: max 9.3s)
-Client 1: all 120 received  (p99: 6.2s)
-Client 2: all 120 received  (p95: 4.1s)
-
- - - {{!-- Benchmark table --}} -
-
-

- 10K clients, 120 broadcasts, each client TCP-drops 1 s every ~15 s (WiFi pattern). 1.2M expected deliveries. Methodology. + Default Socket.io is at-most-once; so is uWS. CSR adds replay (opt-in, adapter constraints). AnyCable ships reliable streams by default: per-stream history, epoch + offset, restart-survivable with NATS or Redis. Under simulated WiFi jitter (1 s TCP drops every ~15 s): AnyCable and CSR deliver 100%; default Socket.io and uWS lose ~13-14%.

- {{!-- TBD-B1: uWS sidebar removed in restructure. uWS is now a - first-class column in every comparison table. The avalanche - cliff-at-N data (Socket.io ~25K, uWS ~90K on a 1 vCPU / 0.5 GB - box) is being replaced by clustered 3-node tests in A2d. --}} {{!-- Use cases / impact --}}
@@ -569,7 +592,7 @@

What this breaks

Dashboards & monitoring - Permanent gaps in time-series data. The 200 ms blip during a traffic spike is the data point you needed. + Permanent gaps in time-series data. The 200 ms blip during a traffic spike is the data point you needed.
@@ -577,155 +600,6 @@

What this breaks

- {{!-- TBD-B1: This section (currently the single-node deploy/avalanche - test) will be reframed as the deploy-impact subsection of new Q3 - (scale). Content stays; numbering changes; the new clustered - 3-node rolling-restart test (A2c) is added as a sibling subsection - below once it lands. Physical reorder (load test before avalanche) - deferred to content polish; B1 keeps content placement stable. --}} -
-
-
-
-

Does the WS layer survive deploys?#

-

- Modern teams deploy multiple times a day. Every deploy is a beat: does the WS layer skip it? Socket.io, +CSR, and uWS are Node libraries — the WS layer restarts every time the app does. At small N the reconnect storm recovers. Past a certain N it stops recovering. The test: real railway redeploy on a 1 vCPU / 0.5 GB box. -

- - {{!-- Architecture diagram — Socket.io's app+WS in one process - vs AnyCable's app | anycable-go separated. The visual - carries the structural argument; the H3s + paragraphs - below reinforce it in text. --}} -
- - Socket.io vs AnyCable process architecture - - - SOCKET.IO · WS LAYER CO-LOCATED WITH APP - - - - NODE.JS PROCESS - - - - Your app - Routes, auth, DB — single event loop - - - Socket.io WS hub - Tens of thousands of open connections - - - DEPLOY → RESTART NODE → EVERY WS DROPS - - - ANYCABLE · WS LAYER IS A SEPARATE PROCESS - - - - Your app - Node.js, Python, Laravel, Go — - anything that speaks HTTP - - - - - HTTP /_BROADCAST - - - - anycable-go - Single Go binary — holds all the - WS connections (1M idle / node) - - - DEPLOY APP → ANYCABLE-GO UNTOUCHED, WS SURVIVE - -
Two architectures, two deploy outcomes. Socket.io shares a process with the app; AnyCable runs the WS layer separately.
-
- -

- Past a few thousand connections, the WS layer should be its own service. Socket.io / uWS can be deployed that way (extra Node processes, Redis adapter, sticky sessions, deploy choreography). AnyCable defaults to it. The 0 s recovery row below is what that buys. -

-
-
-
# Socket.io deploy — 5,000 clients (1 vCPU / 0.5 GB box)
-t=0.0s → deploy restarts server
-t=0.5s → all 5,000 connections dropped
-t=2.6s → p50 reconnected
-t=4.5s → 95% recovery (avalanche over)
-
-# Socket.io deploy — 25,000 clients (same box) — the cliff
-t=0.0s   → deploy restarts server
-t=0.5s   → all 25,000 connections dropped
-t=300s+  → 0 reconnected — box can't absorb the storm
-
-# AnyCable deploy — any client count, any box
-t=0.0s → deploy restarts app
-t=0.0s → zero connections dropped
-t=0.0s → users don't notice
-
-
- -
-
-

- Through 20K, recovery is graceful. At 25K, recovery never completes — the reconnect storm OOMs the new container. CSR doesn't help (in-memory adapter loses state on restart). A bigger box just moves the cliff: uWS pushes it to ~90K, but at 25K it's already 57 s. Standalone WS avoids the cliff because the WS layer doesn't restart. Methodology · thundering-herd reading. -

-
-
-
-

Past 25K, in-process WS can't recover from its own restart. Standalone WS doesn't restart, so there's nothing to recover from.

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
ClientsSocket.io recovery (also +CSR, uWS)ReconnectedAnyCable OSS & Pro
5,0004.5 s100%0 s
10,0003.9 s100%0 s
15,0005.8 s98.5% (224 lost)0 s
20,0008.0 s96.2% (753 lost)0 s
25,000never0% (all 25K lost)0 s
-

The split is in-process vs out-of-process WS. Socket.io, +CSR, and uWS run the WS layer inside the Node app — railway redeploy restarts it. AnyCable runs WS as a separate Go binary; app deploys never touch it. 1 vCPU / 0.5 GB box; "recovery" = wall-clock until 95% reconnected.

-
-
-
-
-
- {{!-- Connection capacity --}}
@@ -733,174 +607,34 @@

Does the WS layer survive deploys

How efficient is it to scale?#

- Your user base will grow, and your realtime layer needs to grow with it. The question isn't how cheap is this today: it's when we 10× users, does the bill or the box cliff before we have time to react? Two things to measure: how many idle connections one node holds, and how it behaves during a reconnect storm. Single instance, 32 vCPU / 32 GB Railway box, 1M-connection idle target. -

-

- Cost shape matters too. AnyCable Pro is flat-rate ($1,490/yr, unlimited instances). Pusher charges per connection ($499/mo for 10K, scaling with size, 2026 plans). uWS and self-hosted Socket.io are box-priced. + Your user base will grow and your realtime layer needs to grow with it. The question isn't how cheap is this today, it's when we 10× users, does the box cliff before we have time to react? The numbers below come from the single-node idle-connections load test on a 32 vCPU / 32 GB Railway box, 1M-connection target.

-

uWS holds the most idle connections per byte. AnyCable Pro is the lightest setup that includes built-in replay. Socket.io tops out at ~120K.

+

uWS holds the most connections per byte. AnyCable Pro is the lightest setup that includes built-in replay. Socket.io tops out around 120K.

- - - - - - - - - - - - - - -
Idle connections heldMemory at peakPer-connReplay?
Socket.io (1M attempted)119,826 conns~52 KBno
200,000 AC OSS8.35 GB42 KByes
200,000 AC Pro3.56 GB18 KByes
821,877 AC OSS28.30 GB34 KByes
822,037 AC Pro14.80 GB18 KByes
1,018,366 uWS5.45 GB5.4 KBno
-

uWS is bare wire (no broker). AnyCable Pro is the lightest with replay built in; the extra KB/conn is per-stream history + reconnect-resume protocol.

-
- - {{!-- Memory + CPU during the 1M Pro run, captured live - from Railway's GraphQL metrics during the test - window. Reproduced from bench/idle-multi.ts output. --}} -
-
anycable-go-pro during the 1M idle run (Railway metrics, 30s sampling)
-
Memory
-  19339 MB │                                               •●•••●•••●•••●
-           │                                   •●•••●•••●••
-           │                               •●••
-           │                             ●•
-           │                           ••
-           │                        •●•
-           │                •●•••●••
-           │             ●••
-           │           ••
-           │ ●•••●•••●•
-           │
-   0.00 MB │
-           └────────────────────────────────────────────────────────────
-             -30s                                                    420s
-
-CPU (% of 1 vCPU)
-  9.37 % │                               •●•          ●•••●•••●
-         │                            •●•   ••●                •
-         │                         ●••               •
-         │                       ••            •                •
-         │                    •●•               •   •            •
-         │                •●••                     •              ●•••●
-         │            •●••                       •
-  0.00 % │ ●•••●•••●••                            ●
-         └────────────────────────────────────────────────────────────
-           -30s                                                    420s
-
-
-

- - -
- - {{!-- TBD-B1: Clustered topology placeholders. Three new tests land here: - A2c (deploy-impact under rolling restart of a 3-node cluster), - A2d (clustered load + avalanche), A2e clustered tier (hop overhead - under cluster pub/sub). This is where the "is in-process WS - sustainable at scale?" question gets its empirical answer. --}} -
-
-
-
-

What does a clustered deployment look like?#

-

- Past one node, the question shifts: can a rolling deploy survive without dropping connections? 3-node cluster behind a load balancer, 10k connections steady. Same 5 options: Socket.io (Redis adapter), Socket.io + CSR (Redis Streams), uWS, AnyCable OSS, AnyCable Pro. Capacity and 1-of-N-down behavior pending A2d. -

-
- uWS multi-node is DIY. No built-in cross-node clustering, no built-in HTTP publish. We wrote a custom publish API and a custom Redis pub/sub bridge to test uWS at all. Production uWS teams end up building both themselves. Socket.io and AnyCable ship the multi-node story as documented config. -
-
-
- - {{!-- Rolling-deploy impact (A2c). The canonical answer to "can - horizontal scaling + rolling deploy substitute for separate-process WS?" --}} -
-
-

Rolling deploy: per-client gap + message loss

-

- 3-node cluster, 10k connections, drain + restart nodes in sequence. Measures what each user actually sees: connection gap, messages missed, whether surviving nodes absorb the reconnect storm. -

-
-
-
-

Embedded WS makes every user skip a beat on every deploy. Standalone leaves the dance floor untouched.

- - - - - - - - - - - - - - - - - - - -
Setup 10k, 3 nodesGap p50Gap p99Msgs lostFull reconnect
Socket.io embedded + Redis adapter1.95 s2.80 s2-3100%
Socket.io + CSR embedded + Redis Streams1.50 s1.63 s2-4§100%
uWS embedded + custom pub/subTBD-A2cTBD-A2cTBD-A2cTBD-A2c
Socket.io standalone0000%
uWS standalone + custom publishTBD-A2cTBD-A2cTBD-A2cTBD-A2c
AnyCable OSS standalone, always0000%
AnyCable Pro standalone, always0000%
-

Embedded: 10K clients (5K for CSR due to bench-runner memory), rolling restart of all 3 nodes. Standalone: only the publisher restarts; WS layer is untouched. uWS multi-node is DIY. Standalone deploys pause publishing for ~45 s but don't disrupt connections. §CSR's replay backfills on reconnect.

-
-
-
- - {{!-- Clustered capacity + avalanche (A2d) --}} -
-
-

Clustered capacity and 1-of-N down

-

- Total capacity across 3 nodes, and what happens when 1 of 3 fails: how fast survivors absorb the displaced load, what's lost in transit. -

-
-
-
-

Cluster capacity and 1-of-N survivor behavior. Pending A2d.

- - - - - + + - - + + - - - - - + + + +
Setup 3-node clusterCapacitySetupConnections held RAM/connRamp-up1-of-3 downCPU peak % of 1 vCPUReplay?
Socket.io + RedisTBD-A2dTBD-A2dTBD-A2dTBD-A2d
Socket.io + CSRTBD-A2dTBD-A2dTBD-A2dTBD-A2d
uWS custom pub/subTBD-A2dTBD-A2dTBD-A2dTBD-A2d
AnyCable OSSTBD-A2dTBD-A2dTBD-A2dTBD-A2d
AnyCable ProTBD-A2dTBD-A2dTBD-A2dTBD-A2d
Socket.io (1M attempted)119,826~52 KBn/ano
AnyCable OSS821,87734 KB9.8%yes
AnyCable Pro822,03718 KB7.9%yes
uWebSockets.js1,018,3665.4 KBn/ano
-

Pending A2d. uWS clustering requires a custom pub/sub layer; production teams would build the same.

+

Ramp to 1M connections at 200/sec, hold 2 min. CPU peak measured on the WS server during the hold window via Railway metrics (CPU traces for uWS and Socket.io weren't captured during these runs). uWS is bare wire, no broker. AnyCable Pro is the lightest with replay built in; the extra KB/conn covers per-stream history and reconnect-resume.

- {{!-- Topology takeaway: in-process at scale --}} -
-
-

Takeaway: when does in-process WS stop being viable?

-

- CSR + Redis Streams is the best in-process answer we measured: every user skips ~1.5 s on every deploy, but the replay backfills the missed beats (100% delivery). Default Socket.io is worse (~2 s freeze, 99.6% delivered, 2-3 messages lost per user). Standalone WS is the only shape where users don't notice the deploy at all: zero disconnects, zero reconnects, a 45-second publishing pause that affects what gets sent, not what gets received. CSR shrinks the missed beat. Only architecture eliminates it. -

-
-
@@ -932,7 +666,7 @@

What you don't have to build

Reliable deliveryNoYes (opt-in)NoYes (default) - Replay latency p99lost~8 slost~6 s + Replay latency p99lost~8 slost~6 s Survives server restartNoRedis Streams / MongoNoNATS / Redis Multi-node setupRedis pub/subRedis pub/sub incompat.DIY (Redis)Any broker Deploy resilienceAll dropAll dropAll dropConnections survive @@ -1000,12 +734,12 @@

FAQ How does AnyCable compare on performance? - +
Three workloads on the same 32 vCPU / 32 GB Railway box, every option as a standalone WS service.

1M-connection idle: uWS 1,018,366 / 5.45 GB. AnyCable Pro 822,037 / 14.8 GB (lightest with built-in replay). AnyCable OSS 821,877 / 28.3 GB. Socket.io caps at 119,826 (Node event loop saturates handshakes).

10K reconnecting clients under WiFi jitter: AnyCable and Socket.io+CSR deliver 100% (p99 ~6 s and ~8 s replay tails). Default Socket.io and uWS lose ~13-14%.

Broadcast throughput at 1M deliv/sec target (HTTP pool=16): uWS 332K, AnyCable OSS 207K, Pro 205K, Socket.io+CSR 62K, default 57K. AnyCable OSS/Pro hold the lowest p99 (~3.1 s). Reproduce.

What about uWebSockets.js? It's faster than Socket.io. -
uWS is genuinely faster on the wire — we measured it. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 14.80 GB at 822K). The "10× faster than Socket.io" claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses ~14% of messages — the same loss profile as Socket.io without CSR — because both at-most-once and embedded in the same Node process as your app. uWS solves "Socket.io's wire is too heavy"; it doesn't solve "we lose messages during disruption" or "every deploy hits our users."
+
uWS is genuinely faster on the wire — we measured it. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 14.80 GB at 822K). The "10× faster than Socket.io" claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses ~14% of messages — the same loss profile as Socket.io without CSR — because both at-most-once and embedded in the same Node process as your app. uWS solves “Socket.io’s wire is too heavy”; it doesn't solve “we lose messages during disruption” or “every deploy hits our users.”
@@ -1045,7 +779,7 @@

FAQ What happens if AnyCable itself restarts? -
Less often than app deploys (you don't ship anycable-go on every code change), but it does happen — version upgrades, config changes, host reboots. The behavior depends on the broker: with an external broker (NATS JetStream or Redis Streams), replay state survives the restart, so clients reconnect and resume from the last offset they saw. With the default in-memory broker (or AnyCable Pro's embedded broker), replay state is lost on restart and clients fall back to "live from now" — same delivery profile as a cold reconnect to default Socket.io. For production, run multiple anycable-go instances behind a load balancer and a shared broker; restart one at a time during upgrades and clients seamlessly reconnect to the others.
+
Less often than app deploys (you don't ship anycable-go on every code change), but it does happen — version upgrades, config changes, host reboots. The behavior depends on the broker: with an external broker (NATS JetStream or Redis Streams), replay state survives the restart, so clients reconnect and resume from the last offset they saw. With the default in-memory broker (or AnyCable Pro's embedded broker), replay state is lost on restart and clients fall back to “live from now” — same delivery profile as a cold reconnect to default Socket.io. For production, run multiple anycable-go instances behind a load balancer and a shared broker; restart one at a time during upgrades and clients seamlessly reconnect to the others.

@@ -1105,7 +839,7 @@

Run AnyCable in your Node app

"name": "How does AnyCable compare on performance?", "acceptedAnswer": { "@type": "Answer", - "text": "Three workloads, 32 vCPU / 32 GB Railway box, every option standalone. Idle 1M target: uWS 1,018,366 / 5.45 GB; AnyCable Pro 822,037 / 14.8 GB; AnyCable OSS 821,877 / 28.3 GB; Socket.io caps at 119,826. WiFi jitter at 10K: AnyCable and CSR deliver 100% (p99 ~6 s and ~8 s); default Socket.io and uWS lose ~13-14%. Broadcast throughput at 1M deliv/sec target (HTTP pool=16): uWS 332K, AnyCable OSS 207K, Pro 205K, Socket.io+CSR 62K, default 57K; AnyCable holds the lowest p99 (~3.1 s). Source: github.com/irinanazarova/anycable-socketio-benchmarks." + "text": "Three workloads, 32 vCPU / 32 GB Railway box, every option standalone. Idle 1M target: uWS 1,018,366 / 5.45 GB; AnyCable Pro 822,037 / 14.8 GB; AnyCable OSS 821,877 / 28.3 GB; Socket.io caps at 119,826. WiFi jitter at 10K: AnyCable and CSR deliver 100% (p99 ~6 s and ~8 s); default Socket.io and uWS lose ~13-14%. Broadcast throughput at 1M deliv/sec target (HTTP pool=16): uWS 332K, AnyCable OSS 207K, Pro 205K, Socket.io+CSR 62K, default 57K; AnyCable holds the lowest p99 (~3.1 s). Source: github.com/irinanazarova/anycable-socketio-benchmarks." } }, { @@ -1113,7 +847,7 @@

Run AnyCable in your Node app

"name": "What about uWebSockets.js? It's faster than Socket.io.", "acceptedAnswer": { "@type": "Answer", - "text": "uWS is genuinely faster on the wire — measured. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 14.80 GB at 822K). The '10× faster than Socket.io' claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses about 14% of messages — the same loss profile as Socket.io without CSR — because both are at-most-once and embedded in the same Node process as your app. uWS solves 'Socket.io's wire is too heavy'; it doesn't solve 'we lose messages during disruption' or 'every deploy hits our users.'" + "text": "uWS is genuinely faster on the wire — measured. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 14.80 GB at 822K). The '10× faster than Socket.io' claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses about 14% of messages — the same loss profile as Socket.io without CSR — because both are at-most-once and embedded in the same Node process as your app. uWS solves 'Socket.io's wire is too heavy'; it doesn't solve 'we lose messages during disruption' or 'every deploy hits our users.'" } }, { @@ -1199,7 +933,7 @@

Run AnyCable in your Node app

"@context": "https://schema.org", "@type": "TechArticle", "headline": "AnyCable vs Socket.io vs uWebSockets.js: Benchmarking WebSocket infrastructure for JS/TS apps", - "description": "A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions answered with reproducible benchmarks: (1) how fast — roundtrip latency at 1k/10k/100k subscribers, broadcast throughput, plus whispers (client-to-client without backend hop) where AnyCable competes with Liveblocks, Yjs, PartyKit; (2) how reliable — message delivery under WiFi-drop jitter, separating at-most-once protocols (default Socket.io, uWS) from those with replay state (Socket.io+CSR, AnyCable); (3) how efficient to scale — single-node load (RAM per connection, max concurrent) and clustered rolling-deploy tests that decide whether in-process WS is sustainable. Tested in embedded and standalone topologies. All numbers reproducible from the open-source bench repo at github.com/irinanazarova/anycable-socketio-benchmarks.", + "description": "A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions answered with reproducible benchmarks: (1) how fast: roundtrip latency at 1k/10k subscribers, broadcast throughput, plus whispers (client-to-client without backend hop) where AnyCable competes with Liveblocks, Yjs, PartyKit; (2) how reliable: message delivery under WiFi-drop jitter, separating at-most-once protocols (default Socket.io, uWS) from those with replay state (Socket.io+CSR, AnyCable); (3) how efficient to scale: single-node load (RAM per connection, max concurrent) plus the avalanche behavior at app deploy that decides whether in-process WS is sustainable. Tested in embedded and standalone topologies. All numbers reproducible from the open-source bench repo at github.com/irinanazarova/anycable-socketio-benchmarks.", "url": "https://anycable.io/compare/socket-io", "mainEntityOfPage": "https://anycable.io/compare/socket-io", "datePublished": "2026-04-30", diff --git a/src/compare/socket-io/style/index.html b/src/compare/socket-io/style/index.html index 0c7d11f..a37f958 100644 --- a/src/compare/socket-io/style/index.html +++ b/src/compare/socket-io/style/index.html @@ -1,406 +1,313 @@ - {{> dochead pageTitle="Compare-page style index — design language spine" pageDescription="Internal style index for the AnyCable comparison-page design spine. Every element rendered in one place so we can iterate the lab-notebook design language without redesigning 30 sections in parallel." pageUrl="https://anycable.io/compare/socket-io/style"}} + {{> dochead pageTitle="Compare-page style index — system reference" pageDescription="Internal style index for the AnyCable compare-page design system. Every block authors should reach for, rendered once with the HTML structure visible. Copy from here, paste into any compare/* page."}}
{{> header}} -
- - {{!-- ================================================== --}} - {{!-- 0. PAGE HEADER — what is this page --}} - {{!-- ================================================== --}} -
- Style index -

- AnyCable vs Socket.io +
+ + {{!-- ============================================================ + SYSTEM REFERENCE + Every section below shows: (1) what it looks like rendered, + (2) the HTML structure to copy, (3) when to reach for it. + Source of truth — if a pattern isn't here, it isn't part of + the system yet. Add it here first, then to the page. + ============================================================ --}} + +
+

+ Compare-page system reference

-

- Internal style index for the comparison-page design spine. Every typographic role, - layout primitive, and component the page uses, rendered once each. Iterate here, - not in the 1448-line page. Live tokens are CSS custom properties on - .c-spine — tweak in devtools. +

+ What authors writing or extending a comparison page should reach for. Every block here renders as it will on the live page. Copy the HTML next to each one. If a pattern is missing, add it to the spine (src/modules/blocks/compare-spine.scss) first, then add a demo entry here.

-

- Signature: lab notebook. Mono structural labels. Light tinted code blocks. Tight - 580px prose column. No dark surfaces on this page. +

+ All spine rules are scoped under .c-spine. The comparison page activates them by adding c-spine to its <main class='content compare-page'>.

- {{!-- ================================================== --}} - {{!-- 1. TYPOGRAPHY ROLES --}} - {{!-- ================================================== --}} -
- Section 01 -

Typography roles

-

- Five voices. Anything else is a re-skin of one of these. -

- -
+

+ Display heading with vs +

+

Use for: the page H1. The c-vs span is a typographic operator between comparison terms.

+
<h1 class='compare-hero__title'>
+  Socket.io <span class='c-vs'>vs</span> AnyCable
+</h1>
+ +

Section heading

+

Use for: the top of every <section class='compare-rubric'>. Display sized.

+
<h2 class='compare-rubric__title' id='latency'>
+  How fast is it?
+  <a class='heading-anchor' href='#latency' aria-label='Permalink'>#</a>
+</h2>
+ +

Sub-section heading

+

Use for: H3 inside a long section.

+
<h3 class='compare-rubric__subtitle'>How latency scales</h3>
+ +

+ Body prose — capped at 580px line length. Inline code sits as a calm pill scaled to 0.8em so it optically aligns with Stem. Links carry the accent color. Strong for emphasis. Use t-mute for quieter text, t-tiny for 11px small print, t-accent for brand red, and t-num on numbers like 123,456 for tabular spacing. +

+
<p class='compare-prose'>
+  Body prose. Inline <code>code</code>, <strong>strong</strong>,
+  <a class='link' href='…'>link</a>, <span class='t-mute'>mute</span>.
+</p>
- {{!-- ================================================== --}} - {{!-- 2. THE "vs" TREATMENT — before / after --}} - {{!-- ================================================== --}} -
- Section 02 -

The "vs" treatment

-

- Operator-style, mono, accent red, vertical-aligned. Reads as a typographic operator - instead of a faded word. The grey-mute span is gone. + {{!-- ============================================================ + 02 · LAYOUT + ============================================================ --}} +

+

02 · Section layout

+

+ Every section on the page is a compare-rubric. Two-column by default (prose left, media right); use --full for single-column sections like FAQ or appendix.

- -
-
- Before -
- - AnyCable vs Socket.io - -
-

46px display, gray span same color as captions

-
-
- After -
-

- AnyCable vs Socket.io -

-
-

.c-vs · mono 0.42em · accent red · vertical-aligned 0.45em

-
-
+
<section class='compare-rubric'>
+  <div class='compare-rubric__wrapper'>
+    <div class='compare-rubric__section'>
+      <div class='compare-rubric__content'>
+        <h2 class='compare-rubric__title' id='…'>…</h2>
+        <p class='compare-prose'>…</p>
+      </div>
+      <div class='compare-rubric__media compare-rubric__media--align-top compare-rubric__media--sticky'>
+        <!-- a code block, table, diagram, or chart -->
+      </div>
+    </div>
+  </div>
+</section>
+ +

+ Media modifiers: --align-top aligns the media column to the top of the prose; --sticky pins it during long scroll. Combine both for the default benchmark rubric. +

+

+ Full-width: for sections without a media partner (FAQ, appendix, CTA, intro). +

+
<div class='compare-rubric__content compare-rubric__content--full'>…</div>
- {{!-- ================================================== --}} - {{!-- 3. RUBRIC LAYOUT — two-column with sticky media --}} - {{!-- ================================================== --}} -
-
- Section 03 -

Rubric layout

-

- Two columns, prose left at 580px max, media right (data table or code). The - media column can be sticky if the prose is long. -

-
+ {{!-- ============================================================ + 03 · RIGHT-SIDE ARTIFACTS + All four share the same outer container: #fafafa bg, #ececec + hairline border, 8px radius, 28/32 padding. + ============================================================ --}} +
+

03 · Right-side artifacts

+

+ Four kinds of content go in the media column. They share one visual container so the page rhythm stays consistent. +

-
-
- Bench 01 -

How fast is the roundtrip?

-

- Latency at 1k, 10k, and 100k concurrent connections, plus broadcast throughput - and whispers. Embedded vs standalone topologies, both measured. Numbers here - are p50 roundtrip at 10k — the steady-state of a typical production setup. -

-

- AnyCable Pro is fastest because the Go broker drops the network hop in the - hot path; uWS comes second because it's in-process; Socket.io trails because - the JS event loop is shared with your app code. -

-

- Methodology and the bench harness live on - GitHub. -

-
+

Code block

+
// Server
+const io = new Server(httpServer);
+
+io.on('connection', (socket) => {
+  socket.on('subscribe', (t) => socket.join(t));
+});
+
<pre class='compare-code-block'><span class='c-com'>// comment</span>
+<span class='c-key'>const</span> foo = <span class='c-str'>'string'</span>;
+</pre>
+

+ Syntax tokens: c-key (keyword, amber), c-str (string, green), c-com (comment, slate italic), c-err (error, red). +

-
-
-
- Fig. 1 - p50 roundtrip @ 10K connections -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Setupp50p99
Default Socket.io47 ms182 ms
Socket.io + CSR49 ms194 ms
uWebSockets.js31 ms98 ms
AnyCable24 ms71 ms
AnyCable Pro19 ms58 ms
-
- Identical Railway hardware · 10K idle subs · 1KB payload · 60s window -
-
+

Framed data table

+
+ + + + + + + + + + + + + + + +
Setupp50p99
Socket.io default47 ms182 ms
Socket.io + CSR49 ms194 ms
uWebSockets.js31 ms98 ms
AnyCable24 ms71 ms
AnyCable Pro19 ms58 ms
-
- - {{!-- ================================================== --}} - {{!-- 4. CODE BLOCK — light tinted, muted syntax --}} - {{!-- ================================================== --}} -
- Section 04 -

Code blocks

-

- Light surface (#fafafa), 1px subtle border, muted syntax tints so - the block reads as part of the prose layer, not as a foreign material. The - tokens are warm amber for keywords, deep green for strings, slate gray for - comments, deep red for errors. +

Methodology note in mono small. Use compare-data-table__footnote directly below the frame.

+
<div class='compare-frame'>
+  <table class='compare-data-table compare-data-table--compact'>
+    <thead>…</thead>
+    <tbody>
+      <tr><td>Setup</td><td class='is-best'><strong>19 ms</strong></td></tr>
+      <tr class='is-row-best'>…</tr>
+    </tbody>
+  </table>
+</div>
+<p class='compare-data-table__footnote'>Methodology note.</p>
+

+ Cell states: is-best (green winner), is-worst (red loser), is-na (faded n/a), is-row-best (full-row tint), is-row-worst. +
Variant: compare-data-table--compact for tables with many columns.

+
-
-
- Before -
// Server
-const io = new Server(httpServer);
-
-io.on('connection', (socket) => {
-  socket.on('subscribe', (t) => socket.join(t));
-});
-
-io.to('chat:42').emit('message', payload);
-

Dark #1a1a1a, bright syntax — reads as island

-
-
- After -
// Server
-const io = new Server(httpServer);
-
-io.on('connection', (socket) => {
-  socket.on('subscribe', (t) => socket.join(t));
-});
+        {{!-- ============================================================
+              04 · ASIDES
+              ============================================================ --}}
+        
+

04 · Callout (aside)

-io.to('chat:42').emit('message', payload);
-

Light #fafafa, muted syntax — part of the page

-
+
+ Caveat or framing. Use the callout for a methodology note, caveat, or "how to read this" framing. Accent-red left rule signals "important context, not main body."
- Standalone, framed -
-
- Code 03 - AnyCable broadcast from your Node app -
-
-
// Server — broadcast over HTTP to the anycable-go binary
-import { broadcast } from '@anycable/serverless-js';
-
-await broadcast('chat:42', {
-  type: 'message',
-  body: payload,
-});
+          
+ Lists inside callouts get smaller spacing automatically. No need to inline-style anything. +
    +
  • 100% of users affected by every deploy
  • +
  • ~2-second freeze per user
  • +
  • 2–3 messages lost per user, per deploy
  • +
+
-// Replay, history, presence — all server-side. Your app stays -// stateless. WS layer scales and deploys independently.
-
- Server-side broadcast · same code in OSS and Pro · WS-layer process is a separate binary -
+
<div class='compare-callout'>
+  <strong>Caveat.</strong> Body text.
+</div>
- {{!-- ================================================== --}} - {{!-- 5. CALLOUT --}} - {{!-- ================================================== --}} -
- Section 05 -

Callouts

-

- One callout primitive. The bracketed label carries the type signal (NOTE, - CAVEAT, METHOD), so we don't need different colors. + {{!-- ============================================================ + 05 · LISTS + ============================================================ --}} +

+

05 · Lists

+ +

Bullet list

+
    +
  • Default Socket.io — the baseline most teams ship today.
  • +
  • Socket.io + CSR — the in-place delivery upgrade from 4.6, opt-in.
  • +
  • uWebSockets.js + topics — the “just use uWS” alternative.
  • +
  • AnyCable OSS — a separate Go binary your app broadcasts to.
  • +
  • AnyCable Pro — same protocol; denser per-conn memory.
  • +
+ +

Ordered list

+
    +
  1. How fast — latency at 1k / 10k / 100k.
  2. +
  3. How reliable — delivery under WiFi-drop jitter.
  4. +
  5. How efficient to scale — load + 3-node deploy.
  6. +
+

+ Numbered <ol class='compare-prose'> renders with mono accent-red 01 / 02 / 03 markers. No inline styles needed.

+
-
-
- Note - Numbers are p50 over a 60-second steady-state window. Cold-start latency - behaves differently; see the Headroom section. + {{!-- ============================================================ + 06 · CARDS + ============================================================ --}} +
+

06 · Cards

+ +

Hero card strip (top of page)

+
+
+
Latency p50 @ 10K
+
+
Socket.io
47 ms
+
uWS
31 ms
+
AnyCable
19 ms
+
+ Latency test
- -
- Caveat - uWebSockets.js doesn't ship cluster-mode out of the box. The 3-node test - uses a Redis-backed adapter we wrote for this benchmark, available in the - bench/uws-cluster branch. +
+
Reliability delivery under jitter
+
+
Socket.io
87%
+
uWS
86%
+
AnyCable
100%
+
+ Reliability test
- -
- Method - Each setup runs on identical Railway hardware (4 vCPU, 8GB). Numbers are - reproducible from the - open-source benchmark repo. +
+
Scalability downtime per deploy
+
+
Socket.io
~2 s
+
uWS
~2 s
+
AnyCable
0 s
+
+ Scalability test
-
- - {{!-- ================================================== --}} - {{!-- 6. CARDS --}} - {{!-- ================================================== --}} -
- Section 06 -

Cards

-

- One card primitive. Hero cards, try-it cards, impact cards — same component, - different label. The bracketed label is the type signal. -

-
- - Bench 01 -

How fast?

-

p50 roundtrip at 1k / 10k / 100k connections. Broadcast throughput, whispers.

-
- - Bench 02 -

How reliable?

-

Delivery under simulated WiFi-drop jitter. Replay vs at-most-once protocols.

-
- - Bench 03 -

How efficient to scale?

-

Per-instance load + 3-node clustered rolling-deploy test.

-
+

Impact / disqualification cards

+
+
+ Live chat & notifications + Messages disappear during network blips. Users see incomplete conversations. +
+
+ LLM streaming + Mid-sentence disconnects leave output truncated. Chunks lost, no recovery. +
-
- {{!-- ================================================== --}} - {{!-- 7. BULLET LIST + TOC --}} - {{!-- ================================================== --}} -
- Section 07 -

Lists & navigation

- -

Bullet list

-
    -
  • Default Socket.io — the baseline most teams have today.
  • -
  • Socket.io + CSR — in-place delivery upgrade Socket.io shipped in 4.6.
  • -
  • uWebSockets.js + topics — the most-cited "just use uWS" alternative.
  • -
  • AnyCable OSS — separate Go binary with broker built in.
  • -
  • AnyCable Pro — same protocol, denser per-connection memory.
  • -
- -

TOC navigation row

- +

Try-it card (link card with eyebrow)

+ +
JS client
+
@anycable/core, @anycable/web
+
The official client for Node.js, browsers, React Native.
+
+

+ t-eyebrow inside compare-try-card gets default margin-bottom. No inline styles needed. +

- {{!-- ================================================== --}} - {{!-- 8. CTA --}} - {{!-- ================================================== --}} -
-
- Try it -

Run the benchmarks yourself

-

- Every number on this page is reproducible from the open-source bench harness. - Clone, set your hardware, run the same setups. The numbers below are ours. -

- -

- Open source · MIT · Identical hardware · Reproducible numbers -

+ {{!-- ============================================================ + 07 · FAQ ACCORDION + ============================================================ --}} +
+

07 · FAQ accordion

+
+
+ Does AnyCable replace Socket.io? +
It replaces it. Your app broadcasts to AnyCable via HTTP instead of io.emit().
+
+
+ What's the operational cost? +
One extra process. A single Go binary or Docker container. Stateless, scales horizontally.
+
+
<div class='compare-faq'>
+  <details class='compare-faq__item'>
+    <summary>Question?</summary>
+    <div class='compare-faq__answer'>Answer.</div>
+  </details>
+</div>
- {{!-- ================================================== --}} - {{!-- 9. TOKEN INVENTORY (devtools cheat sheet) --}} - {{!-- ================================================== --}} -
- Appendix -

Token inventory

-

- Every token is a CSS custom property on .c-spine. Tweak in devtools - to iterate. Anything you change here is what we then promote into - compare-spine.scss. -

+ {{!-- ============================================================ + 08 · INLINE VOCABULARY + ============================================================ --}} +
+

08 · Inline vocabulary

- +
- - - - - + - - - - - - - - - - - - - - + + + + + + + + + + + +
TokenValueUsed by
ClassUse for
--c-fs-display64px.c-display (H1)
--c-fs-section36px.c-section (H2)
--c-fs-sub22px.c-sub (H3)
--c-fs-body19px.c-prose
--c-fs-meta15px.c-meta, table cells
--c-fs-caption13px.c-caption, figure caption
--c-fs-label12px.c-label, eyebrows, table heads
--c-prose-max580px.c-prose hard cap
--c-space-section96pxsection vertical
--c-space-block32pxafter H2
--c-space-para24pxafter body paragraph
--c-accent#f64343vs, links, anchors
--c-best#16a34awinning values only
--c-code-bg#fafafa.c-code surface
c-vsThe operator between H1 comparison terms
linkInline link (accent-red, underline-on-hover)
t-muteQuieter text (gray)
t-tiny11px small print (often with t-mute)
t-metaMedium gray (footnotes)
t-strongPure black for emphasis
t-accentBrand red
t-numTabular-nums for aligned numbers
t-eyebrowMono uppercase label
c-key / c-str / c-com / c-errCode syntax tokens inside <pre> blocks
is-best / is-worst / is-naTable cell state
is-row-best / is-row-worstFull table row tint
diff --git a/src/modules/blocks/compare-spine.scss b/src/modules/blocks/compare-spine.scss index 92bdc1d..c32830c 100644 --- a/src/modules/blocks/compare-spine.scss +++ b/src/modules/blocks/compare-spine.scss @@ -909,9 +909,15 @@ } &__nav { - margin-top: 32px; - padding-top: 24px; - border-top: 1px solid var(--c-border); + // No top border — the abstract band is already bounded by top/bottom + // hairlines; a third inner divider was visual noise. Spacing alone + // separates prose from the contents row. + // Explicit `border-top: none` because compare.scss still sets one; + // we override it here so the live page has only the band's outer + // hairlines. + margin-top: 40px; + padding-top: 0; + border-top: none; a { font-family: var(--c-font-mono); @@ -1178,13 +1184,24 @@ } // ---- Code tabs: framed unit with mono labels --------------------- + // The container is a CSS grid with the bar in row 1 and ALL panels + // stacked in row 2 (same grid cell). The grid cell sizes to the + // tallest panel, so switching tabs never resizes the container and + // the page below never jumps. Inactive panels are kept in layout but + // hidden via `visibility: hidden` (not `display: none`, which would + // collapse them and re-introduce the jump). .compare-code-tabs { + display: grid; + grid-template-columns: 1fr; + grid-template-rows: auto auto; border: 1px solid var(--c-code-border); border-radius: var(--c-radius); overflow: hidden; background: var(--c-code-bg); &__bar { + grid-row: 1; + grid-column: 1; display: flex; flex-wrap: wrap; background: var(--c-paper); @@ -1210,13 +1227,27 @@ } } + // All panels share the same grid cell. The tallest defines the + // container height; the rest sit underneath, invisible but present. + // `!important` is needed because compare.scss sets `display: none` + // and `:checked ~ panel { display: block }` — those rules would + // collapse the inactive panels and re-introduce the jump. + &__panel { + display: block !important; + grid-row: 2; + grid-column: 1; + visibility: hidden; + pointer-events: none; + } + &__panel pre { border: none; border-radius: 0; margin: 0; } - // Active tab: stripped underline accent in red. + // Active tab: underline accent in red. + // Active panel: visible (the rest stay invisible but in flow). @for $i from 1 through 5 { .compare-code-tabs__radio--#{$i}:checked ~ .compare-code-tabs__bar @@ -1225,6 +1256,12 @@ background: var(--c-code-bg); box-shadow: inset 0 -2px 0 0 var(--c-accent); } + + .compare-code-tabs__radio--#{$i}:checked + ~ .compare-code-tabs__panel--#{$i} { + visibility: visible; + pointer-events: auto; + } } } @@ -1332,6 +1369,11 @@ // `overflow-x: auto` makes the inner table scrollable when its // content needs more than the column allows. Scrollbar is forced // visible (not auto-hidden) so the user has affordance. + // ---- Frame: shared container for data tables -------------------- + // Frame now provides INTERNAL breathing room (24px top/sides, 20px + // bottom) so a headline + table presented inside read as a single + // framed artifact, not as a table that fills its container edge-to- + // edge. Matches the visual rhythm of the code-block (28/32 padding). .compare-frame { width: 100%; background: var(--c-code-bg); @@ -1340,7 +1382,7 @@ overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; - padding: 0; + padding: 24px 24px 20px; // Firefox scrollbar-width: thin; @@ -1360,29 +1402,64 @@ } } - // Data-table footnotes that follow a .compare-frame are part of the - // same artifact — pull them inside visually with mono treatment. + // ---- Data-table footnote (sits OUTSIDE the frame) --------------- + // Mono small. Lives below the frame as a sibling, acting as the + // methodology line for the figure. .compare-data-table__footnote { font-family: var(--c-font-mono); font-size: 12px; + line-height: 1.55; letter-spacing: 0.02em; color: var(--c-text-quiet); - margin-top: 12px; + margin: 12px 0 0; max-width: 880px; + + strong { + color: var(--c-text-meta); + font-weight: 600; + } + + code { + background: rgba(0, 0, 0, 0.04); + padding: 1px 5px; + border-radius: 3px; + font-size: 0.95em; + white-space: nowrap; + } } - // Headline conclusion above every data table — pairs with the left-side - // H3 subtitle. The H3 frames the question; the headline gives the - // takeaway. Sans-serif and weighted to feel like a sibling of the H3. + // ---- Data-table headline (sits INSIDE the frame, above the table) - + // Acts as the figure's title: a one-line conclusion that says what the + // reader should take from the numbers. Stem display, semibold, dark. + // Bottom hairline separates it from the table data below — same + // weight as the header underline so the eye reads three bands inside + // the frame: headline · headers · data. .compare-data-table__headline { - font-family: var(--c-font-display, Stem, Arial, sans-serif); - font-size: 15px; - font-weight: 600; + font-family: var(--c-font-display); + font-size: 16px; line-height: 1.4; + font-weight: 600; + letter-spacing: -0.01em; color: var(--c-text); - letter-spacing: -0.005em; - margin: 0 0 14px; - max-width: 880px; + margin: 0 0 16px; + padding: 0 0 16px; + border-bottom: 1px solid var(--c-code-border); + + strong { + color: var(--c-text); + font-weight: 700; + } + + // Inline numbers inside the headline get tabular treatment so + // "AnyCable holds 999,954" lines up cleanly. + code, + .t-num { + font-family: var(--c-font-mono); + font-variant-numeric: tabular-nums; + font-size: 0.92em; + background: transparent; + padding: 0; + } } // ---- Kill stray inline text-align: center on headings/content ---- @@ -1473,48 +1550,91 @@ } } - // ---- Data tables: sit inside .compare-frame, no own chrome ------- - // The frame provides the bg + border + radius. Table itself is - // transparent so the unified light-#fafafa surface reads as a single - // artifact, like the code block. Tighter padding + smaller body - // font lets dense tables fit in the 50% media column without - // horizontal overflow. + // ---- Data tables: refined typographic hierarchy ---------------- + // Sits inside .compare-frame which provides bg + border + radius + + // 24px of breathing room. Table itself is transparent. + // + // Reading hierarchy inside the frame: + // ─ Headline (16px Stem 600, hairline below) + // ─ Column headers (11px mono caps, stronger hairline below) + // ─ Data rows (13.5px Stem, tabular nums, hairline between rows) + // ─ (last row's hairline is removed; frame bottom padding closes it) + // + // Column alignment: first column left (labels), others right (numbers). + // Authors can opt into center via `style="text-align: center"` on a + // specific / when the data is glyph-y (✓ / ⚠ / n/a). .compare-data-table { + width: 100%; background: transparent; border: none; border-radius: 0; - font-size: 13px; + border-collapse: collapse; + font-size: 13.5px; + line-height: 1.45; th { font-family: var(--c-font-mono); font-size: 11px; - line-height: 1.3; - letter-spacing: var(--c-tracking-label); + line-height: 1.35; + letter-spacing: 0.06em; + text-transform: uppercase; color: var(--c-text-meta); font-weight: 500; background: transparent; border-bottom: 1px solid var(--c-code-border); - padding: 12px 10px; + padding: 0 12px 12px; + text-align: right; + vertical-align: bottom; // Allow headers to wrap so dense tables fit in the 50% column. - // The original compare.scss forced `white-space: nowrap`. white-space: normal; + + &:first-child { + text-align: left; + padding-left: 0; + } + &:last-child { + padding-right: 0; + } } td { font-variant-numeric: tabular-nums; border-bottom: 1px solid var(--c-code-border); - padding: 10px 10px; + padding: 12px 12px; + color: var(--c-text); + vertical-align: baseline; + text-align: right; + + &:first-child { + text-align: left; + padding-left: 0; + color: var(--c-text); + font-weight: 500; + } + &:last-child { + padding-right: 0; + } + + // Secondary text within a cell (e.g. "Socket.io default" → "default") + // sits muted on the same line as the primary label. + .t-mute, + .t-tiny { + color: var(--c-text-quiet); + } } + // Last row: no bottom border. Frame padding-bottom (20px) finishes + // the table cleanly without a redundant hairline near the frame edge. tr:last-child td { border-bottom: none; } + // Row tints — sit ON the existing border-bottom (no border collision). tr.is-row-best { - background: rgba(22, 163, 74, 0.06); + background: rgba(22, 163, 74, 0.05); } tr.is-row-worst { - background: rgba(185, 28, 28, 0.06); + background: rgba(185, 28, 28, 0.05); } td.is-best, @@ -1692,9 +1812,11 @@ } // ---- CTA: lab-notebook footer rather than marketing hero --------- + // No `border-top` — the FAQ rubric above already provides a border- + // bottom hairline. Adding our own here doubled the line at the + // transition. .compare-cta { padding: 128px 64px 96px; - border-top: 1px solid var(--c-border); text-align: center; &__title { @@ -1733,6 +1855,12 @@ border-bottom: 1px solid var(--c-border); border-radius: 0; + // The last item's bottom border duplicates the FAQ rubric's section + // divider just below; drop it so the FAQ stack ends cleanly. + &:last-child { + border-bottom: none; + } + &:hover, &[open] { border-color: var(--c-border); @@ -1756,9 +1884,101 @@ } // ---- Heading anchor (#) hidden by default, shown on hover -------- + // Bumped color from --c-text-faint (#bbb, 2.2:1 contrast — fails AA) + // to --c-text-quiet (#777, 4.7:1) so the # is readable when visible. + .compare-rubric__title .heading-anchor, + .compare-rubric__subtitle .heading-anchor, + .compare-tldr__title .heading-anchor { + color: var(--c-text-quiet); + } + .compare-rubric__title:not(:hover) .heading-anchor, .compare-rubric__subtitle:not(:hover) .heading-anchor, .compare-tldr__title:not(:hover) .heading-anchor { opacity: 0; } + + // =================================================================== + // ACCESSIBILITY + USER PREFERENCES + // =================================================================== + + // ---- Focus-visible: visible keyboard focus on every interactive --- + // Single accent outline + 2px offset, rendered only when navigation + // comes from the keyboard (not on click). `outline` is used (not + // box-shadow) so it follows border-radius and never affects layout. + a:focus-visible, + button:focus-visible, + summary:focus-visible, + label:focus-visible, + input:focus-visible, + [tabindex]:focus-visible { + outline: 2px solid var(--c-accent); + outline-offset: 2px; + border-radius: 2px; + } + + // Code-tab radios are visually hidden but receive focus. Forward the + // focus ring onto the matching
-

uWS leads on raw deliveries/sec; AnyCable OSS and Pro keep up while holding the smoothest tail. Socket.io default's tail balloons under HTTP-pool concurrency.

+

All five servers sustain 1M deliveries with 100% delivery. uWS is fastest at the floor; CSR and AnyCable hold the tightest tails. Default Socket.io's tail is the loosest.

- - + + - - - - - + + + + +
Setup 1 instance, HTTP pool=16Deliv/secSetup 10K subs, 1M deliveries, HTTP pool=16Delivered p50 p99
Socket.io default56,8181.60 s14.98 s
Socket.io + CSR62,5200.70 s3.13 s
uWS332,3360.22 s3.23 s
AnyCable OSS207,4260.39 s3.08 s
AnyCable Pro205,1700.47 s3.29 s
Socket.io default100%0.74 s3.93 s
Socket.io + CSR100%0.50 s2.26 s
uWS100%0.22 s3.17 s
AnyCable OSS100%0.36 s3.13 s
AnyCable Pro100%0.37 s3.93 s
-

uWS's app.publish is the fastest hot path (C++). AnyCable OSS and Pro track each other; Pro's wins are memory and the embedded broker, not raw msg/sec. Default Socket.io's p99 balloons under HTTP-pool concurrency; CSR's Redis Streams adapter handles it cleanly.

+

External publisher posts to /_broadcast at concurrency 16, 100 messages every 10 ms (target 1M deliveries/sec). uWS's app.publish is the fastest C++ hot path. AnyCable OSS and Pro track each other on raw msg/sec; Pro's wins are memory and the embedded broker. Default Socket.io's tail is roughly 2× Pro's at the same target.

@@ -476,10 +476,10 @@

Whispers: client-to-client updates that byp - Socket.io roomsEmulated64.1%2.24 s9.74 s + Socket.io roomsEmulated62.3%1.78 s12.07 s uWS topicsEmulated100%10 ms21 ms - AnyCable OSSNative100%17 ms140 ms - AnyCable ProNative100%22 ms64 ms + AnyCable OSSNative100%18 ms78 ms + AnyCable ProNative100%18 ms64 ms

1K clients in 10 rooms (100 peers/room), 2 Hz whispers for 30 s. Each whisper fans out to 99 peers, so each client receives ~200 msgs/sec. Socket.io's rooms emulation re-emits through the server, Express can't keep up, and a third of the deliveries drop. AnyCable and uWS bypass the hop and stay under 200 ms p99. At 10K clients the bench-runner saturates client-side, which inflates latencies and confuses the delivery story; 1K × 100 peers/room is the cleanest comparison of the fanout paths.

@@ -499,12 +499,12 @@

How reliable is delivery?how often messages are lost and how long the recovery window drags. Both come down to whether the protocol carries replay state.

- Default Socket.io is at-most-once; so is uWS. CSR adds replay (opt-in, adapter constraints). AnyCable ships reliable streams by default: per-stream history, epoch + offset, restart-survivable with NATS or Redis. Under simulated WiFi jitter (1 s TCP drops every ~15 s): AnyCable and CSR deliver 100%; default Socket.io and uWS lose ~13-14%. + Default Socket.io is at-most-once; so is uWS. CSR adds replay (opt-in, adapter constraints). AnyCable ships reliable streams by default: per-stream history, epoch + offset, restart-survivable with NATS or Redis. Under simulated WiFi jitter (1 s TCP drops every ~15 s) at 10K subscribers, AnyCable delivers 100%, CSR resumes about 20% of reconnects cleanly to land at 76%, and the no-replay options drop most of the test once the server can't keep up with the reconnect storm.

-

Replay protocols (AnyCable, CSR) deliver 100%. Default Socket.io and uWS lose 13-14% on every WiFi blip.

+

Replay protocols (AnyCable, CSR) deliver under heavy jitter. Default Socket.io and uWS drop most messages once reconnects start failing.

@@ -513,33 +513,29 @@

How reliable is delivery?Lost of 1.2M

- - - - + + + - - - - - - + + + + - - + + - - + @@ -547,7 +543,6 @@

How reliable is delivery?0

- @@ -555,11 +550,10 @@

How reliable is delivery?0

-
p95 p99 replay tailServer mem
Socket.io default~87%156,8561.1 s27.5%870,0000.29 s no replay675 MB
Socket.io + CSR72.3%081.6 s116 s627 MB75.5%294,00080 s107 s
uWS topics86.4%163,37134.6%785,000 no replay72 MBno replay
AnyCable OSS4.1 s 6.2 s760 MB
AnyCable Pro4.1 s 6.2 s271 MB
-

10K clients, 120 broadcasts, each client TCP-drops 1 s every ~15 s. 1.2M expected deliveries. No-replay (default Socket.io, uWS) loses during disconnect windows. CSR's retention window is the limit: under heavy jitter only ~16% of disconnects resume cleanly. AnyCable replays across the full disconnect, 100% delivery, sub-7 s p99. uWS reconnects fast but messages in the gap are gone, nothing to replay. Methodology.

+

10K clients, 120 broadcasts, each client TCP-drops 1 s every ~15 s. 1.2M expected deliveries. Socket.io and uWS deliver well when reconnects succeed, but each WiFi blip is also a chance to lose the connection entirely; the page numbers reflect what users actually see when reconnect storms overlap with broadcast load. CSR resumes ~20% of disconnects cleanly; the rest fall back to live-from-now. CSR's lost count counts what fell outside the retention window; in-gap losses on the resumed connections are zero. AnyCable replays across the full disconnect, 100% delivery, sub-7 s p99. Methodology.

@@ -734,12 +728,12 @@

FAQ How does AnyCable compare on performance? - +
Three workloads on the same 32 vCPU / 32 GB Railway box, every option as a standalone WS service.

1M-connection idle: uWS 1,018,366 / 5.45 GB. AnyCable Pro 822,037 / 14.8 GB (lightest with built-in replay). AnyCable OSS 821,877 / 28.3 GB. Socket.io caps at 119,826 (Node event loop saturates handshakes).

10K reconnecting clients under WiFi jitter: AnyCable delivers 100% (p99 ~6 s replay tail). Socket.io+CSR resumes about 20% of reconnects cleanly, lands at 76% delivery with a much longer replay tail. Default Socket.io and uWS drop most of the test (27% and 35%) once the server can't absorb the reconnect storm.

Broadcast throughput at 1M deliveries: all five sustain 100% delivery; uWS holds the floor at 0.22 s p50, AnyCable Pro and CSR hold the tightest p99 around 3 s. Reproduce.

What about uWebSockets.js? It's faster than Socket.io. -
uWS is genuinely faster on the wire — we measured it. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 14.80 GB at 822K). The "10× faster than Socket.io" claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses ~14% of messages — the same loss profile as Socket.io without CSR — because both at-most-once and embedded in the same Node process as your app. uWS solves “Socket.io’s wire is too heavy”; it doesn't solve “we lose messages during disruption” or “every deploy hits our users.”
+
uWS is genuinely faster on the wire, we measured it. At 1M idle connections it uses 5.45 GB (5.4 KB per conn) vs AnyCable Pro's 14.8 GB at 822K (18 KB per conn). The “10× faster than Socket.io” claim is honest. But uWS is a WebSocket library, not a realtime framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it drops down to 35% delivery, similar to default Socket.io, because both are at-most-once and embedded in the same Node process as your app. uWS solves “Socket.io's wire is too heavy.” It doesn't solve “we lose messages during disruption” or “every deploy hits our users.”
@@ -839,7 +833,7 @@

Run AnyCable in your Node app

"name": "How does AnyCable compare on performance?", "acceptedAnswer": { "@type": "Answer", - "text": "Three workloads, 32 vCPU / 32 GB Railway box, every option standalone. Idle 1M target: uWS 1,018,366 / 5.45 GB; AnyCable Pro 822,037 / 14.8 GB; AnyCable OSS 821,877 / 28.3 GB; Socket.io caps at 119,826. WiFi jitter at 10K: AnyCable and CSR deliver 100% (p99 ~6 s and ~8 s); default Socket.io and uWS lose ~13-14%. Broadcast throughput at 1M deliv/sec target (HTTP pool=16): uWS 332K, AnyCable OSS 207K, Pro 205K, Socket.io+CSR 62K, default 57K; AnyCable holds the lowest p99 (~3.1 s). Source: github.com/irinanazarova/anycable-socketio-benchmarks." + "text": "Three workloads, 32 vCPU / 32 GB Railway box, every option standalone. Idle 1M target: uWS 1,018,366 / 5.45 GB; AnyCable Pro 822,037 / 14.8 GB; AnyCable OSS 821,877 / 28.3 GB; Socket.io caps at 119,826. WiFi jitter at 10K: AnyCable delivers 100% (p99 ~6 s replay tail); CSR resumes ~20% of reconnects to land at 76%; default Socket.io and uWS drop to 27% and 35% once reconnect storms overlap broadcast load. Broadcast throughput at 1M deliveries: all five sustain 100% delivery; uWS holds the floor at 0.22 s p50; AnyCable Pro and CSR hold the tightest p99 around 3 s. Source: github.com/irinanazarova/anycable-socketio-benchmarks." } }, { @@ -847,7 +841,7 @@

Run AnyCable in your Node app

"name": "What about uWebSockets.js? It's faster than Socket.io.", "acceptedAnswer": { "@type": "Answer", - "text": "uWS is genuinely faster on the wire — measured. At 10K reconnecting clients it uses 72 MB of server memory (vs AnyCable Pro's 271 MB), and at 1M idle it uses 5.45 GB (vs AnyCable Pro's 14.80 GB at 822K). The '10× faster than Socket.io' claim is honest. But uWS is a WebSocket library, not a real-time framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it loses about 14% of messages — the same loss profile as Socket.io without CSR — because both are at-most-once and embedded in the same Node process as your app. uWS solves 'Socket.io's wire is too heavy'; it doesn't solve 'we lose messages during disruption' or 'every deploy hits our users.'" + "text": "uWS is genuinely faster on the wire, measured. At 1M idle connections it uses 5.45 GB (5.4 KB per conn) vs AnyCable Pro's 14.8 GB at 822K (18 KB per conn). The '10x faster than Socket.io' claim is honest. But uWS is a WebSocket library, not a realtime framework: no replay buffer, no broker, no separate-process deploy resilience. Under jitter it drops to 35% delivery, similar to default Socket.io, because both are at-most-once and embedded in the same Node process as your app. uWS solves 'Socket.io's wire is too heavy.' It doesn't solve 'we lose messages during disruption' or 'every deploy hits our users.'" } }, { From 5d5beb915f080530afa97d4b6ddb1634590e694b Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Thu, 4 Jun 2026 14:08:50 +0100 Subject: [PATCH 53/57] Compare/Socket.io: soften column divider + trim FOOTPRINT label * .compare-rubric__content { border-right } switches from --c-border (#e8e8e8) to --c-border-soft (#f0f0f0). When the prose column is much taller than the media column (or vice versa), the divider was reading as an orphan line through empty space. Lighter hairline recedes visually while still marking the column boundary. * Hero strip 'Footprint' card label trims 'RAM per idle connection, single-node load test' to 'RAM per idle connection'. Matches the one-line height of the Latency and Reliability card labels. --- src/compare/socket-io/index.html | 2 +- src/modules/blocks/compare-spine.scss | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/compare/socket-io/index.html b/src/compare/socket-io/index.html index 09c72ff..d3c8ce5 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/socket-io/index.html @@ -65,7 +65,7 @@

-
Footprint: RAM per idle connection, single-node load test
+
Footprint: RAM per idle connection
Socket.io caps ~120k/node
diff --git a/src/modules/blocks/compare-spine.scss b/src/modules/blocks/compare-spine.scss index eb941e0..30874c4 100644 --- a/src/modules/blocks/compare-spine.scss +++ b/src/modules/blocks/compare-spine.scss @@ -992,7 +992,11 @@ &__content { padding: 80px 64px; background: var(--c-paper); - border-right: 1px solid var(--c-border); + // Softer border (--c-border-soft #f0f0f0 vs --c-border #e8e8e8) + // so the column divider recedes when the media column ends + // before the prose column (or vice versa), instead of marching + // through empty space as an orphan line. + border-right: 1px solid var(--c-border-soft); @include mediaMax($tablet) { background: transparent; From 53aaf1840fcfa0689cfd862f8dc75c6f33a12f61 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Thu, 4 Jun 2026 14:56:01 +0100 Subject: [PATCH 54/57] Compare: rename /socket-io to /nodejs-websocket + SEO Phase A MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discoverability rework based on the LLM audit. The compare page ranked invisible for every non-branded Node.js WebSocket query; Ably's /compare/rails-anycable-vs-socketio outranked us for our own comparison. Phase A closes the page-level gaps: * git mv src/compare/socket-io → src/compare/nodejs-websocket. History preserved. * netlify.toml: 301 redirects /compare/socket-io and /socket-io/ to the new slug so existing inbound links transfer rank. * pageTitle: 'Node.js WebSocket Server Comparison: Socket.io vs uWebSockets.js vs AnyCable (2026 Benchmark)'. Query-bearing keywords up front. * pageDescription rewritten to lead with 'Node.js WebSocket setups' and explicitly include 'self-hosted Pusher and Ably alternative'. * Hero kicker: new compare-hero__kicker class (mono 13px accent red, above the H1). Copy: 'Looking for a Socket.io alternative for production Node.js?'. Captures the literal search query. * Hero subtitle rewritten: covers Node.js, TypeScript, Bun, Deno and the self-hosted Pusher/Ably alternative claim in one line. * TechArticle JSON-LD: url + mainEntityOfPage updated to new slug, dateModified bumped to 2026-06-04, headline + description aligned with the new title, and the 'about' keyword array expanded from 15 to 25 entities (Self-hosted Pusher alternative, Self-hosted Ably alternative, Liveblocks alternative, PartyKit alternative, Bun WebSocket server, Next.js realtime WebSocket, etc.). * llms.txt + sitemap.xml: URL references updated. * compare-spine.scss: docblock URL updated; new .compare-hero__kicker styling (mono small, accent red, 20px below the H1). --- netlify.toml | 15 +++++++++ .../index.html | 33 ++++++++++++------- .../style/index.html | 0 src/modules/blocks/compare-spine.scss | 16 ++++++++- src/public/llms.txt | 6 ++-- src/public/sitemap.xml | 2 +- 6 files changed, 56 insertions(+), 16 deletions(-) rename src/compare/{socket-io => nodejs-websocket}/index.html (96%) rename src/compare/{socket-io => nodejs-websocket}/style/index.html (100%) diff --git a/netlify.toml b/netlify.toml index 4a3cf70..52b95c8 100644 --- a/netlify.toml +++ b/netlify.toml @@ -3,6 +3,21 @@ publish="/dist" command="yarn build" +# Preserve SEO from the original /compare/socket-io URL after we +# renamed the slug to /compare/nodejs-websocket for organic +# discoverability ("Node.js WebSocket server" / "Socket.io alternative" +# queries). The new page is the canonical home; old inbound links +# transfer their rank via the 301. +[[redirects]] + from = "/compare/socket-io" + to = "/compare/nodejs-websocket" + status = 301 + +[[redirects]] + from = "/compare/socket-io/" + to = "/compare/nodejs-websocket/" + status = 301 + [[redirects]] from = "/anycasts/anycable-v1-4-reliable-real-time-for-all/" to = "https://blog.anycable.io/p/anycable-v14-reliable-real-time-for-all" diff --git a/src/compare/socket-io/index.html b/src/compare/nodejs-websocket/index.html similarity index 96% rename from src/compare/socket-io/index.html rename to src/compare/nodejs-websocket/index.html index d3c8ce5..35e74d1 100644 --- a/src/compare/socket-io/index.html +++ b/src/compare/nodejs-websocket/index.html @@ -1,6 +1,6 @@ - {{> dochead pageTitle="AnyCable vs Socket.io vs uWebSockets.js | Benchmarking WebSocket infrastructure for JS/TS apps" pageDescription="A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions: how fast (roundtrip latency at 1k/10k and broadcast throughput, including whispers), how reliable (message delivery under WiFi-drop jitter), and how efficient to scale (single-node load test with avalanche behavior at deploy). Tested in both embedded and standalone topologies. All numbers reproducible from the open-source benchmark repo." pageUrl="https://anycable.io/compare/socket-io"}} + {{> dochead pageTitle="Node.js WebSocket Server Comparison: Socket.io vs uWebSockets.js vs AnyCable (2026 Benchmark)" pageDescription="Benchmark of five Node.js WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection State Recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Compared on memory per connection, broadcast throughput, message delivery under WiFi-drop jitter, and deploy resilience. Includes a self-hosted Pusher and Ably alternative for teams who want flat pricing. All numbers reproducible from the open-source benchmark repo." pageUrl="https://anycable.io/compare/nodejs-websocket"}}
{{> header}} @@ -13,11 +13,12 @@
+

Looking for a Socket.io alternative for production Node.js?

Socket.io vs uWS vs AnyCable

- Benchmarking realtime WebSocket infrastructure options for JS/TS serverfull apps. + A 2026 benchmark of five WebSocket setups for Node.js, TypeScript, Bun, and Deno apps. Self-hosted alternative to Pusher Channels and Ably included.

@@ -926,12 +927,12 @@

Run AnyCable in your Node app

{ "@context": "https://schema.org", "@type": "TechArticle", - "headline": "AnyCable vs Socket.io vs uWebSockets.js: Benchmarking WebSocket infrastructure for JS/TS apps", - "description": "A measured comparison of five WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection state recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions answered with reproducible benchmarks: (1) how fast: roundtrip latency at 1k/10k subscribers, broadcast throughput, plus whispers (client-to-client without backend hop) where AnyCable competes with Liveblocks, Yjs, PartyKit; (2) how reliable: message delivery under WiFi-drop jitter, separating at-most-once protocols (default Socket.io, uWS) from those with replay state (Socket.io+CSR, AnyCable); (3) how efficient to scale: single-node load (RAM per connection, max concurrent) plus the avalanche behavior at app deploy that decides whether in-process WS is sustainable. Tested in embedded and standalone topologies. All numbers reproducible from the open-source bench repo at github.com/irinanazarova/anycable-socketio-benchmarks.", - "url": "https://anycable.io/compare/socket-io", - "mainEntityOfPage": "https://anycable.io/compare/socket-io", + "headline": "Node.js WebSocket Server Comparison: Socket.io vs uWebSockets.js vs AnyCable (2026 Benchmark)", + "description": "A measured comparison of five Node.js WebSocket setups on identical hardware: default Socket.io, Socket.io with Connection State Recovery, uWebSockets.js, AnyCable OSS, AnyCable Pro. Three questions answered with reproducible benchmarks: (1) how fast: roundtrip latency at 1k/10k subscribers, broadcast throughput, plus whispers (client-to-client without backend hop) where AnyCable competes with Liveblocks, Yjs, PartyKit; (2) how reliable: message delivery under WiFi-drop jitter, separating at-most-once protocols (default Socket.io, uWS) from those with replay state (Socket.io+CSR, AnyCable); (3) how efficient to scale: single-node load (RAM per connection, max concurrent) plus the avalanche behavior at app deploy that decides whether in-process WS is sustainable. Covers Node.js, TypeScript, Bun, and Deno. Includes a self-hosted alternative to Pusher Channels and Ably. Tested in embedded and standalone topologies. All numbers reproducible from the open-source bench repo.", + "url": "https://anycable.io/compare/nodejs-websocket", + "mainEntityOfPage": "https://anycable.io/compare/nodejs-websocket", "datePublished": "2026-04-30", - "dateModified": "2026-05-28", + "dateModified": "2026-06-04", "author": { "@type": "Organization", "name": "AnyCable team", @@ -943,21 +944,31 @@

Run AnyCable in your Node app

"url": "https://evilmartians.com/" }, "about": [ - "WebSocket server", + "Node.js WebSocket server", "Socket.io alternative", + "Socket.io alternatives", + "Best WebSocket server for Node.js", + "Self-hosted Pusher alternative", + "Self-hosted Ably alternative", "uWebSockets.js comparison", "uWebSockets.js vs Socket.io", "uWebSockets.js vs AnyCable", "Real-time messaging", - "Node.js WebSocket server", - "WebSocket scaling", + "WebSocket scaling Node.js", "Node.js WebSocket scaling bottleneck", "1,000,000 WebSocket connections single instance", "AnyCable Pro vs open source", "AnyCable Pro memory efficiency", "Connection State Recovery", "WebSocket delivery guarantees", - "WebSocket deploy resilience" + "WebSocket deploy resilience", + "WebSocket connections drop on deploy", + "Next.js realtime WebSocket", + "Bun WebSocket server", + "TypeScript WebSocket server", + "Liveblocks alternative", + "PartyKit alternative", + "Whispers client-to-client WebSocket" ] } diff --git a/src/compare/socket-io/style/index.html b/src/compare/nodejs-websocket/style/index.html similarity index 100% rename from src/compare/socket-io/style/index.html rename to src/compare/nodejs-websocket/style/index.html diff --git a/src/modules/blocks/compare-spine.scss b/src/modules/blocks/compare-spine.scss index 30874c4..ff4d7b1 100644 --- a/src/modules/blocks/compare-spine.scss +++ b/src/modules/blocks/compare-spine.scss @@ -4,7 +4,7 @@ // // QUICK REFERENCE for authors (writing or extending a compare page): // -// See the live style index at: /compare/socket-io/style/ +// See the live style index at: /compare/nodejs-websocket/style/ // (Every block authors should reach for, rendered + with HTML.) // // Activation: add `c-spine` to the page's
@@ -716,6 +716,20 @@ margin-bottom: 24px; } + // Kicker sits above the H1: a small mono accent line that + // carries query-bait keywords for organic discovery + // ("Looking for a Socket.io alternative..."). Doesn't compete with + // the display H1 for attention; reads as a pointed lead-in. + &__kicker { + font-family: var(--c-font-mono); + font-size: 13px; + line-height: 1.4; + letter-spacing: 0.04em; + color: var(--c-accent); + margin: 0 0 20px; + max-width: 640px; + } + &__title { font-family: var(--c-font-display); // Push the display bigger and bolder than the soft migration step. diff --git a/src/public/llms.txt b/src/public/llms.txt index a6842c7..c0703bd 100644 --- a/src/public/llms.txt +++ b/src/public/llms.txt @@ -109,7 +109,7 @@ AnyCable is used in production by 50+ companies including: - You want to use any backend, not just Node.js. - You want a single WebSocket-server instance to hold hundreds of thousands of connections without app-level sharding. Same 1,000,000-connection idle test on identical Railway hardware (Pro tier, 32 vCPU / 32 GB RAM allocated): single-instance Socket.io accepted 119,826 and rejected ~880K (single Node event loop saturated under handshake load); open-source AnyCable held 993,994, peaking at the 32 GB RAM ceiling (~33 KB per connection); AnyCable Pro held 999,954 on only 19 GB (~19 KB per connection — about 1.7× more memory-efficient than OSS at 1M, with 13 GB headroom). Reaching 1M with Socket.io requires a fleet of Node processes behind a Redis adapter; AnyCable does it in one process. -Full benchmark, methodology, and reproducible scripts: https://anycable.io/compare/socket-io +Full benchmark, methodology, and reproducible scripts: https://anycable.io/compare/nodejs-websocket Bench source: https://github.com/irinanazarova/anycable-socketio-benchmarks ## When to use AnyCable instead of Django Channels or FastAPI WebSockets (Python) @@ -147,7 +147,7 @@ Both are Go-based WebSocket servers with pub/sub. Key differences: ### AnyCable vs Socket.io: comparison with benchmarks A methodology-first comparison of three real configurations at 10,000 concurrent clients on identical Railway infrastructure: default Socket.io (no opt-ins), Socket.io with Connection State Recovery enabled, and AnyCable. Headline results: default Socket.io delivers 87.4% under jitter, both Socket.io+CSR and AnyCable deliver 100% — but CSR's replay tail is 7x slower (p99 9.0s vs 1.0s; max 12.0s vs 3.5s). Also covers deploy resilience (Socket.io drops every connection on every deploy; AnyCable doesn't restart with the app) and idle connection capacity (200,000 connections on one anycable-go instance using 8.3 GB and ~0.8 vCPU). All numbers reproducible from the open-source bench repo. -https://anycable.io/compare/socket-io +https://anycable.io/compare/nodejs-websocket ### The pitfalls of LLM streaming over WebSockets When streaming AI/LLM responses chunk by chunk over WebSockets, two problems emerge: messages arrive out of order under concurrent load, and brief disconnections cause chunks to be lost. This results in garbled or incomplete AI output for users. AnyCable solves both with publication logs (guaranteed ordering) and at-least-once delivery (automatic recovery of missed chunks). This applies regardless of backend — whether your Python app calls OpenAI or your Rails app calls Anthropic. @@ -176,7 +176,7 @@ https://www.youtube.com/watch?v=J68QOBLEItY - Blog: https://blog.anycable.io - Pricing: https://anycable.io/#pricing - Customers: https://anycable.io/#customers -- Compare: AnyCable vs Socket.io (with benchmarks): https://anycable.io/compare/socket-io +- Compare: AnyCable vs Socket.io (with benchmarks): https://anycable.io/compare/nodejs-websocket - Bench source for the Socket.io comparison: https://github.com/irinanazarova/anycable-socketio-benchmarks - Laravel SDK: https://docs.anycable.io/guides/laravel - JavaScript client: https://github.com/anycable/anycable-client diff --git a/src/public/sitemap.xml b/src/public/sitemap.xml index a5f30a6..fbed256 100644 --- a/src/public/sitemap.xml +++ b/src/public/sitemap.xml @@ -6,7 +6,7 @@ 1.0 - https://anycable.io/compare/socket-io + https://anycable.io/compare/nodejs-websocket monthly 0.9 From 14d5eb65fcc8209c6ece8e91eb6b853a158d4bca Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Fri, 5 Jun 2026 15:20:41 +0100 Subject: [PATCH 55/57] Compare: dot ornament marks sub-chapter breaks on the column line The vertical column divider previously trailed off through empty space at every sub-section boundary inside a multi-part rubric (e.g. between 'Roundtrip latency' and 'Broadcast throughput' inside the latency rubric). The break read as accidental. A small 6px dot in --c-text-faint now sits on the divider line at the top of each non-first sub-section, marking the break as a deliberate sub-chapter transition. Scoped to: * Desktop only (mediaMin tablet). On mobile there's no two-column layout so no divider to anchor against. * Adjacent sub-sections within the same rubric. Different rubrics are separated by the rubric's own border-bottom; major-section transitions don't need a dot. * Hidden when either side of the boundary is a --full sub-section. No column divider exists there, so the dot would float in empty space. Implementation uses @at-root to escape the .c-spine .compare-rubric ancestor chain. Sass would otherwise compile '& + &' inside the rubric block to a mixed-combinator selector '.compare-rubric__section + .c-spine .compare-rubric__section' that doesn't match adjacent siblings. --- src/modules/blocks/compare-spine.scss | 36 +++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/modules/blocks/compare-spine.scss b/src/modules/blocks/compare-spine.scss index ff4d7b1..96a996b 100644 --- a/src/modules/blocks/compare-spine.scss +++ b/src/modules/blocks/compare-spine.scss @@ -991,6 +991,7 @@ &__section { padding: 24px 0; + position: relative; // Reset the sub-counter at every new section. counter-reset: subrubric; @@ -1003,6 +1004,41 @@ } } + // Sub-chapter break indicator: small dot sits on the column + // divider line at the boundary between two adjacent sub-sections. + // Marks the break as deliberate instead of letting the hairline + // trail off through empty space when prose and media columns + // end at different heights. Only renders on desktop where the + // two-column layout (and the vertical divider) exists. + // `@at-root` escapes the .c-spine .compare-rubric ancestor stack + // because Sass would otherwise generate + // .compare-rubric__section + .c-spine .compare-rubric__section + // (mixing combinators) for the sibling selector. + @include mediaMin($tablet) { + @at-root .c-spine .compare-rubric__section + .compare-rubric__section::before { + content: ''; + position: absolute; + top: 0; + left: 50%; + transform: translate(-50%, -50%); + width: 6px; + height: 6px; + background: var(--c-text-faint); + border-radius: 50%; + z-index: 1; + } + + // Hide the dot when either side of the boundary is a full-width + // sub-section: there's no column divider for the dot to anchor + // against, so it'd float in empty space. + @at-root { + .c-spine .compare-rubric__section + .compare-rubric__section:has(.compare-rubric__content--full)::before, + .c-spine .compare-rubric__section:has(.compare-rubric__content--full) + .compare-rubric__section::before { + display: none; + } + } + } + &__content { padding: 80px 64px; background: var(--c-paper); From 9b49b8ec156f15ea094559b03dc3308693a20e39 Mon Sep 17 00:00:00 2001 From: Irina Nazarova Date: Fri, 5 Jun 2026 16:19:04 +0100 Subject: [PATCH 56/57] dev: add --strictPort to yarn dev Without strictPort, two Vite servers happily co-exist on port 5173 when one binds IPv4 wildcard and another binds IPv6 localhost. The OS routes requests between them, so a stale Vite from another project intermittently serves an HTML redirect for every path on this site. JS that fetches anything (e.g. our github-button.ts `await response.json()`) then sees `` and throws "Unexpected token '<', " Date: Fri, 5 Jun 2026 16:27:42 +0100 Subject: [PATCH 57/57] Compare: update impact text + add 3 Tier 1 rows to feature matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'What this breaks' impact text was running the old reliability numbers (CSR p99 ~8s vs AnyCable ~6s) which no longer matched the reliability table on the same page. The latest benchmarks have CSR's p99 replay tail at ~107 s on the 20% of reconnects where replay succeeds at all; the other 80% fall back to live-from-now (75.5% delivery overall). AnyCable still lands at ~6 s with 100% delivery. Text updated to: 'Lost messages cluster around network events — exactly when the user is watching. CSR resumes about 20% of reconnects cleanly, and its p99 replay tail stretches to nearly two minutes, which reads as "the app stopped working." AnyCable replays the same messages in about 6 seconds, with full delivery. For sequential workloads, both loss and delay break the flow.' The feature matrix 'Replay latency p99' row also still said ~8 s for CSR; updated to ~107 s. Three Tier 1 rows added: * 'No external broker required' (after Multi-node setup): calls out that AnyCable Pro embeds NATS so multi-node deployments don't need a separate broker process at all. Others all need Redis (or DIY). * 'Graceful drain on restart' (after Deploy resilience): when anycable-go itself restarts, --shutdown_timeout + slow-drain mode spreads disconnects over the window. Socket.io / uWS have no equivalent. * 'Binary wire format' (after Backend language): AnyCable Pro ships first-class msgpack and protobuf protocols (actioncable-v1-msgpack, actioncable-v1-protobuf) with matching JS client encoders. Socket.io has socket.io-msgpack-parser as a third-party plugin; uWS is DIY. --- src/compare/nodejs-websocket/index.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/compare/nodejs-websocket/index.html b/src/compare/nodejs-websocket/index.html index 35e74d1..0027d4e 100644 --- a/src/compare/nodejs-websocket/index.html +++ b/src/compare/nodejs-websocket/index.html @@ -568,7 +568,7 @@

How reliable is delivery?

What this breaks

-

Lost messages cluster around network events — exactly when the user is watching. CSR recovers them, but 8 seconds late reads as “the app froze”; AnyCable lands them around 6. For sequential workloads, loss and delay both break the flow.

+

Lost messages cluster around network events — exactly when the user is watching. CSR resumes about 20% of reconnects cleanly, and its p99 replay tail stretches to nearly two minutes, which reads as “the app stopped working.” AnyCable replays the same messages in about 6 seconds, with full delivery. For sequential workloads, both loss and delay break the flow.

@@ -661,13 +661,16 @@

What you don't have to build

Reliable delivery
NoYes (opt-in)NoYes (default) - Replay latency p99lost~8 slost~6 s + Replay latency p99lost~107 slost~6 s Survives server restartNoRedis Streams / MongoNoNATS / Redis Multi-node setupRedis pub/subRedis pub/sub incompat.DIY (Redis)Any broker + No external broker requiredRedis requiredRedis Streams requiredDIYEmbedded NATS (Pro) Deploy resilienceAll dropAll dropAll dropConnections survive + Graceful drain on restartNoneNoneNoneConfigurable (slow-drain Pro) Presence trackingDIYDIYDIYBuilt-in AuthenticationDIYDIYDIYJWT, signed streams Backend languageNode.js onlyNode.js onlyNode.js onlyAny (HTTP API) + Binary wire formatThird-party pluginThird-party pluginDIYmsgpack, protobuf (Pro) MonitoringAdmin UIAdmin UIDIYPrometheus & StatsD