Solid Cable: scaling WebSockets
This course follows one Turbo refresh broadcast through Solid Cable, the database-backed pub/sub adapter for Action Cable. It shows that broadcast_refresh_to is a synchronous write with no queue, and how the solid_cable_messages table stores each message with a signed 64-bit channel hash. It covers the listener thread that polls for new rows, its two watermarks and its shutdown semaphore. It explains the trim lottery that deletes expired rows, why polling cost grows with process count, and how to read a live cable table as evidence.
What you'll learn
- ✓How broadcast_refresh_to writes synchronously through Action Cable with no queue
- ✓The single solid_cable_messages table and its signed SHA256 channel hash
- ✓How the listener thread polls, tracks watermarks and shuts down safely
- ✓How the trim lottery deletes expired messages during broadcasts
- ✓Why polling cost grows with the number of app server processes
- ✓Reading row counts and id ranges in a live cable table
broadcast_refresh_to is a synchronous write
Three files in this app call the same line. Admin::Catalog::SaveCourseService, Admin::Catalog::ReorderCourseService and Admin::Catalog::ReorderLessonService each end with Turbo::StreamsChannel.broadcast_refresh_to("catalog_roadmap"), and app/views/catalog/courses/index.html.erb opens with <%=…
Read the full lesson free →Curriculum
7 lessonsbroadcast_refresh_to is a synchronous write
broadcast_refresh_to sends a 45-character refresh tag through Action Cable synchronously, with no queue, custom channel or route.
One table, a hashed channel, and a raw insert
Solid Cable stores messages in one table, hashes channels into signed 64-bit integers, and writes with a bare insert.
The listener: one thread that only reads
A single listener thread per process polls for new rows, tracks two watermarks, and uses a semaphore for safe shutdown.
The trim lottery: a two percent chance per broadcast
Each broadcast runs TrimJob inline, which deletes up to 100 expired rows about 2% of the time.
Polling is a per-process cost, not a per-thread one
Listener threads scale with Puma processes, so polling queries grow with WEB_CONCURRENCY whatever the message volume.
This app's real numbers: a live cable table, fully overdue for a trim
A development cable table full of overdue rows shows that trimming only happens when broadcasts keep arriving.
Putting it together: one refresh, two processes, one table
One reorder broadcast traced from synchronous write through trim lottery, per-process polling and the browser's refresh GET.