Skip to content

Add Rails Action Cable comparison page#58

Open
irinanazarova wants to merge 14 commits into
masterfrom
compare-rails-actioncable
Open

Add Rails Action Cable comparison page#58
irinanazarova wants to merge 14 commits into
masterfrom
compare-rails-actioncable

Conversation

@irinanazarova

Copy link
Copy Markdown
Contributor

New /compare/rails-actioncable page: Action Cable vs Solid Cable vs AsyncCable vs AnyCable, the Rails counterpart to the existing /compare/nodejs-websocket page.

What it compares

Four Rails WebSocket adapters running the same Rails 8.1 app on identical Railway hardware, in one shared-tenant window:

Adapter Runtime Topology
Action Cable Puma + Redis in-process
Solid Cable Puma + database in-process
AsyncCable Falcon (fibers) in-process
AnyCable Go gateway + Rails RPC out-of-process

All four speak the Action Cable API, so channels and Turbo Streams are unchanged. The only thing that varies is what runs the WebSockets.

Findings (sharded load fleet)

  • Latency: AnyCable fastest at every scale (7 ms p50 at 5K vs 13 ms Action Cable, 20 ms AsyncCable, 74 ms Solid Cable)
  • Jitter delivery: AnyCable 99.9% vs ~78% for the three at-most-once in-process adapters (base actioncable-v1-json has no resume; AnyCable's actioncable-v1-ext-json replays per-stream history)
  • Deploy survival: AnyCable drops 0 connections on an app redeploy; the in-process adapters drop all 5,000 and reconnect ~96% in ~8 s
  • Capacity: all four hold 10K subscribers at 100% delivery; pushed to failure on a 32 GB box, the Puma adapters wall at ~52K (file-descriptor ceiling), Falcon at ~97K (memory), and AnyCable held 600K with zero failures

Implementation

  • Reuses the global compare.scss / compare-spine.scss system and the document skeleton from the nodejs-websocket page
  • Inline architecture SVG (in-process vs out-of-process gateway), code-tab widget, four data tables, feature matrix, FAQ accordion
  • FAQPage + TechArticle JSON-LD
  • All numbers reproducible from the open-source bench repo; raw results committed there

Benchmark methodology and raw data live in the separate bench repo (nodejs-websocket-bench).

New /compare/rails-actioncable page comparing four Rails WebSocket
adapters on identical hardware: Action Cable (Puma+Redis), Solid Cable
(Puma+DB), AsyncCable (Falcon), and AnyCable (out-of-process Go gateway).

All four speak the Action Cable API, so channels and Turbo Streams are
unchanged. Measured across four rubrics with a sharded load fleet:

- Latency: AnyCable fastest at every scale (7ms p50 at 5K)
- Jitter delivery: AnyCable 99.9% vs ~78% for the at-most-once in-process trio
- Deploy survival: AnyCable 0 dropped vs all-drop for in-process adapters
- Capacity: all hold 10K at 100%; pushed to failure, Puma adapters wall
  ~52K (fd limit), Falcon ~97K (memory), AnyCable held 600K with 0 failures

Reuses the compare.scss / compare-spine.scss system and the document
skeleton from the nodejs-websocket page. Includes FAQPage + TechArticle
JSON-LD.
@bolt-new-by-stackblitz

Copy link
Copy Markdown

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

Applied feedback from reviews channeling DHH, David Cramer, Matt Biilmann,
Guillermo Rauch, a scaling-Rails-shop CTO, and LLM/answer-engine retrieval:

- Lead the callout with the architectural choice (in-process vs separate
  Go gateway) and the undeniable wins (jitter, deploy), not latency; state
  the two precise causes (process for deploys/capacity, protocol for delivery)
- Add a "Should you switch?" decision box with triggers + one-line rollback
- Add a "How it was measured" methodology disclosure (box, driver locality,
  runtimes, per-cell counts, single-pass honesty, raw-results link)
- De-FUD "What this breaks": acknowledge the refetch-on-reconnect workaround,
  drop "nobody can recover"; label the jitter run as a deliberate stress test;
  soften deploy "stranded" to "still reconnecting when we stopped watching"
- Present 600K as a floor (ran out of load fleet, not server); drop the
  ~700K extrapolation
- Note latency is where the four sit closest (7 vs 13 ms barely felt)
- Hero card labels: outcome phrases instead of questions; deploy values
  carry the count ("all 5,000"); jitter p99 column made non-numeric (n/a)
- New FAQs: which adapter to choose, what is AnyCable, Action Cable
  connection limit, do I need Go, does 99.9% need a client swap
- Sharpen the architecture thesis (subtitle, section heading, figcaption)
- Add keywords + a machine-readable Dataset JSON-LD; mirror new FAQs in
  FAQPage JSON-LD
Report how long the in-process adapters stay dropped before reconnecting
(~7.5 to 8 s, ~96% recovered) instead of only the reconnect percentage.
Reorder the deploy table to Dropped / Down for / Reconnected, redefine the
footnote, and add downtime to the Dataset JSON-LD.
New hand-authored /compare landing page that hosts every comparison,
reusing the existing compare-hub-* CSS component set (added earlier,
previously unused). Hero, then three stack cards (Node.js and Rails
live, Laravel/PaaS coming soon), a three-topology landscape map with
AnyCable tagged "You are here", and a CTA. Adds CollectionPage +
ItemList JSON-LD.

Point the footer "Comparisons" link at /compare instead of the
Node.js page now that there is a hub.
Add the /compare hub and the new Rails benchmark page to llms.txt and
llms-full.txt: a benchmark-backed "when to use AnyCable instead of
Action Cable / Solid Cable / AsyncCable" section, a Rails article entry
and (in the full file) a complete Rails methodology + results section
with latency, jitter, capacity, and deploy tables. Link the hub and the
Rails page in both Links sections.
- Capacity: the 1M idle figures were stale (OSS 993,994 / Pro 999,954).
  Correct to the published compare-page numbers: OSS 821,877 @ 28.3 GB
  (~34 KB/conn), Pro 822,037 @ 14.8 GB (~18 KB/conn), uWS 1,018,366 @
  5.45 GB. Fixes an internal contradiction where llms.txt disagreed with
  itself. Recompute Pro efficiency (~1.9x, was 1.7x).
- Avalanche: replace the single-scale Socket.io table with the page's
  scale sweep (5K-25K), adding the uWS data point.
- Pricing: Managed is free for early users (up to 2,000 connections),
  not "$29/month" (no such price on the site). Pro is unlimited
  instances and connections.
- Action Cable "~500 connections" claim corrected to the benchmarked
  in-process ceiling (~52,000) and reframed around deploy drops and
  at-most-once.
- Customer figures aligned to the site / web sources: CompanyCam
  $452M raised / $2B valuation (was $30M+), Qualified acquired by
  Salesforce (was $95M raised), Circle $33M, Fullscript $240M+. Drop
  Jane's "$100M+ raised" (that figure is revenue/secondary, not capital).
Match the gem's own naming: the README heading and Ruby constant are
Async::Cable (the gem package stays async-cable). Update the Rails
comparison page (title, hero, tables, prose, FAQ, JSON-LD) and the
Rails sections of both llms files.
Surface per-connection idle RAM for all four adapters in the "How far
does it scale?" table (~45 KB Action Cable, ~50 KB Solid Cable, ~290 KB
Async::Cable, ~47 KB AnyCable), computed as peak RSS / connections held.
Flag Falcon's ~290 KB as the outlier that makes it memory-bound. Move
the figure out of the prose footnote to avoid duplication.
…ped"

"All 5,000 dropped" is a given for any in-process server, so it earned a
test we didn't need. Drop that column and lead with the metrics that
actually inform: blackout duration (Down for) and the share that
reconnects within the window (Recovered, with the residual still-out
count). Every in-process adapter still dropped all 5,000; that fact moves
to the footnote as context. Reframe the hero card to downtime (7.5-8 s vs
0 s) and update the headline and Dataset JSON-LD to match.
The "Down for" figure is recoveryTimeMs from the avalanche runner,
defined as time from redeploy until 95% of clients are back. Say so
explicitly in the column header, headline, footnote, hero card, and
Dataset JSON-LD instead of the vaguer "blackout before reconnect".
Refresh every metric on the Rails comparison page from the re-run with matched
process counts, a runner fleet sized so the server is always the bottleneck,
and each adapter driven by the JS client it ships in production (@rails/actioncable
for Action Cable / Solid Cable / Async::Cable, @anycable/core for AnyCable).

- Latency p50/p99 at 1K/5K, hero card and table.
- Jitter delivery 78% -> 53%: with the real default client the at-most-once
  adapters lose about half under 2s drops; AnyCable resumes to 100%.
- Deploy survival 7.5s -> 13.5s to 95% reconnect: the stock client's poll-based
  reconnect is the slow part. Reframed as Recovery / Reconnected to match the
  Node.js comparison page.
- Capacity: 8-worker Puma now holds ~217K (was measured at a single worker),
  above Falcon's ~105K memory wall; AnyCable 600K -> 792K floor.
- FAQ answers and both JSON-LD blocks (FAQPage + Dataset) updated to match.
@irinanazarova

Copy link
Copy Markdown
Contributor Author

Refreshed every number on the page from a corrected 4-adapter re-run. Key methodology fixes vs the first pass:

  • Matched process counts. The stock puma.rb omits the workers directive, so Puma had silently run a single worker. It now runs its full 8, which changes the capacity picture.
  • No load-generator limits. The fleet was sharded up to ~66 drivers (~250 cables each) so the server, not the drivers, is always the bottleneck.
  • Each server on its real client. Action Cable / Solid Cable / Async::Cable are driven by the official @rails/actioncable (native poll-based reconnect, no resume); AnyCable by @anycable/core (extended protocol, resume). Using AnyCable's client for everything had flattered Action Cable's reconnect.

What moved:

Metric Was Now
Jitter delivery (in-process) 78% ~53%
Deploy, time to 95% 7.5s 13.5s
Capacity, Puma ~52K ~217K
Capacity, Falcon ~97K ~105K
Capacity, AnyCable 600K 792K

Latency is roughly unchanged (AnyCable 6ms p50 at 5K). The deploy section is reframed as Recovery / Reconnected to match the Node.js comparison page. Tooling lives in anycable/nodejs-websocket-bench#2.

…nd hero deploy to 14s

- Jitter table p50 was from an intermediate run; corrected to the final
  native-client run (Action Cable 12ms, Solid Cable 68ms, Async::Cable 14ms).
- Round the hero deploy card to 14s (the detailed table keeps 13.5/13.8s).
- AnyCable 10K p99 stays 31ms (representative clean-gateway tail; the p95 is 13ms).
…d; honest deploy/avalanche

- Order every hero card + table Action Cable, Solid Cable, Async::Cable, AnyCable
  (Redis/DB/Falcon tags), AnyCable last.
- Capacity: relabel max-held as an idle ceiling; add an under-sustained-load
  comparison (2 msg/s: in-process p50 degrades to 149-571ms by 50K and delivery
  breaks 75-85% by 100K; AnyCable ~33ms). Drop the idle-based '~200K = fine'
  switch advice; tie it to broadcast load. Note AnyCable's cold-connect storm is
  a separate RPC-sizing axis, not steady-state.
- Deploy: modern jittered clients blunt the herd (even a constrained box
  recovers, ~23s on 2 workers vs ~14s on 8), so the honest point is the
  guaranteed per-deploy disruption (gap + missed messages + re-auth/re-subscribe),
  which AnyCable removes entirely (gateway holds connections).
…tnote

The media column is a flex column that centers its content (justify-content:
center). Combined with the sticky variant's max-height + overflow-y: auto, a
frame taller than the viewport (e.g. the capacity section's two stacked tables,
or a long jitter footnote on a short window) got centered, pushing its bottom
past the clip edge where the footnote was cut off and unreachable by scroll.
Anchor sticky media with justify-content: flex-start so overflow scrolls
normally and the last line stays reachable.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant