{
    "version": "https://jsonfeed.org/version/1",
    "title": "",
    "home_page_url": "https://centrifugal.dev/blog",
    "items": [
        {
            "id": "https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections",
            "content_html": "<p>A lot of real-time systems spend most of their time doing nothing. Think about a notifications service, a presence dashboard, a \"new message\" badge — the connections sit open for hours, and messages arrive rarely. The interesting scaling question for such systems is not throughput, but how cheaply a single node can hold a large pool of connections that are mostly idle.</p>\n<p>An idle connection still isn't free: even with no application traffic, every connection costs a bit of CPU (Centrifugo runs periodic per-connection work — an application-level ping, presence refresh, subscription checks) and a bit of memory (goroutines, buffers, protocol state). Multiply that by hundreds of thousands and the constant, do-nothing overhead becomes the thing that decides how many connections fit on a node.</p>\n<p>This post measures that overhead and shows how five Centrifugo PRO options reduce it. One cuts CPU, the other four cut memory — and almost every byte they save turns out to come from a specific piece of the connection: a stack or a buffer.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"the-benchmark\">The benchmark<a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#the-benchmark\" class=\"hash-link\" aria-label=\"Direct link to The benchmark\" title=\"Direct link to The benchmark\" translate=\"no\">​</a></h2>\n<p>We hold 200,000 real WebSocket connections open against a single Centrifugo node and leave them idle. \"Idle\" here means the connections do nothing but stay alive — the server sends its application-level ping every 25 seconds (the default), the clients answer it, and that is the only traffic. Then we measure the server process: CPU (in cores) and resident memory (RSS).</p>\n<ul>\n<li class=\"\">Node: one Centrifugo instance on a 16 vCPU cloud VM (AMD EPYC-Genoa, KVM), 30 GB RAM, Ubuntu Linux.</li>\n<li class=\"\">Connections: 200,000 bidirectional WebSocket, protocol v2, <strong>no subscriptions</strong>.</li>\n<li class=\"\">Ping interval: 25s (default). Everything else at defaults unless stated.</li>\n<li class=\"\">Every figure below is the mean of two independent full runs of the whole matrix; the two rounds agreed within ~3% on CPU and ~0.5% on memory.</li>\n</ul>\n<p>We measure the Centrifugo process specifically — CPU as the process's own user + system time over a steady-state window well after the connection ramp, memory as its RSS after a forced GC — so the numbers are the server's own consumption, not the whole machine's.</p>\n<p>One thing RSS does not include, and which matters when you size a machine: the kernel keeps its own structures for every socket — the <code>tcp_sock</code>, an inode, a dentry, a file — and charges them to slab, not to your process. We measured about 4 KB per socket, and it shows up in neither <code>ps</code>, nor <code>top</code>'s RSS column, nor Go's runtime memory stats. It is real memory and cgroup v2 does count it against a container limit, so add it to every per-connection figure below when planning capacity.</p>\n<p>At these connection counts the server uses well under one core out of sixteen, so it's the per-connection overhead we're isolating, not saturation.</p>\n<p>Everything below is the server's own cost — the number that decides how many connections a node holds.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"baseline\">Baseline<a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#baseline\" class=\"hash-link\" aria-label=\"Direct link to Baseline\" title=\"Direct link to Baseline\" translate=\"no\">​</a></h2>\n<p>With default settings, holding 200,000 idle connections costs:</p>\n<table><thead><tr><th>Metric</th><th>Value</th><th>Per connection</th></tr></thead><tbody><tr><td>CPU</td><td>~0.94 cores</td><td>~4.7 µs/s</td></tr><tr><td>Memory (RSS)</td><td>~5.4 GB</td><td>~27 KB</td></tr><tr><td>Goroutines</td><td>~400,000</td><td>2</td></tr></tbody></table>\n<p>Two goroutines per connection: one reads from the socket, one owns writes to it. The CPU is almost entirely the machinery of the periodic per-connection timers — for 200k connections that is a lot of independent timers for the Go runtime to track and fire.</p>\n<p>It's worth splitting that ~27 KB, because it explains everything that follows:</p>\n<table><thead><tr><th>Component</th><th>Per connection</th></tr></thead><tbody><tr><td>Goroutine stacks</td><td>~12.3 KB</td></tr><tr><td>Heap (buffers, protocol state, client struct)</td><td>~12.7 KB</td></tr><tr><td>Runtime overhead</td><td>~2 KB</td></tr></tbody></table>\n<p>Nearly half of an idle connection is goroutine stacks. Keep that number in mind: two of the memory options below are, underneath, different ways of making it smaller, and the other two go after the heap half.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"cutting-cpu-batch_periodic_events\">Cutting CPU: <code>batch_periodic_events</code><a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#cutting-cpu-batch_periodic_events\" class=\"hash-link\" aria-label=\"Direct link to cutting-cpu-batch_periodic_events\" title=\"Direct link to cutting-cpu-batch_periodic_events\" translate=\"no\">​</a></h2>\n<p>Each connection runs several periodic tasks — the keepalive ping, presence updates, subscription expiration checks. By default each is an independent timer. With many connections, the Go runtime spends real CPU maintaining all of them and waking a goroutine every time one fires.</p>\n<p><code>batch_periodic_events</code> groups these per-connection events onto a shared timing wheel instead of scheduling each one independently. The periodic work still happens on schedule; it's just dispatched in batches rather than as hundreds of thousands of separate timer wakeups.</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"client\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"batch_periodic_events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>The effect on the same 200,000 idle connections:</p>\n<table><thead><tr><th>Metric</th><th>Default</th><th><code>batch_periodic_events</code></th></tr></thead><tbody><tr><td>CPU</td><td>~0.94 cores</td><td><strong>~0.41 cores</strong></td></tr><tr><td>Memory (RSS)</td><td>~5.4 GB</td><td>~5.4 GB</td></tr></tbody></table>\n<p>That's over 2× less CPU to hold the same pool — down to ~2.0 µs/s per connection. Memory is unchanged to within a rounding error: this option is purely about how periodic timers are scheduled.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"cutting-memory-part-1-the-writer-goroutine\">Cutting memory, part 1: the writer goroutine<a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#cutting-memory-part-1-the-writer-goroutine\" class=\"hash-link\" aria-label=\"Direct link to Cutting memory, part 1: the writer goroutine\" title=\"Direct link to Cutting memory, part 1: the writer goroutine\" translate=\"no\">​</a></h2>\n<p>By default every connection has a dedicated goroutine that owns writes to its socket and blocks waiting for something to send. For an idle connection that goroutine does nothing almost all the time — but it still exists, and a parked goroutine still costs a stack.</p>\n<p><code>write_delay</code> batches outgoing messages: instead of writing each message immediately, the connection waits a short delay to coalesce whatever arrives into a single write, which reduces system calls under load. <code>write_with_timer</code> changes <em>how</em> that batching waits — it uses a shared timer to trigger the flush instead of a dedicated per-connection writer goroutine. For this post the interesting part is the side effect at rest: idle connections no longer each hold a writer goroutine.</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"client\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"write_delay\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"100ms\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"write_with_timer\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<table><thead><tr><th>Metric</th><th>Default</th><th><code>write_with_timer</code></th></tr></thead><tbody><tr><td>Goroutines</td><td>~400,000</td><td><strong>~200,000</strong></td></tr><tr><td>Memory (RSS)</td><td>~27 KB/conn</td><td><strong>~22.4 KB/conn</strong></td></tr><tr><td>Goroutine stacks</td><td>~12.3 KB/conn</td><td><strong>~8.2 KB/conn</strong></td></tr></tbody></table>\n<p>One goroutine per connection instead of two, and the saving is exactly one 4 KB goroutine stack.</p>\n<p>The tradeoff to be aware of: <code>write_delay</code> adds latency equal to the delay (100 ms here) before a message is flushed, because the connection waits that long to batch. For notification-style workloads that is usually invisible; for latency-sensitive request/reply it may not be what you want. Tune the delay, or leave it off, accordingly.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"cutting-memory-part-2-the-read-goroutines-stack\">Cutting memory, part 2: the read goroutine's stack<a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#cutting-memory-part-2-the-read-goroutines-stack\" class=\"hash-link\" aria-label=\"Direct link to Cutting memory, part 2: the read goroutine's stack\" title=\"Direct link to Cutting memory, part 2: the read goroutine's stack\" translate=\"no\">​</a></h2>\n<p>That leaves one goroutine per connection — the one reading the socket. It can't be removed the same way; something has to wait for the client's next message. But it turns out most of what it costs isn't the goroutine. It's the <em>size</em> of its stack.</p>\n<p>Go grows a goroutine's stack on demand, doubling it whenever a call chain doesn't fit, and — this is the part that matters — a long-lived goroutine never really gives that space back. So the stack grows to the deepest call the goroutine has ever made, and stays there.</p>\n<p>A connection's read loop reads a frame and then processes the command inline: the event handler, the broker, the broadcast fan-out. That is a deep call chain, and it only has to happen <strong>once</strong> — even just the initial <code>connect</code> command — for the read goroutine to grow to 8 KB and hold it for the entire life of the connection, however idle it then becomes.</p>\n<p><code>process_commands_off_read_loop</code> moves command processing to a short-lived goroutine that exits when the frame is done. The read loop itself stays shallow. The deep stack still gets used, but it goes back to the Go runtime's stack pool and is reused by the next connection that needs it, instead of being pinned to a connection that spends its life waiting.</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"websocket\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"process_commands_off_read_loop\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<table><thead><tr><th>Metric</th><th>Default</th><th><code>process_commands_off_read_loop</code></th></tr></thead><tbody><tr><td>Goroutines</td><td>~400,000</td><td>~400,000 (unchanged)</td></tr><tr><td>Memory (RSS)</td><td>~27 KB/conn</td><td><strong>~23 KB/conn</strong></td></tr><tr><td>Goroutine stacks</td><td>~12.3 KB/conn</td><td><strong>~8.2 KB/conn</strong></td></tr></tbody></table>\n<p>Same saving as removing the writer goroutine — around 4 KB per connection — but by a completely different route: the goroutine count doesn't change at all, its stack just halves from 8 KB to 4 KB. The two options are independent, and they stack.</p>\n<p>The tradeoff: handing each frame to another goroutine costs a goroutine start and two scheduler handoffs. For a connection that is genuinely busy, that shows up as a small increase in per-command latency. For a large pool of mostly-idle connections — the case this whole post is about — you are trading an operation that almost never happens for memory you pay for constantly.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"cutting-memory-part-3-use_write_buffer_pool\">Cutting memory, part 3: <code>use_write_buffer_pool</code><a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#cutting-memory-part-3-use_write_buffer_pool\" class=\"hash-link\" aria-label=\"Direct link to cutting-memory-part-3-use_write_buffer_pool\" title=\"Direct link to cutting-memory-part-3-use_write_buffer_pool\" translate=\"no\">​</a></h2>\n<p>The last piece isn't a goroutine at all. Every WebSocket connection holds a write buffer — 4 KB by default — whether or not it is writing anything. <code>use_write_buffer_pool</code> makes connections share a pool of write buffers instead, so a buffer is held only while a write is actually in flight.</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"websocket\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"use_write_buffer_pool\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<table><thead><tr><th>Metric</th><th>Default</th><th><code>use_write_buffer_pool</code></th></tr></thead><tbody><tr><td>Memory (RSS)</td><td>~27 KB/conn</td><td><strong>~24.0 KB/conn</strong></td></tr><tr><td>Heap</td><td>~12.7 KB/conn</td><td><strong>~8.6 KB/conn</strong></td></tr></tbody></table>\n<p>Another ~4 KB per connection, this time out of the heap rather than the stacks. Under sustained write load the pool sees more contention than a per-connection buffer, so this option is meant for pools that are idle most of the time.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"cutting-memory-part-4-read_buffer_size\">Cutting memory, part 4: <code>read_buffer_size</code><a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#cutting-memory-part-4-read_buffer_size\" class=\"hash-link\" aria-label=\"Direct link to cutting-memory-part-4-read_buffer_size\" title=\"Direct link to cutting-memory-part-4-read_buffer_size\" translate=\"no\">​</a></h2>\n<p>With the write buffer pooled, a heap profile of the tuned server shows that the\nWebSocket read buffer is now 47% of everything left. Each\nconnection holds one, and by default it takes the 4 KB buffer that <code>net/http</code>\nallocated for the request it was upgraded from.</p>\n<p>Centrifugo lets you size it. A client command is small — a subscribe, a publish,\na pong — so 4 KB is far more than a mostly-idle connection needs.</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"websocket\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"read_buffer_size\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1024</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<table><thead><tr><th>Metric</th><th>All previous options</th><th><code>+ read_buffer_size: 1024</code></th></tr></thead><tbody><tr><td>CPU</td><td>0.46 cores</td><td><strong>0.45 cores</strong></td></tr><tr><td>Memory (RSS)</td><td>15.3 KB/conn</td><td><strong>10.9 KB/conn</strong></td></tr><tr><td>Heap</td><td>8.3 KB/conn</td><td><strong>5.1 KB/conn</strong></td></tr></tbody></table>\n<p>That's another 4.4 KB per connection, and CPU does not move — the largest\nsingle memory saving in this post, and the cheapest.</p>\n<p>Dropping to 512 bytes saves only another 0.5 KB, so 1024 is the sweet spot. The\ntradeoff is that a client sending messages larger than the buffer needs more\nread syscalls to pull them in — irrelevant for command traffic, worth thinking\nabout if your clients push large payloads.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"everything-together\">Everything together<a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#everything-together\" class=\"hash-link\" aria-label=\"Direct link to Everything together\" title=\"Direct link to Everything together\" translate=\"no\">​</a></h2>\n<p>The five options target different costs, so they stack cleanly:</p>\n<table><thead><tr><th>Configuration</th><th>CPU</th><th>RSS/conn</th><th>Stacks/conn</th><th>Heap/conn</th></tr></thead><tbody><tr><td>Default</td><td>0.94 cores</td><td>27.0 KB</td><td>12.3 KB</td><td>12.7 KB</td></tr><tr><td><code>batch_periodic_events</code></td><td><strong>0.41 cores</strong></td><td>26.9 KB</td><td>12.3 KB</td><td>12.6 KB</td></tr><tr><td><code>write_with_timer</code></td><td>0.98 cores</td><td>22.4 KB</td><td>8.2 KB</td><td>12.3 KB</td></tr><tr><td><code>process_commands_off_read_loop</code></td><td>0.96 cores</td><td>22.9 KB</td><td>8.2 KB</td><td>12.8 KB</td></tr><tr><td><code>use_write_buffer_pool</code></td><td>0.92 cores</td><td>24.0 KB</td><td>12.3 KB</td><td>8.6 KB</td></tr><tr><td><strong>First four together</strong></td><td><strong>0.46 cores</strong></td><td><strong>15.3 KB</strong></td><td><strong>4.2 KB</strong></td><td><strong>8.3 KB</strong></td></tr><tr><td><strong>All five (+ <code>read_buffer_size: 1024</code>)</strong></td><td><strong>0.45 cores</strong></td><td><strong>10.9 KB</strong></td><td><strong>4.2 KB</strong></td><td><strong>5.1 KB</strong></td></tr></tbody></table>\n<p>With everything enabled, one node holds 200,000 idle connections for ~0.45 cores and ~2.2 GB — 52% less CPU and 60% less memory than the default. Per connection that's roughly 2.2 µs/s of CPU and ~11 KB of RAM.</p>\n<p>Look at where the memory went. The default 27 KB was 12.3 KB of stacks and 12.7 KB of heap; tuned, those are 4.2 KB and 5.1 KB. The four memory options each removed roughly 4 KB — one goroutine stack, one goroutine's <em>worth</em> of stack, one write buffer, and most of a read buffer.</p>\n<p>A note on CPU: the memory options are not entirely free. The first three individually cost between 2% and 7% CPU and together around 12%, while <code>read_buffer_size</code> costs nothing measurable. <code>batch_periodic_events</code> more than pays for all of it, which is why the combined figure is still less than half the default — but if you enable only the memory options, expect CPU to tick up slightly.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"when-this-matters\">When this matters<a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#when-this-matters\" class=\"hash-link\" aria-label=\"Direct link to When this matters\" title=\"Direct link to When this matters\" translate=\"no\">​</a></h2>\n<p>These options shine for the workload we benchmarked: large pools of mostly-idle connections. Notifications, presence, live badges, dashboards that update occasionally — anywhere the connection count is high and the message rate per connection is low.</p>\n<p>They matter less for high-throughput workloads. If every connection is receiving a steady stream of messages, the cost moves from the idle overhead we optimized here into the actual message fan-out and socket writes, which is a different benchmark. (<code>write_delay</code> still helps there — batching writes cuts system calls under load — but that's a throughput story, not the idle-pool story of this post.)</p>\n<p>A couple of practical notes:</p>\n<ul>\n<li class=\"\"><code>batch_periodic_events</code> is currently marked experimental. It's a scheduling change with no effect on delivery semantics, but the flag reflects that it's a newer option.</li>\n<li class=\"\"><code>write_with_timer</code> only applies when <code>write_delay</code> is set — the timer-based flush is meaningless without a delay to flush against.</li>\n<li class=\"\"><code>process_commands_off_read_loop</code> and <code>use_write_buffer_pool</code> are WebSocket-transport options, so they live under <code>websocket</code> rather than <code>client</code>. They do not affect SSE or HTTP-streaming connections, which don't hold a persistent read loop in the same way.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"one-more-thing-a-million-connections\">One more thing: a million connections<a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#one-more-thing-a-million-connections\" class=\"hash-link\" aria-label=\"Direct link to One more thing: a million connections\" title=\"Direct link to One more thing: a million connections\" translate=\"no\">​</a></h2>\n<p>Everything above is measured at 200,000, which is where the full comparison is\npractical to run. But once the tuned configuration was in hand, the obvious\nquestion was how far the same machine goes.</p>\n<p>We pointed the load generator at the same 16 vCPU / 30 GB node with every option\nfrom this post enabled, and asked it for 1,000,000 connections:</p>\n<table><thead><tr><th>Metric</th><th>Value</th><th>Per connection</th></tr></thead><tbody><tr><td>Connections</td><td>1,000,000</td><td></td></tr><tr><td>CPU</td><td>~2.33 cores</td><td>~2.3 µs/s</td></tr><tr><td>Memory (RSS)</td><td>~11.3 GB</td><td>~11.6 KB</td></tr><tr><td>Goroutines</td><td>1,000,163</td><td>1.00</td></tr><tr><td>— of which stacks</td><td>~4.1 GB</td><td>~4.1 KB</td></tr><tr><td>— of which heap</td><td>~5.2 GB</td><td>~5.2 KB</td></tr></tbody></table>\n<p>Add the kernel's own per-socket structures — about 4 KB each, so roughly 4.2 GB\nfor a million sockets — and the true cost of the node is closer to 15.5 GB,\nstill only half the machine.</p>\n<p>Two and a third cores and 11 GB of process memory, on one node, for a million\nidle WebSocket connections. The per-connection cost is only slightly above the 200k figure —\n~11.6 KB against ~10.9 KB — so the tuning holds its shape at five times the\nscale rather than degrading.</p>\n<p>To be clear about what this run is: a single run of one configuration, not the\ntwo-round matrix the rest of the post is built on, and the connections are\nidle — they connect, answer pings, and nothing else. Real traffic adds its own\nbudget on top.</p>\n<p>And one number that is not in the table, because we could not measure it: the\ndefault configuration simply cannot hold a million connections on this machine. At\nthe ~27 KB per connection we measured at 200k, a million of them would need\naround 27 GB of process memory on a 30 GB box — before the kernel's own\nper-socket structures, which add roughly 4 KB per socket on top. The tuned\nconfiguration does it in 11.3 GB and leaves half the machine idle.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"summing-up\">Summing up<a href=\"https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections#summing-up\" class=\"hash-link\" aria-label=\"Direct link to Summing up\" title=\"Direct link to Summing up\" translate=\"no\">​</a></h2>\n<p>An idle connection costs CPU (periodic per-connection timers) and memory — and that memory is mostly goroutine stacks and buffers held by connections that are doing nothing. Centrifugo PRO gives you a knob for each:</p>\n<ul>\n<li class=\"\"><strong><code>client.batch_periodic_events</code></strong> batches the periodic timers — over 2× less CPU.</li>\n<li class=\"\"><strong><code>client.write_delay</code> + <code>write_with_timer</code></strong> drops the per-connection writer goroutine — ~4 KB less per connection.</li>\n<li class=\"\"><strong><code>websocket.process_commands_off_read_loop</code></strong> halves the read goroutine's stack — another ~4 KB.</li>\n<li class=\"\"><strong><code>websocket.use_write_buffer_pool</code></strong> shares write buffers between connections — another ~4 KB.</li>\n<li class=\"\"><strong><code>websocket.read_buffer_size</code></strong> sizes the read buffer to what commands actually need — another ~4.4 KB, the largest saving of them all.</li>\n</ul>\n<p>The split between the <code>client</code> and <code>websocket</code> sections is not cosmetic — it is\nalso the split between what helps everywhere and what does not.</p>\n<p>The two <code>client</code> options act on the connection object itself, which every\ntransport builds the same way, so <code>batch_periodic_events</code> and <code>write_delay</code> +\n<code>write_with_timer</code> help SSE and HTTP-streaming connections too: the periodic\ntimers and the per-connection writer goroutine exist regardless of how the\nconnection was established.</p>\n<p>The three <code>websocket</code> options do not. Two of them size buffers that belong to\nthe WebSocket connection, and <code>process_commands_off_read_loop</code> only means\nanything where there is a long-lived read loop to keep shallow — SSE and\nHTTP-streaming receive client commands over ordinary HTTP requests, whose\ngoroutines are released when the request ends, so there is no persistent stack\nto shrink.</p>\n<p>So if your fleet is not WebSocket, the CPU win and one of the four memory wins\nstill apply.</p>\n<p>Together they roughly halve the CPU and cut the memory of a large idle connection pool by 60% — enough to hold a million idle connections on a single 16-core node with half its RAM to spare. If your workload is a lot of connections that mostly wait, they're worth turning on.</p>",
            "url": "https://centrifugal.dev/blog/2026/08/17/tuning-idle-connections",
            "title": "Tuning Centrifugo PRO for large number of idle WebSocket connections",
            "summary": "An idle WebSocket connection costs CPU and memory even when no messages flow. The post starts with 200k idle connections on a single node, cuts the overhead with five Centrifugo PRO options, then holds a million idle connections on the same node.",
            "date_modified": "2026-08-17T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "websocket",
                "performance"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions",
            "content_html": "<p>Most real-time features start from data that already exists somewhere — orders in an <code>orders</code> table, notifications, a document, a ticket queue. To show it live, the browser needs two things: the current state, and every change that happens after. In a typical app the state comes from an HTTP endpoint and the changes come from a WebSocket subscription.</p>\n<p>Each part is easy on its own. The hard part is joining them correctly, and it turns out to be hard in the same way in every application that keeps state in its own database.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"what-goes-wrong\">What goes wrong<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#what-goes-wrong\" class=\"hash-link\" aria-label=\"Direct link to What goes wrong\" title=\"Direct link to What goes wrong\" translate=\"no\">​</a></h2>\n<p>The natural approach is to load the state first and subscribe after. Three problems hide in this sequence.</p>\n<p>The first is a gap in time. Between the moment the HTTP response was produced and the moment the subscription becomes active, updates keep happening. They are not in the loaded snapshot, and the subscription started too late to catch them, so they are simply lost. The component shows stale data until the user reloads the page.</p>\n<p>The second one shows up later, on reconnects. After a network drop the SDK asks Centrifugo to replay the publications the client missed. Usually it can. But sometimes it can't — the client was offline longer than the history retention, or the stream epoch changed. The subscription then reports <code>recovered: false</code> and the application has to reload state from the backend. While that reload is in flight, new publications keep arriving — the same race as on the first load, except now it can happen on any reconnect.</p>\n<p>The third is ordering between the two sources. If a publication arrives while the state fetch is still running, it has to be applied on top of the fetch result, not before it — otherwise a fresh value gets overwritten by an older one.</p>\n<p>None of this is specific to Centrifugo: any system that combines \"fetch a snapshot\" with \"subscribe to a feed\" runs into the same three problems. <a class=\"\" href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync\">Proper real-time document state synchronization</a> showed how to solve them by hand — buffer publications during the fetch, track versions, re-sync when continuity is lost. It works, but it's a fair amount of code to repeat in every client.</p>\n<p>Stream subscriptions now have this logic built in.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"the-getstate-callback\">The getState callback<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#the-getstate-callback\" class=\"hash-link\" aria-label=\"Direct link to The getState callback\" title=\"Direct link to The getState callback\" translate=\"no\">​</a></h2>\n<p><code>getState</code> is an option on a stream subscription. The SDK calls it before subscribing. Inside the callback the application loads its state and returns the stream position that state corresponds to:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'orders:user_42'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">getState</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// 1. Capture the stream position FIRST.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> pos </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> api</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">getStreamPosition</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'orders:user_42'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// 2. Then load and render your data.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">renderOrders</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> api</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">getOrders</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">42</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// 3. Return the position — the SDK subscribes from exactly here.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">offset</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> pos</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">offset</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">epoch</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> pos</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">epoch</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Incremental updates: catch-up first, then live.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">applyOrderUpdate</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>This is the entire integration. The application answers one question — what is the current state, and at which stream position was it taken — and the SDK takes care of everything else.</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   getState()                    subscribe(recover from pos)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       │                                    │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       ▼                                    ▼</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  ┌─────────┐   pos          ┌───────────────────────────┐</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  │ App API │ ─────────────► │  catch-up publications    │──► live</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  │  + DB   │   state        │  (offset &gt; pos.offset)    │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  └─────────┘                └───────────────────────────┘</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       ▲                                                     │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       │        recovery impossible — reload from scratch     │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       └─────────────────────────────────────────────────────┘</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"what-the-sdk-does-with-it\">What the SDK does with it<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#what-the-sdk-does-with-it\" class=\"hash-link\" aria-label=\"Direct link to What the SDK does with it\" title=\"Direct link to What the SDK does with it\" translate=\"no\">​</a></h2>\n<p>It's worth walking through the behaviour case by case, because it defines how much the application still has to handle on its own.</p>\n<p>On the first subscribe, the SDK runs <code>getState</code> and subscribes with recovery from the returned position. Everything published after that position arrives through the normal <code>publication</code> handler, so catch-up publications and live ones look identical to the application.</p>\n<p>On a normal reconnect, <code>getState</code> is not called. The SDK already holds a valid position, so it lets the server replay the missed publications from channel history. This is exactly what recovery exists for: when thousands of clients reconnect at once, the broker absorbs the load instead of the application database.</p>\n<p>When recovery turns out impossible, <code>getState</code> runs again — and this is the part that replaces the manual re-sync code. A subscription with <code>getState</code> asks the server to reject a subscribe it cannot recover, instead of accepting it with <code>recovered: false</code>. The server responds with the <code>unrecoverable position</code> error, the SDK drops its saved position, and the next subscribe attempt calls <code>getState</code> again: fresh state, fresh position, recovery from there. At no point is the subscription live while the state under it is stale.</p>\n<p>And if <code>getState</code> itself fails — a network error, a database timeout — the SDK emits a subscription error and retries with the usual backoff, so a failed state load never leaves a component half-subscribed.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"read-the-position-first\">Read the position first<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#read-the-position-first\" class=\"hash-link\" aria-label=\"Direct link to Read the position first\" title=\"Direct link to Read the position first\" translate=\"no\">​</a></h2>\n<p>There is one rule the application has to get right inside the callback: read the stream position before reading the data.</p>\n<p>In this order the position becomes a lower bound. Anything committed between the two reads shows up twice — once in the loaded data, and once more as a catch-up publication. Seeing an update twice is harmless, missing one is not, and reading the data first would produce exactly the kind of gap this whole mechanism exists to close.</p>\n<p>Since duplicates are possible, the application must be able to apply the same update twice. Most update shapes already can:</p>\n<ul>\n<li class=\"\"><strong>Absolute values.</strong> \"Status is <code>shipped</code>\", \"price is 42.17\". Applying it twice changes nothing.</li>\n<li class=\"\"><strong>Keyed upserts with a version or timestamp.</strong> Apply by <code>id</code>, keep the newer <code>updated_at</code>. Last write wins.</li>\n<li class=\"\"><strong>Offset-based dedup.</strong> Store the offset the state was loaded at and drop publications at or below it. This one is needed when updates are deltas — \"add 3 items\" is not safe to apply twice.</li>\n</ul>\n<p>And when the data source is a single relational database, the overlap can be removed entirely: wrap both reads in one <code>REPEATABLE READ</code> transaction. Both statements then see the same MVCC snapshot, the returned position is the exact watermark of the loaded data, and catch-up delivers only what was committed strictly after it. Nothing left to reconcile, at the cost of a transaction on the read path.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"where-the-position-comes-from\">Where the position comes from<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#where-the-position-comes-from\" class=\"hash-link\" aria-label=\"Direct link to Where the position comes from\" title=\"Direct link to Where the position comes from\" translate=\"no\">​</a></h2>\n<p><code>getState</code> must return an <code>offset</code> and an <code>epoch</code> for the channel. There are two ways to get them.</p>\n<p>With any broker, the server API works: a <code>history</code> call without a <code>limit</code> returns just the current stream position, without publications:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">curl -X POST http://localhost:8000/api/history \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  -H \"X-API-Key: &lt;key&gt;\" \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  -d '{\"channel\": \"orders:user_42\"}'</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token property\">\"result\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token property\">\"offset\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">174</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token property\">\"epoch\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"xcf4\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>The application's own endpoint calls this, then reads its data, then returns both to the client. Clients never talk to the Centrifugo API directly.</p>\n<p>With the <a class=\"\" href=\"https://centrifugal.dev/docs/server/engines#postgresql-broker\">PostgreSQL stream broker</a>, the position lives in the same database as the data, so both reads happen in one place — and, if needed, in one transaction:</p>\n<div class=\"language-sql codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-sql codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">SELECT</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">FROM</span><span class=\"token plain\"> cf_stream_top_position</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'orders:user_42'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">SELECT</span><span class=\"token plain\"> id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">status</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> updated_at </span><span class=\"token keyword\" style=\"font-style:italic\">FROM</span><span class=\"token plain\"> orders </span><span class=\"token keyword\" style=\"font-style:italic\">WHERE</span><span class=\"token plain\"> user_id </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">42</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>This is what makes the PostgreSQL broker such a natural fit for app-owned state: the position and the rows share a transaction boundary, so the <code>REPEATABLE READ</code> trick above becomes a two-line change. The write side has the same property — <code>cf_stream_publish</code> commits the publication together with the row it announces, which was the subject of <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits\">Transactional publishing for stream subscriptions with PostgreSQL</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"server-configuration\">Server configuration<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#server-configuration\" class=\"hash-link\" aria-label=\"Direct link to Server configuration\" title=\"Direct link to Server configuration\" translate=\"no\">​</a></h2>\n<p><code>getState</code> builds on standard <a class=\"\" href=\"https://centrifugal.dev/docs/server/history_and_recovery\">history and recovery</a>, so the channel namespace needs history and recovery enabled:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"channel\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"namespaces\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"orders\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"history_size\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">100</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"history_ttl\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"300s\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"force_recovery\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Note that the history window only has to cover the reconnect gap, not the lifetime of the data. Anything longer than that is served by <code>getState</code> reloading from the application database — which is the right split: channel history is a bounded cache, the application database is the source of truth.</p>\n<p>Alternatively use <a class=\"\" href=\"https://centrifugal.dev/docs/server/channels#allow_recovery\"><code>allow_recovery</code></a> and let clients opt in per subscription with the <code>recoverable</code> option.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"two-examples\">Two examples<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#two-examples\" class=\"hash-link\" aria-label=\"Direct link to Two examples\" title=\"Direct link to Two examples\" translate=\"no\">​</a></h2>\n<p>The rules above are short, so here are two examples showing how they play out in real systems. Both use the PostgreSQL stream broker, which makes the write side transactional too — but the read side works the same with any broker.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"an-upstream-feed-shaped-into-stored-views\">An upstream feed shaped into stored views<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#an-upstream-feed-shaped-into-stored-views\" class=\"hash-link\" aria-label=\"Direct link to An upstream feed shaped into stored views\" title=\"Direct link to An upstream feed shaped into stored views\" translate=\"no\">​</a></h3>\n<p>The first one is a service that consumes a Kafka topic and maintains aggregated views in PostgreSQL — say, a price board built from a market-data topic. The browser needs the current aggregate, then live updates.</p>\n<p>The obvious wiring is to point Centrifugo at the same Kafka topic and let it fan events out to subscribers in parallel with the aggregator. That works, but it leaves two unrelated offset spaces to bridge: the snapshot row stores a Kafka offset, the live subscription speaks Centrifugo offsets, and the client has to subscribe with a recent stream position and throw away everything older than the snapshot's Kafka offset. This bridging logic is awkward and easy to get subtly wrong.</p>\n<p>There is a simpler wiring: make the aggregator the publisher. For each Kafka batch it processes, it does both things in one PostgreSQL transaction — updates the snapshot and calls <code>cf_stream_publish(...)</code> for the new events. Now the stored aggregate and the change stream can never disagree about what has been observed, and the snapshot row stays minimal: just the aggregate, no offset bookkeeping.</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">─── Write path (single PG txn ties snapshot + publish) ───────────</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   Kafka topic</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       │ batch</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       ▼</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  ┌────────────┐</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  │ Aggregator │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  └─────┬──────┘</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        │   BEGIN</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        │     UPDATE snapshot SET aggregate = ...</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        │     cf_stream_publish(p_channel := ch, p_data := evt)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        │   COMMIT              ← both land atomically</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        ▼</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  ┌──────────────────────────────┐</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  │          PostgreSQL          │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  │   ┌──────────┐ ┌──────────┐  │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  │   │ snapshot │ │cf_stream │  │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  │   │   row    │ │  outbox  │  │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  │   └──────────┘ └────┬─────┘  │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  └────────────────────┬┴────────┘</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                       │ LISTEN/NOTIFY + poll</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                       ▼</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                 ┌──────────────┐</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                 │  Centrifugo  │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                 │ (PG broker)  │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                 └──────────────┘</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">─── Read path (position first; catch-up applied idempotently) ────</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  Browser ──1. GET /state──► App server</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                                 │ pos  = cf_stream_top_position(ch)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                                 │ snap = SELECT aggregate FROM ...</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  Browser ◄──── (snap, pos) ─────┘</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  Browser ──2. subscribe(ch, since=pos)──► Centrifugo</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  Browser ◄─── catch-up from pos → live ──┘</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">             (events committed between the two reads</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">              arrive here; idempotent apply reconciles)</span><br></div></code></pre></div></div>\n<p>The read path is the recipe from the previous sections: <code>cf_stream_top_position</code> first, then the snapshot, then return the captured position. Events committed between the two reads arrive as catch-up on top of the snapshot. A price board reconciles this on its own, since each event carries an absolute price rather than a delta. If the events were deltas, the <code>REPEATABLE READ</code> variant would remove the overlap along with the need for dedup.</p>\n<p>The same shape fits any upstream feed being turned into stored views — Kafka, NATS, CDC, a polling worker. One process owns both the stored aggregate and the change stream, so there is no cross-system offset bridge to maintain.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"internal-writes-partitioned-by-tenant\">Internal writes partitioned by tenant<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#internal-writes-partitioned-by-tenant\" class=\"hash-link\" aria-label=\"Direct link to Internal writes partitioned by tenant\" title=\"Direct link to Internal writes partitioned by tenant\" translate=\"no\">​</a></h3>\n<p>The second example is even more common: one shared table, many independent consumers. The <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits#a-concrete-example-per-tenant-channels\">kitchen-orders demo</a> shows it well — a single <code>orders</code> table across all restaurants, one channel per restaurant (<code>kitchen:{restaurant_id}</code>), and each kitchen display subscribed to its own channel. The write side follows one rule: every code path that changes a row for restaurant X publishes on <code>kitchen:X</code> in the same transaction.</p>\n<p>The read side behind <code>getState</code> is the position-first recipe, with data filtered by restaurant:</p>\n<div class=\"language-sql codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-sql codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">SELECT</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">FROM</span><span class=\"token plain\"> cf_stream_top_position</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'kitchen:42'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">SELECT</span><span class=\"token plain\"> id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">status</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> items</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> updated_at</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">FROM</span><span class=\"token plain\"> orders</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">WHERE</span><span class=\"token plain\"> restaurant_id </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">42</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">AND</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">status</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">IN</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'received'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'preparing'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'ready'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Events carry <code>order_id</code> and <code>updated_at</code>, so the client applies them as upserts with last-write-wins — the keyed-upsert case from the list above, which makes catch-up replay harmless. There are no offsets stored in the application schema and no dedup bookkeeping: the position is captured at read time and handed to the SDK.</p>\n<p>The same client code serves every restaurant — only the channel name and the <code>WHERE</code> clause change. That's what lets this shape scale to thousands of tenants without any per-tenant machinery.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"when-this-pattern-doesnt-fit\">When this pattern doesn't fit<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#when-this-pattern-doesnt-fit\" class=\"hash-link\" aria-label=\"Direct link to When this pattern doesn't fit\" title=\"Direct link to When this pattern doesn't fit\" translate=\"no\">​</a></h2>\n<p>The pattern assumes the data has a home in the application's schema. Plenty of real-time data does not: cursors that live for a few seconds, presence sets, lobby members, device telemetry, feature flags. Building a table, a snapshot endpoint and a change feed for each of those is a lot of machinery for state that is disposable by nature.</p>\n<p>For such data the broker can act as the store instead — see <a class=\"\" href=\"https://centrifugal.dev/docs/server/map_subscriptions\">map subscriptions</a> for keyed collections, and <a class=\"\" href=\"https://centrifugal.dev/docs/server/cache_recovery\">cache recovery mode</a> when a channel only ever needs its latest value.</p>\n<p>A simple rule of thumb: if a <code>SELECT</code> in your own database can already answer \"what is the current state\", then <code>getState</code> on a stream subscription is the smaller design. If it can't, look at the state-carrying subscription types.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"availability\">Availability<a href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions#availability\" class=\"hash-link\" aria-label=\"Direct link to Availability\" title=\"Direct link to Availability\" translate=\"no\">​</a></h2>\n<p>The <code>getState</code> callback is available in Centrifugo v6.8.0+ and is supported by all bidirectional SDKs — <a href=\"https://github.com/centrifugal/centrifuge-js\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">JavaScript</a>, <a href=\"https://github.com/centrifugal/centrifuge-dart\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Dart</a>, <a href=\"https://github.com/centrifugal/centrifuge-swift\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Swift</a>, <a href=\"https://github.com/centrifugal/centrifuge-go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Go</a>, <a href=\"https://github.com/centrifugal/centrifuge-java\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Java</a>, <a href=\"https://github.com/centrifugal/centrifuge-python\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Python</a>.</p>\n<p>See the <a class=\"\" href=\"https://centrifugal.dev/docs/transports/client_api#subscription-getstate\">Subscription getState</a> API reference for the option details, and the <a href=\"https://github.com/centrifugal/examples/tree/master/v6/pg_stream_broker\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">pg_stream_broker example</a> for a runnable app built on this pattern.</p>",
            "url": "https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions",
            "title": "App-owned state with stream subscriptions",
            "summary": "Describing the getState callback on stream subscriptions — capture the stream position first, load the data, and let the SDK handle every reconnect after that. With two worked examples — a Kafka aggregator and per-tenant kitchen orders.",
            "date_modified": "2026-07-27T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "websocket",
                "docsync"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub",
            "content_html": "<p><a href=\"https://redis.io/docs/latest/develop/pubsub/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Redis Pub/Sub</a> is a popular choice for passing messages between nodes in real-time messaging systems. It lets a system run many nodes — each holding many real-time client connections — and deliver each message to the nodes that have interested subscribers.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/blog_redis_scaling_real_time_system-745341dc1da429728581d3aaf0093587.jpg\" width=\"1774\" height=\"887\" class=\"img_ev3q\"></p>\n<p>At small scale things are quite simple. But at millions of channels and hundreds of subscriber nodes — the scale some Centrifugo users run at — Pub/Sub stops being a simple part of the design: a single Redis instance is limited by one CPU core, and switching to Redis Cluster can make throughput worse. This post walks through those gotchas, and the techniques that get from a single instance's ceiling to millions of messages per second — across isolated Redis shards and specifically in a Redis Cluster.</p>\n<p><a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#summing-up\" class=\"\">Jump to the end for TLDR</a></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Applies to Valkey too</div><div class=\"admonitionContent_BuS1\"><p>This post talks about <strong>Redis</strong>, but everything here applies equally to <strong><a href=\"https://valkey.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Valkey</a></strong> too.</p></div></div>\n<p>For a general-purpose real-time messaging server, every deployment looks different, so the design has to handle many cases. Two facts about real-time messaging specifics shaped the decisions in this post:</p>\n<ul>\n<li class=\"\"><strong>The system can have a lot of active channels</strong> — millions of them. There might be one per user, per document, or per game session, each created and thrown away all the time, so the server is constantly subscribing and unsubscribing. Usually each channel carries fairly light traffic (up to 60-100 messages per second); the load is spread across many channels rather than concentrated in a few.</li>\n<li class=\"\"><strong>There can be a lot of subscriber nodes too.</strong> Any node might care about any channel at any moment, so each one subscribes to whatever its clients need and has to receive everything published there. Once a deployment grows to hundreds of nodes, anything that costs something per node adds up very fast. In Centrifugo's case, one node usually serves up to 100k-200k connections — so setups which aim to have millions of real-time connections end up with hundreds of connection nodes, each subscribed to the channels its users care about.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"keeping-up-with-redis\">Keeping up with Redis<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#keeping-up-with-redis\" class=\"hash-link\" aria-label=\"Direct link to Keeping up with Redis\" title=\"Direct link to Keeping up with Redis\" translate=\"no\">​</a></h2>\n<p>Redis Pub/Sub is intentionally simple: a client subscribes to a named <em>channel</em>, and a publish to that channel reaches whoever is subscribed at that exact moment. Nothing is stored and nothing is retried, so a subscriber that wasn't connected when a message went out never sees it — delivery is at most once.</p>\n<svg viewBox=\"0 0 720 232\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b;margin:1.2rem 0\"><defs><marker id=\"fan-blue\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5b8def\"></polygon></marker><marker id=\"fan-green\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5bef7b\"></polygon></marker></defs><text x=\"20\" y=\"26\" font-size=\"13\" font-weight=\"bold\" fill=\"#ccc\" font-family=\"system-ui, sans-serif\">Baseline: publishers fan out to subscribers through Redis</text><rect x=\"24\" y=\"66\" width=\"92\" height=\"34\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.4\"></rect><text x=\"70\" y=\"87\" font-size=\"11\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">publisher</text><rect x=\"24\" y=\"128\" width=\"92\" height=\"34\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.4\"></rect><text x=\"70\" y=\"149\" font-size=\"11\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">publisher</text><rect x=\"296\" y=\"80\" width=\"128\" height=\"68\" rx=\"9\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.8\"></rect><text x=\"360\" y=\"110\" font-size=\"13\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\" font-weight=\"bold\">Redis</text><text x=\"360\" y=\"130\" font-size=\"10.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">PUB/SUB</text><line x1=\"116\" y1=\"83\" x2=\"294\" y2=\"104\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#fan-blue)\"></line><line x1=\"116\" y1=\"145\" x2=\"294\" y2=\"124\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#fan-blue)\"></line><text x=\"205\" y=\"84\" font-size=\"9.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">PUBLISH</text><g><rect x=\"596\" y=\"54\" width=\"100\" height=\"34\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.4\"></rect><text x=\"646\" y=\"75\" font-size=\"11\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->1</text><line x1=\"424\" y1=\"114\" x2=\"594\" y2=\"71\" stroke=\"#5bef7b\" stroke-width=\"1.4\" marker-end=\"url(#fan-green)\" opacity=\"0.9\"></line></g><g><rect x=\"596\" y=\"110\" width=\"100\" height=\"34\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.4\"></rect><text x=\"646\" y=\"131\" font-size=\"11\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->2</text><line x1=\"424\" y1=\"114\" x2=\"594\" y2=\"127\" stroke=\"#5bef7b\" stroke-width=\"1.4\" marker-end=\"url(#fan-green)\" opacity=\"0.9\"></line></g><g><rect x=\"596\" y=\"166\" width=\"100\" height=\"34\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.4\"></rect><text x=\"646\" y=\"187\" font-size=\"11\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->3</text><line x1=\"424\" y1=\"114\" x2=\"594\" y2=\"183\" stroke=\"#5bef7b\" stroke-width=\"1.4\" marker-end=\"url(#fan-green)\" opacity=\"0.9\"></line></g><text x=\"500\" y=\"78\" font-size=\"9.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">fan-out</text></svg>\n<p>Redis runs every command (in this case PUBLISH and SUBSCRIBE) on a single thread sequentially. Modern Redis and Valkey can spread network I/O across extra threads, which <a href=\"https://valkey.io/blog/unlock-one-million-rps/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">lifts throughput</a>. Also, an application may issue subscriptions to Redis replicas to spread the load a bit. However, command execution stays on the Redis master and remains serialized, so one instance still has a performance ceiling you can hit.</p>\n<p>Whatever Redis setup you run, your app has to keep up with Redis first — otherwise it's the first bottleneck, before Redis itself is even reached. With Redis Pub/Sub, each application node — also called a <em>subscriber node</em> here — does two things: it publishes messages and subscribes to receive them.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"efficient-publishing\">Efficient publishing<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#efficient-publishing\" class=\"hash-link\" aria-label=\"Direct link to Efficient publishing\" title=\"Direct link to Efficient publishing\" translate=\"no\">​</a></h3>\n<p>Centrifugo publishes over a <strong>pipelined</strong> connection instead of the connection-pool approach, where each command takes its own network round trip. The <a href=\"https://github.com/redis/rueidis\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">rueidis</a> client gathers commands issued close together and writes them as a single batch, with a small flush delay (<a class=\"\" href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance\"><code>MaxFlushDelay</code></a>, about 100µs) marking a batch boundary. One round trip then carries many publishes, so the publisher feeds Redis instead of stalling on the wire.</p>\n<p>Pipelining lifts the overall throughput and surprisingly cuts CPU usage on both the client and Redis sides due to reduced READ/WRITE syscalls — these gains were shown in detail before in <a class=\"\" href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance\">Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library</a>.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"efficient-subscribing\">Efficient subscribing<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#efficient-subscribing\" class=\"hash-link\" aria-label=\"Direct link to Efficient subscribing\" title=\"Direct link to Efficient subscribing\" translate=\"no\">​</a></h3>\n<p>The <code>SUBSCRIBE</code> command supports subscribing to many channels at once. When many clients reconnect at once or application nodes resubscribe after a network glitch, a few large commands with batched channels are far more efficient across the client, protocol, and Redis layers.</p>\n<p>Redis <a href=\"https://redis.io/docs/latest/develop/reference/clients/#output-buffer-limits\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">keeps a buffer</a> for each Pub/Sub connection, and if the reader doesn't drain it fast enough, Redis drops the connection. So the application has to drain the socket as fast as it can and <strong>delegate processing to a dedicated pool of workers</strong>. Because Pub/Sub delivery is at most once, the workers have some freedom in how they handle overload — including dropping messages at the application level.</p>\n<p>It's worth tracking an application metric for the end-to-end lag of a message sent through Pub/Sub — record the publication timestamp on the publisher side and subtract it from the time the message is received on the subscriber side. In most cases, workers falling behind is a signal that it's time to scale the application nodes.</p>\n<p>To keep a channel's messages in order, pick the worker by a hash of the channel name: every message for a channel goes to the same worker, while different channels spread across all the available cores. Also, using a few pipelined connections instead of one can further raise read throughput.</p>\n<svg viewBox=\"0 0 760 332\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b;margin:1.2rem 0\"><defs><marker id=\"rl-green\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5bef7b\"></polygon></marker><marker id=\"rl-redis\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#f5c451\"></polygon></marker></defs><text x=\"24\" y=\"28\" font-size=\"13\" font-weight=\"bold\" fill=\"#5bef7b\" font-family=\"system-ui, sans-serif\">Keep up with Redis: read fast, process in parallel</text><rect x=\"300\" y=\"44\" width=\"160\" height=\"34\" rx=\"7\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.6\"></rect><text x=\"380\" y=\"65\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Redis · Pub/Sub</text><line x1=\"380\" y1=\"78\" x2=\"380\" y2=\"106\" stroke=\"#f5c451\" stroke-width=\"2\" marker-end=\"url(#rl-redis)\"></line><text x=\"393\" y=\"90\" font-size=\"10\" fill=\"#f5c451\" font-family=\"ui-monospace, monospace\">1 connection</text><text x=\"393\" y=\"102\" font-size=\"8.5\" fill=\"#8a8a94\" font-family=\"system-ui, sans-serif\">usually enough</text><rect x=\"268\" y=\"108\" width=\"224\" height=\"50\" rx=\"8\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.8\"></rect><text x=\"380\" y=\"129\" font-size=\"12\" font-weight=\"bold\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">read loop</text><text x=\"380\" y=\"146\" font-size=\"9.5\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">drains the socket — nothing else</text><line x1=\"492\" y1=\"133\" x2=\"508\" y2=\"133\" stroke=\"#fe5e5e\" stroke-width=\"1.4\"></line><text x=\"512\" y=\"128\" font-size=\"10\" fill=\"#fe5e5e\" font-family=\"system-ui, sans-serif\" font-weight=\"600\">must keep up</text><text x=\"512\" y=\"141\" font-size=\"8.8\" fill=\"#8a8a94\" font-family=\"system-ui, sans-serif\">a slow reader gets its</text><text x=\"512\" y=\"152\" font-size=\"8.8\" fill=\"#8a8a94\" font-family=\"system-ui, sans-serif\">connection dropped by Redis</text><line x1=\"380\" y1=\"160\" x2=\"380\" y2=\"186\" stroke=\"#5bef7b\" stroke-width=\"2\" marker-end=\"url(#rl-green)\"></line><rect x=\"300\" y=\"172\" width=\"160\" height=\"15\" rx=\"7.5\" fill=\"#17171b\"></rect><text x=\"380\" y=\"183\" font-size=\"9.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">hash(channel)</text><text x=\"116\" y=\"206\" font-size=\"9.5\" fill=\"#8a8a94\" font-family=\"system-ui, sans-serif\">worker pool · one goroutine per core</text><rect x=\"116\" y=\"212\" width=\"528\" height=\"80\" rx=\"9\" fill=\"none\" stroke=\"#5bef7b\" stroke-width=\"1.1\" stroke-dasharray=\"5,4\"></rect><g><line x1=\"380\" y1=\"190\" x2=\"174\" y2=\"228\" stroke=\"#5bef7b\" stroke-width=\"1.2\" opacity=\"0.75\" marker-end=\"url(#rl-green)\"></line><rect x=\"132\" y=\"230\" width=\"84\" height=\"38\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"174\" y=\"246\" font-size=\"9.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">worker</text><text x=\"174\" y=\"259\" font-size=\"8.2\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">decode + deliver</text></g><g><line x1=\"380\" y1=\"190\" x2=\"277\" y2=\"228\" stroke=\"#5bef7b\" stroke-width=\"1.2\" opacity=\"0.75\" marker-end=\"url(#rl-green)\"></line><rect x=\"235\" y=\"230\" width=\"84\" height=\"38\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"277\" y=\"246\" font-size=\"9.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">worker</text><text x=\"277\" y=\"259\" font-size=\"8.2\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">decode + deliver</text></g><g><line x1=\"380\" y1=\"190\" x2=\"380\" y2=\"228\" stroke=\"#5bef7b\" stroke-width=\"1.2\" opacity=\"0.75\" marker-end=\"url(#rl-green)\"></line><rect x=\"338\" y=\"230\" width=\"84\" height=\"38\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"380\" y=\"246\" font-size=\"9.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">worker</text><text x=\"380\" y=\"259\" font-size=\"8.2\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">decode + deliver</text></g><g><line x1=\"380\" y1=\"190\" x2=\"483\" y2=\"228\" stroke=\"#5bef7b\" stroke-width=\"1.2\" opacity=\"0.75\" marker-end=\"url(#rl-green)\"></line><rect x=\"441\" y=\"230\" width=\"84\" height=\"38\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"483\" y=\"246\" font-size=\"9.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">worker</text><text x=\"483\" y=\"259\" font-size=\"8.2\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">decode + deliver</text></g><g><line x1=\"380\" y1=\"190\" x2=\"586\" y2=\"228\" stroke=\"#5bef7b\" stroke-width=\"1.2\" opacity=\"0.75\" marker-end=\"url(#rl-green)\"></line><rect x=\"544\" y=\"230\" width=\"84\" height=\"38\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"586\" y=\"246\" font-size=\"9.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">worker</text><text x=\"586\" y=\"259\" font-size=\"8.2\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">decode + deliver</text></g><text x=\"380\" y=\"284\" font-size=\"9.5\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">runs in parallel across cores · same channel → same worker, so per-channel order is kept</text><text x=\"380\" y=\"306\" font-size=\"10.5\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">The read path must never block. If workers fall behind, it's time to scale the nodes.</text><text x=\"380\" y=\"322\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">One connection usually keeps up — add a few pipelined connections only if a single reader can't.</text></svg>\n<p>Adding more application nodes helps absorb higher throughput, but keeping each node fast still matters. Every node opens its own connections to Redis, and at scale the total connection count becomes a concern of its own — more on that later.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"single-redis-pubsub-limit\">Single Redis Pub/Sub limit<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#single-redis-pubsub-limit\" class=\"hash-link\" aria-label=\"Direct link to Single Redis Pub/Sub limit\" title=\"Direct link to Single Redis Pub/Sub limit\" translate=\"no\">​</a></h2>\n<p>A single Redis instance is very fast, and I/O threading lets it use several cores for network work — but it still runs every command on one thread, and once that thread saturates there's nothing Redis can do.</p>\n<p>In a Centrifugo benchmark on Hetzner, on a machine with dedicated vCPU, a single Redis instance handled up to about <strong>650,000 messages per second</strong> (not just published, but all delivered to active subscribers, each message 256 bytes). After that, end-to-end latency started to grow fast — a signal of saturation. This throughput should be enough for most use cases. But to go further you have to stop using a single Redis instance.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"client-side-pubsub-sharding\">Client-side Pub/Sub sharding<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#client-side-pubsub-sharding\" class=\"hash-link\" aria-label=\"Direct link to Client-side Pub/Sub sharding\" title=\"Direct link to Client-side Pub/Sub sharding\" translate=\"no\">​</a></h2>\n<p>The simplest solution is to spread the Pub/Sub load across several independent Redis instances. The application decides which Redis instance handles a given channel by hashing the channel's name, for example with a <a href=\"https://arxiv.org/abs/1406.2294\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Jump consistent hash</a> or any other. Consistent hashing here may help to keep most of the data on the same Redis instances after adding a Redis node and re-sharding. In Centrifugo's case, we keep channel history in Redis in addition to Pub/Sub — the protocol is designed to tolerate the loss of it and provide a signal to real-time clients, so the loss during re-sharding is tolerable.</p>\n<p>In such a setup, a publisher and a subscriber should use the same hash function over the channel name, so they always land on the same instance without having to talk to each other, and the instances don't even know about one another — so each instance you add takes a share of the load.</p>\n<svg viewBox=\"0 0 720 300\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b;margin:1.2rem 0\"><defs><marker id=\"sh-blue\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5b8def\"></polygon></marker><marker id=\"sh-amber\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#f5c451\"></polygon></marker></defs><text x=\"20\" y=\"26\" font-size=\"13\" font-weight=\"bold\" fill=\"#ccc\" font-family=\"system-ui, sans-serif\">One Redis is single-threaded — shard by channel</text><g><rect x=\"24\" y=\"60\" width=\"124\" height=\"32\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"86\" y=\"81\" font-size=\"11\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">chat:42</text><line x1=\"148\" y1=\"76\" x2=\"266\" y2=\"140\" stroke=\"#5b8def\" stroke-width=\"1.1\" marker-end=\"url(#sh-blue)\" opacity=\"0.55\"></line></g><g><rect x=\"24\" y=\"104\" width=\"124\" height=\"32\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"86\" y=\"125\" font-size=\"11\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">doc:91</text><line x1=\"148\" y1=\"120\" x2=\"266\" y2=\"140\" stroke=\"#5b8def\" stroke-width=\"1.1\" marker-end=\"url(#sh-blue)\" opacity=\"0.55\"></line></g><g><rect x=\"24\" y=\"148\" width=\"124\" height=\"32\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"86\" y=\"169\" font-size=\"11\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">game:7</text><line x1=\"148\" y1=\"164\" x2=\"266\" y2=\"140\" stroke=\"#5b8def\" stroke-width=\"1.1\" marker-end=\"url(#sh-blue)\" opacity=\"0.55\"></line></g><g><rect x=\"24\" y=\"192\" width=\"124\" height=\"32\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"86\" y=\"213\" font-size=\"11\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">feed:3</text><line x1=\"148\" y1=\"208\" x2=\"266\" y2=\"140\" stroke=\"#5b8def\" stroke-width=\"1.1\" marker-end=\"url(#sh-blue)\" opacity=\"0.55\"></line></g><rect x=\"268\" y=\"108\" width=\"150\" height=\"64\" rx=\"32\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.8\"></rect><text x=\"343\" y=\"135\" font-size=\"11.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\" font-weight=\"bold\">Jump hash</text><text x=\"343\" y=\"153\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">(channel) → shard</text><g><rect x=\"560\" y=\"60\" width=\"132\" height=\"40\" rx=\"8\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.6\"></rect><text x=\"626\" y=\"85\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Redis <!-- -->A</text><line x1=\"418\" y1=\"140\" x2=\"558\" y2=\"80\" stroke=\"#f5c451\" stroke-width=\"1.3\" marker-end=\"url(#sh-amber)\" opacity=\"0.8\"></line></g><g><rect x=\"560\" y=\"124\" width=\"132\" height=\"40\" rx=\"8\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.6\"></rect><text x=\"626\" y=\"149\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Redis <!-- -->B</text><line x1=\"418\" y1=\"140\" x2=\"558\" y2=\"144\" stroke=\"#f5c451\" stroke-width=\"1.3\" marker-end=\"url(#sh-amber)\" opacity=\"0.8\"></line></g><g><rect x=\"560\" y=\"188\" width=\"132\" height=\"40\" rx=\"8\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.6\"></rect><text x=\"626\" y=\"213\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Redis <!-- -->C</text><line x1=\"418\" y1=\"140\" x2=\"558\" y2=\"208\" stroke=\"#f5c451\" stroke-width=\"1.3\" marker-end=\"url(#sh-amber)\" opacity=\"0.8\"></line></g><text x=\"360\" y=\"268\" font-size=\"10.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Independent instances, no replication between them — aggregate throughput grows with the shard count.</text><text x=\"360\" y=\"286\" font-size=\"10.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">A channel's publishers and subscribers always hash to the same shard.</text></svg>\n<p>In our benchmarks Pub/Sub throughput increased linearly as more Redis instances were added — each one took an even share of the load, and latency stayed low. So 650k messages per second became around 5M messages per second with 8 Redis instances. The instances share nothing, so the total capacity is just one instance's limit times the number of instances. Connection count naturally grows with the number of shards.</p>\n<div class=\"rv-card\"><div class=\"rv-head\"><div><div class=\"rv-title\">Throughput scales linearly as you add Redis instances</div><div class=\"rv-subtitle\">One instance has a fixed ceiling. Independent instances share nothing, so aggregate throughput is just that ceiling times the count.</div></div></div><div class=\"rv-controls\"><div class=\"rv-control\" style=\"--rv-accent:#5bef7b\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Independent Redis instances</span><span class=\"rv-control-value\" style=\"color:#5bef7b\">8</span></div><input class=\"rv-range\" type=\"range\" min=\"1\" max=\"16\" step=\"1\" style=\"--rv-accent:#5bef7b\" value=\"8\"></div><div class=\"rv-control\" style=\"--rv-accent:#62b0ff\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Subscriber nodes</span><span class=\"rv-control-value\" style=\"color:#62b0ff\">24</span></div><input class=\"rv-range\" type=\"range\" min=\"8\" max=\"200\" step=\"8\" style=\"--rv-accent:#62b0ff\" value=\"24\"></div><div class=\"rv-control\" style=\"--rv-accent:#f5c451\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Pipeline conns / instance</span><span class=\"rv-control-value\" style=\"color:#f5c451\">1</span></div><input class=\"rv-range\" type=\"range\" min=\"1\" max=\"4\" step=\"1\" style=\"--rv-accent:#f5c451\" value=\"1\"></div></div><div class=\"rv-control-label\" style=\"line-height:1.5;margin:-2px 0 10px\">8<!-- --> <!-- -->instances<!-- --> × <!-- -->650k<!-- --> msg/s each = <b style=\"color:#5bef7b\">5.2M<!-- --> msg/s</b><span style=\"color:#8a8a94\"> · connections <!-- -->24<!-- --> × <!-- -->8<!-- --> = </span><b style=\"color:#f5c451\">192</b></div><svg viewBox=\"0 0 720 132\" class=\"rv-svg\" xmlns=\"http://www.w3.org/2000/svg\" style=\"margin:2px 0 8px\"><rect x=\"34\" y=\"46\" width=\"652\" height=\"48\" rx=\"8\" fill=\"#23242b\" stroke=\"#2c2c34\" stroke-width=\"1\"></rect><rect x=\"34\" y=\"46\" height=\"48\" rx=\"8\" fill=\"#5bef7b\" style=\"width:326px;transition:width 0.42s cubic-bezier(0.22,0.61,0.36,1)\"></rect><line x1=\"74.75\" y1=\"46\" x2=\"74.75\" y2=\"94\" stroke=\"#17171b\" stroke-width=\"1.5\" opacity=\"0.6\"></line><line x1=\"115.5\" y1=\"46\" x2=\"115.5\" y2=\"94\" stroke=\"#17171b\" stroke-width=\"1.5\" opacity=\"0.6\"></line><line x1=\"156.25\" y1=\"46\" x2=\"156.25\" y2=\"94\" stroke=\"#17171b\" stroke-width=\"1.5\" opacity=\"0.6\"></line><line x1=\"197\" y1=\"46\" x2=\"197\" y2=\"94\" stroke=\"#17171b\" stroke-width=\"1.5\" opacity=\"0.6\"></line><line x1=\"237.75\" y1=\"46\" x2=\"237.75\" y2=\"94\" stroke=\"#17171b\" stroke-width=\"1.5\" opacity=\"0.6\"></line><line x1=\"278.5\" y1=\"46\" x2=\"278.5\" y2=\"94\" stroke=\"#17171b\" stroke-width=\"1.5\" opacity=\"0.6\"></line><line x1=\"319.25\" y1=\"46\" x2=\"319.25\" y2=\"94\" stroke=\"#17171b\" stroke-width=\"1.5\" opacity=\"0.6\"></line><line x1=\"360\" y1=\"46\" x2=\"360\" y2=\"94\" stroke=\"#34343d\" stroke-width=\"1\" opacity=\"0.5\"></line><line x1=\"400.75\" y1=\"46\" x2=\"400.75\" y2=\"94\" stroke=\"#34343d\" stroke-width=\"1\" opacity=\"0.5\"></line><line x1=\"441.5\" y1=\"46\" x2=\"441.5\" y2=\"94\" stroke=\"#34343d\" stroke-width=\"1\" opacity=\"0.5\"></line><line x1=\"482.25\" y1=\"46\" x2=\"482.25\" y2=\"94\" stroke=\"#34343d\" stroke-width=\"1\" opacity=\"0.5\"></line><line x1=\"523\" y1=\"46\" x2=\"523\" y2=\"94\" stroke=\"#34343d\" stroke-width=\"1\" opacity=\"0.5\"></line><line x1=\"563.75\" y1=\"46\" x2=\"563.75\" y2=\"94\" stroke=\"#34343d\" stroke-width=\"1\" opacity=\"0.5\"></line><line x1=\"604.5\" y1=\"46\" x2=\"604.5\" y2=\"94\" stroke=\"#34343d\" stroke-width=\"1\" opacity=\"0.5\"></line><line x1=\"645.25\" y1=\"46\" x2=\"645.25\" y2=\"94\" stroke=\"#34343d\" stroke-width=\"1\" opacity=\"0.5\"></line><line x1=\"74.75\" y1=\"34\" x2=\"74.75\" y2=\"98\" stroke=\"#f5c451\" stroke-width=\"1.5\" stroke-dasharray=\"3,3\" opacity=\"0.95\"></line><circle cx=\"74.75\" cy=\"34\" r=\"2.5\" fill=\"#f5c451\"></circle><text x=\"81.75\" y=\"38\" font-size=\"11\" fill=\"#f5c451\" font-family=\"ui-monospace, monospace\" font-weight=\"700\">single-instance ceiling (<!-- -->650k<!-- -->)</text><text x=\"350\" y=\"74.5\" font-size=\"13\" font-weight=\"700\" font-family=\"ui-monospace, monospace\" text-anchor=\"end\" fill=\"#11140f\">5.2M<!-- --> msg/s</text><g><line x1=\"34\" y1=\"94\" x2=\"34\" y2=\"99\" stroke=\"#2c2c34\" stroke-width=\"1\"></line><text x=\"34\" y=\"112\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">0</text></g><g><line x1=\"197\" y1=\"94\" x2=\"197\" y2=\"99\" stroke=\"#2c2c34\" stroke-width=\"1\"></line><text x=\"197\" y=\"112\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">2.6M</text></g><g><line x1=\"360\" y1=\"94\" x2=\"360\" y2=\"99\" stroke=\"#2c2c34\" stroke-width=\"1\"></line><text x=\"360\" y=\"112\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">5.2M</text></g><g><line x1=\"523\" y1=\"94\" x2=\"523\" y2=\"99\" stroke=\"#2c2c34\" stroke-width=\"1\"></line><text x=\"523\" y=\"112\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">7.8M</text></g><g><line x1=\"686\" y1=\"94\" x2=\"686\" y2=\"99\" stroke=\"#2c2c34\" stroke-width=\"1\"></line><text x=\"686\" y=\"112\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">10.4M</text></g></svg><div class=\"rv-stats\"><div class=\"rv-stat\"><div class=\"rv-stat-label\">Aggregate throughput</div><div class=\"rv-stat-value\" style=\"color:#5bef7b\">5.2M msg/s</div><div class=\"rv-stat-sub\">8 × one-instance ceiling</div></div><div class=\"rv-stat\"><div class=\"rv-stat-label\">Total connections to Redis</div><div class=\"rv-stat-value\" style=\"color:#f5c451\">192</div><div class=\"rv-stat-sub\">subscriber nodes × instances × pipe</div></div><div class=\"rv-stat\"><div class=\"rv-stat-label\">Per Redis instance</div><div class=\"rv-stat-value\" style=\"color:#d6d6db\">24</div><div class=\"rv-stat-sub\">connections landing on each one</div></div></div><div class=\"rv-caption\">Every instance you add takes about <b>1/N</b> of the load and contributes its own ~<!-- -->650k<!-- --> msg/s, so throughput grows in a straight line. The flip side rides along: every subscriber node opens its own pipeline connections to every instance, so the total into Redis is <b style=\"color:#f5c451\">subscriber nodes × instances × pipeline conns</b>. That product is what starts to bite at scale — the connection widgets further down tackle it.</div></div>\n<p>The number of connections per subscriber node is multiplied by the number of Redis shards. For example, for 8 separate Redis instances and 24 subscriber nodes it's 24 x 8 x (N pipeline conns).</p>\n<p>Client-side sharding works, but requires manual configuration. The list of Redis instances lives in the server's own configuration, so adding capacity isn't just starting another Redis — every node has to reload the new list.</p>\n<p>The obvious next step is to switch to Redis Cluster, which manages the group of nodes for you. But it's not that simple.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"classic-redis-cluster-pubsub-does-not-scale\">Classic Redis Cluster Pub/Sub does not scale<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#classic-redis-cluster-pubsub-does-not-scale\" class=\"hash-link\" aria-label=\"Direct link to Classic Redis Cluster Pub/Sub does not scale\" title=\"Direct link to Classic Redis Cluster Pub/Sub does not scale\" translate=\"no\">​</a></h2>\n<p>A single Redis holds everything in one process, while Redis Cluster spreads the data across several nodes. It does this by splitting all possible keys into <strong>16,384 buckets</strong> called <em>hash slots</em>: every key belongs to exactly one of them, chosen by <code>CRC16(key) % 16384</code>, and each node owns a range of slots. To reach a key, the client hashes it and goes straight to the node that owns that slot.</p>\n<p>There's also a handy trick called a <strong>hash tag</strong>. If a key contains braces, Redis only hashes the part inside the <code>{...}</code>, which lets you force a group of keys into the same slot — and so onto the same node — whenever you want them to stay together.</p>\n<svg viewBox=\"0 0 720 256\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b;margin:1.2rem 0\"><defs><marker id=\"cs-blue\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5b8def\"></polygon></marker><marker id=\"cs-n1\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5bef7b\"></polygon></marker><marker id=\"cs-n3\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#f5c451\"></polygon></marker></defs><text x=\"20\" y=\"26\" font-size=\"13\" font-weight=\"bold\" fill=\"#ccc\" font-family=\"system-ui, sans-serif\">Redis Cluster: every key belongs to one of 16,384 slots</text><rect x=\"20\" y=\"54\" width=\"108\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"74\" y=\"74\" font-size=\"11\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">chat:42</text><rect x=\"20\" y=\"104\" width=\"108\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"74\" y=\"124\" font-size=\"11\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">doc:91</text><rect x=\"162\" y=\"64\" width=\"132\" height=\"60\" rx=\"14\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.6\"></rect><text x=\"228\" y=\"89\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">CRC16(key)</text><text x=\"228\" y=\"106\" font-size=\"11\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"monospace\">% 16384</text><line x1=\"128\" y1=\"69\" x2=\"160\" y2=\"84\" stroke=\"#5b8def\" stroke-width=\"1.2\" marker-end=\"url(#cs-blue)\"></line><line x1=\"128\" y1=\"119\" x2=\"160\" y2=\"104\" stroke=\"#5b8def\" stroke-width=\"1.2\" marker-end=\"url(#cs-blue)\"></line><g><rect x=\"40\" y=\"176\" width=\"202\" height=\"34\" rx=\"6\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.5\"></rect><text x=\"141\" y=\"197\" font-size=\"11\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\" font-weight=\"bold\">node 1</text><text x=\"141\" y=\"225\" font-size=\"9.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">slots 0–5460</text></g><g><rect x=\"259\" y=\"176\" width=\"202\" height=\"34\" rx=\"6\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.5\"></rect><text x=\"360\" y=\"197\" font-size=\"11\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\" font-weight=\"bold\">node 2</text><text x=\"360\" y=\"225\" font-size=\"9.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">slots 5461–10922</text></g><g><rect x=\"478\" y=\"176\" width=\"202\" height=\"34\" rx=\"6\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.5\"></rect><text x=\"579\" y=\"197\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\" font-weight=\"bold\">node 3</text><text x=\"579\" y=\"225\" font-size=\"9.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">slots 10923–16383</text></g><line x1=\"240\" y1=\"124\" x2=\"161\" y2=\"141\" stroke=\"#5bef7b\" stroke-width=\"1.3\" marker-end=\"url(#cs-n1)\" opacity=\"0.9\"></line><line x1=\"296\" y1=\"100\" x2=\"552\" y2=\"141\" stroke=\"#f5c451\" stroke-width=\"1.3\" marker-end=\"url(#cs-n3)\" opacity=\"0.9\"></line><text x=\"141\" y=\"153\" font-size=\"11\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">slot 3401</text><text x=\"579\" y=\"153\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">slot 12182</text><line x1=\"141\" y1=\"159\" x2=\"141\" y2=\"174\" stroke=\"#5bef7b\" stroke-width=\"1.3\" marker-end=\"url(#cs-n1)\"></line><line x1=\"579\" y1=\"159\" x2=\"579\" y2=\"174\" stroke=\"#f5c451\" stroke-width=\"1.3\" marker-end=\"url(#cs-n3)\"></line><text x=\"360\" y=\"248\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">A hash tag hashes only the braced part, so <tspan font-family=\"monospace\" fill=\"#ccc\">{p}.a</tspan> and <tspan font-family=\"monospace\" fill=\"#ccc\">{p}.b</tspan> always land on the same slot — and the same node.</text></svg>\n<p>Because keys are spread across nodes this way, ordinary operations scale cleanly. A <code>GET</code> or <code>SET</code> goes only to the node that owns the key, so adding nodes spreads both the data and the load, and throughput grows almost linearly — this is the main benefit of Redis Cluster. Pub/Sub is the exception.</p>\n<p>Ordinary Pub/Sub in Redis Cluster sends every published message to <em>every</em> node — even the nodes where nobody is subscribed to that channel. It has no real choice: a subscriber could be attached to any node, so the only way to be sure they get the message is to copy it everywhere. That sounds harmless until you see the cost — the more nodes you add, the worse it gets, because each one has to carry every message, including all the ones it has no subscriber for.</p>\n<svg viewBox=\"0 0 740 330\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b;margin:1.2rem 0\"><defs><marker id=\"cb-hot\" markerWidth=\"9\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#fe5e5e\"></polygon></marker><marker id=\"cb-blue\" markerWidth=\"9\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5b8def\"></polygon></marker><marker id=\"cb-green\" markerWidth=\"9\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5bef7b\"></polygon></marker></defs><text x=\"20\" y=\"26\" font-size=\"13\" font-weight=\"bold\" fill=\"#ccc\" font-family=\"system-ui, sans-serif\">Classic Cluster Pub/Sub: every node gets every message</text><path d=\"M 80 134 Q 149 94 218 134\" fill=\"none\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#cb-hot)\" opacity=\"0.8\"></path><path d=\"M 80 134 Q 218 68 356 134\" fill=\"none\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#cb-hot)\" opacity=\"0.8\"></path><path d=\"M 80 134 Q 287 42 494 134\" fill=\"none\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#cb-hot)\" opacity=\"0.8\"></path><path d=\"M 80 134 Q 356 16 632 134\" fill=\"none\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#cb-hot)\" opacity=\"0.8\"></path><text x=\"356\" y=\"62\" font-size=\"10\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">copied to every node</text><g><rect x=\"30\" y=\"136\" width=\"100\" height=\"46\" rx=\"8\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.7\"></rect><text x=\"80\" y=\"157\" font-size=\"11.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->1</text><circle cx=\"80\" cy=\"171\" r=\"4.5\" fill=\"#fe5e5e\"></circle></g><g><rect x=\"168\" y=\"136\" width=\"100\" height=\"46\" rx=\"8\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.7\"></rect><text x=\"218\" y=\"157\" font-size=\"11.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->2</text><circle cx=\"218\" cy=\"171\" r=\"4.5\" fill=\"#fe5e5e\"></circle><text x=\"218\" y=\"204\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">no subscriber</text></g><g><rect x=\"306\" y=\"136\" width=\"100\" height=\"46\" rx=\"8\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.7\"></rect><text x=\"356\" y=\"157\" font-size=\"11.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->3</text><circle cx=\"356\" cy=\"171\" r=\"4.5\" fill=\"#fe5e5e\"></circle><text x=\"356\" y=\"204\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">no subscriber</text></g><g><rect x=\"444\" y=\"136\" width=\"100\" height=\"46\" rx=\"8\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.7\"></rect><text x=\"494\" y=\"157\" font-size=\"11.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->4</text><circle cx=\"494\" cy=\"171\" r=\"4.5\" fill=\"#fe5e5e\"></circle><text x=\"494\" y=\"204\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">no subscriber</text></g><g><rect x=\"582\" y=\"136\" width=\"100\" height=\"46\" rx=\"8\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.7\"></rect><text x=\"632\" y=\"157\" font-size=\"11.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->5</text><circle cx=\"632\" cy=\"171\" r=\"4.5\" fill=\"#fe5e5e\"></circle></g><rect x=\"28\" y=\"240\" width=\"104\" height=\"38\" rx=\"8\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.4\"></rect><text x=\"80\" y=\"263\" font-size=\"11\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">publisher</text><line x1=\"80\" y1=\"240\" x2=\"80\" y2=\"183\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#cb-blue)\"></line><text x=\"126\" y=\"216\" font-size=\"10\" fill=\"#888\" font-family=\"monospace\">PUBLISH</text><line x1=\"632\" y1=\"183\" x2=\"632\" y2=\"238\" stroke=\"#5bef7b\" stroke-width=\"1.5\" marker-end=\"url(#cb-green)\"></line><rect x=\"580\" y=\"240\" width=\"104\" height=\"38\" rx=\"8\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.4\"></rect><text x=\"632\" y=\"263\" font-size=\"11\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">subscriber</text><text x=\"586\" y=\"216\" font-size=\"10\" fill=\"#888\" text-anchor=\"end\" font-family=\"monospace\">delivered</text><text x=\"370\" y=\"300\" font-size=\"10.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Only node 5 has a subscriber for this channel, yet every node still receives a copy.</text><text x=\"370\" y=\"318\" font-size=\"10.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">The nodes in between carry the message for nothing — and it gets worse with every node you add.</text></svg>\n<p>There's a second, less visible problem with this. With classic Pub/Sub you can't choose which node handles your subscriptions — they all go over a single connection to whichever node it happens to point at for the first subscribe operation. So one node can end up tracking every subscription while the rest sit nearly idle — a subscription imbalance.</p>\n<p>In our benchmarks, a classic Redis Cluster behaved just as the theory above says. Under load in a 3-node Redis Cluster, the delivery latency grew from milliseconds to whole seconds long before the 650k messages per second a single instance reached, somewhere around 400k messages per second. And it got even worse as nodes were added.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"sharded-pubsub\">Sharded Pub/Sub<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#sharded-pubsub\" class=\"hash-link\" aria-label=\"Direct link to Sharded Pub/Sub\" title=\"Direct link to Sharded Pub/Sub\" translate=\"no\">​</a></h2>\n<p>Redis 7 introduced <strong><a href=\"https://redis.io/docs/latest/develop/pubsub/#sharded-pubsub\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">sharded Pub/Sub</a></strong> (<code>SSUBSCRIBE</code> / <code>SPUBLISH</code> commands) to fix exactly this. Within these new commands a Pub/Sub channel respects hash slots and behaves like a normal key: a message goes only to the Redis node that owns the channel's slot, instead of being copied to every node — which solves the throughput problem. The price is that Pub/Sub now has to be coordinated by the Redis client driver: each subscribe and publish must go to the node that owns the slot, instead of one shared connection to any Redis Cluster node.</p>\n<svg viewBox=\"0 0 740 330\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b;margin:1.2rem 0\"><defs><marker id=\"csp-blue\" markerWidth=\"9\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5b8def\"></polygon></marker><marker id=\"csp-green\" markerWidth=\"9\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5bef7b\"></polygon></marker></defs><text x=\"20\" y=\"26\" font-size=\"13\" font-weight=\"bold\" fill=\"#ccc\" font-family=\"system-ui, sans-serif\">Sharded Pub/Sub: publish and subscribe load spread across the nodes</text><text x=\"149\" y=\"106\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">SPUBLISH</text><text x=\"149\" y=\"212\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">delivered</text><g><rect x=\"33\" y=\"46\" width=\"94\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"80\" y=\"65\" font-size=\"10\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">publisher</text><line x1=\"80\" y1=\"76\" x2=\"80\" y2=\"126\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#csp-blue)\"></line><rect x=\"30\" y=\"128\" width=\"100\" height=\"50\" rx=\"8\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.5\"></rect><text x=\"80\" y=\"149\" font-size=\"11.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->1</text><text x=\"80\" y=\"167\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">{a}</text><line x1=\"80\" y1=\"180\" x2=\"80\" y2=\"230\" stroke=\"#5bef7b\" stroke-width=\"1.5\" marker-end=\"url(#csp-green)\"></line><rect x=\"33\" y=\"232\" width=\"94\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"80\" y=\"251\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">subscriber</text></g><g><rect x=\"171\" y=\"46\" width=\"94\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"218\" y=\"65\" font-size=\"10\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">publisher</text><line x1=\"218\" y1=\"76\" x2=\"218\" y2=\"126\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#csp-blue)\"></line><rect x=\"168\" y=\"128\" width=\"100\" height=\"50\" rx=\"8\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.5\"></rect><text x=\"218\" y=\"149\" font-size=\"11.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->2</text><text x=\"218\" y=\"167\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">{b}</text><line x1=\"218\" y1=\"180\" x2=\"218\" y2=\"230\" stroke=\"#5bef7b\" stroke-width=\"1.5\" marker-end=\"url(#csp-green)\"></line><rect x=\"171\" y=\"232\" width=\"94\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"218\" y=\"251\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">subscriber</text></g><g><rect x=\"309\" y=\"46\" width=\"94\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"356\" y=\"65\" font-size=\"10\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">publisher</text><line x1=\"356\" y1=\"76\" x2=\"356\" y2=\"126\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#csp-blue)\"></line><rect x=\"306\" y=\"128\" width=\"100\" height=\"50\" rx=\"8\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.5\"></rect><text x=\"356\" y=\"149\" font-size=\"11.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->3</text><text x=\"356\" y=\"167\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">{c}</text><line x1=\"356\" y1=\"180\" x2=\"356\" y2=\"230\" stroke=\"#5bef7b\" stroke-width=\"1.5\" marker-end=\"url(#csp-green)\"></line><rect x=\"309\" y=\"232\" width=\"94\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"356\" y=\"251\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">subscriber</text></g><g><rect x=\"447\" y=\"46\" width=\"94\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"494\" y=\"65\" font-size=\"10\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">publisher</text><line x1=\"494\" y1=\"76\" x2=\"494\" y2=\"126\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#csp-blue)\"></line><rect x=\"444\" y=\"128\" width=\"100\" height=\"50\" rx=\"8\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.5\"></rect><text x=\"494\" y=\"149\" font-size=\"11.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->4</text><text x=\"494\" y=\"167\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">{d}</text><line x1=\"494\" y1=\"180\" x2=\"494\" y2=\"230\" stroke=\"#5bef7b\" stroke-width=\"1.5\" marker-end=\"url(#csp-green)\"></line><rect x=\"447\" y=\"232\" width=\"94\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"494\" y=\"251\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">subscriber</text></g><g><rect x=\"585\" y=\"46\" width=\"94\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.3\"></rect><text x=\"632\" y=\"65\" font-size=\"10\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">publisher</text><line x1=\"632\" y1=\"76\" x2=\"632\" y2=\"126\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#csp-blue)\"></line><rect x=\"582\" y=\"128\" width=\"100\" height=\"50\" rx=\"8\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.5\"></rect><text x=\"632\" y=\"149\" font-size=\"11.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->5</text><text x=\"632\" y=\"167\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">{e}</text><line x1=\"632\" y1=\"180\" x2=\"632\" y2=\"230\" stroke=\"#5bef7b\" stroke-width=\"1.5\" marker-end=\"url(#csp-green)\"></line><rect x=\"585\" y=\"232\" width=\"94\" height=\"30\" rx=\"7\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"632\" y=\"251\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">subscriber</text></g><text x=\"370\" y=\"292\" font-size=\"10.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Each channel lives on the one node that owns its slot, so its publisher and subscribers meet on that node — and nowhere else.</text><text x=\"370\" y=\"310\" font-size=\"10.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Different channels land on different nodes, so publish and subscribe load spreads across the whole cluster.</text></svg>\n<p>This visualization shows the benefit of sharded Pub/Sub and why classic Pub/Sub fails to increase Pub/Sub throughput:</p>\n<div class=\"rv-card\"><div class=\"rv-head\"><div><div class=\"rv-title\">Classic broadcast vs sharded delivery</div><div class=\"rv-subtitle\">A publisher sends one message to one node. Classic Pub/Sub makes that node copy it to every other node over the cluster bus; sharded Pub/Sub routes it straight to the single node that owns the channel's slot.</div></div></div><style>\n        .rv-amp-flow {\n          stroke-dasharray: 5 7;\n          animation: rv-amp-dash 0.95s linear infinite;\n        }\n        @keyframes rv-amp-dash { to { stroke-dashoffset: -12; } }\n        .rv-amp-pulse { animation: rv-amp-glow 1.8s ease-in-out infinite; transform-origin: center; }\n        @keyframes rv-amp-glow { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }\n        @media (prefers-reduced-motion: reduce) {\n          .rv-amp-flow { animation: none; stroke-dasharray: none; }\n          .rv-amp-pulse { animation: none; }\n        }\n      </style><div class=\"rv-controls\"><div class=\"rv-control\" style=\"--rv-accent:#f5c451\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Redis cluster nodes</span><span class=\"rv-control-value\" style=\"color:#f5c451\">6</span></div><input class=\"rv-range\" type=\"range\" min=\"3\" max=\"12\" step=\"1\" style=\"--rv-accent:#f5c451\" value=\"6\"></div><div class=\"rv-control\" style=\"--rv-accent:#62b0ff\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Publish rate</span><span class=\"rv-control-value\" style=\"color:#62b0ff\">200,000/s</span></div><input class=\"rv-range\" type=\"range\" min=\"50000\" max=\"650000\" step=\"10000\" style=\"--rv-accent:#62b0ff\" value=\"200000\"></div></div><svg viewBox=\"0 0 720 290\" class=\"rv-svg\" xmlns=\"http://www.w3.org/2000/svg\" style=\"margin:2px 0 6px\"><defs><marker id=\"amp-hot\" markerWidth=\"8\" markerHeight=\"6\" refX=\"6.5\" refY=\"3\" orient=\"auto\"><polygon points=\"0 0, 8 3, 0 6\" fill=\"#fe5e5e\"></polygon></marker><marker id=\"amp-green\" markerWidth=\"8\" markerHeight=\"6\" refX=\"6.5\" refY=\"3\" orient=\"auto\"><polygon points=\"0 0, 8 3, 0 6\" fill=\"#5bef7b\"></polygon></marker><marker id=\"amp-blue\" markerWidth=\"8\" markerHeight=\"6\" refX=\"6.5\" refY=\"3\" orient=\"auto\"><polygon points=\"0 0, 8 3, 0 6\" fill=\"#62b0ff\"></polygon></marker></defs><text x=\"180\" y=\"26\" font-size=\"12.5\" font-weight=\"700\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Classic Pub/Sub</text><text x=\"180\" y=\"40\" font-size=\"10\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">one node copies it to all the rest</text><text x=\"540\" y=\"26\" font-size=\"12.5\" font-weight=\"700\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Sharded Pub/Sub</text><text x=\"540\" y=\"40\" font-size=\"10\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">one copy to the owner</text><line x1=\"360\" y1=\"14\" x2=\"360\" y2=\"258\" stroke=\"#2c2c34\" stroke-width=\"1\" stroke-dasharray=\"3,5\"></line><g><line class=\"rv-amp-flow\" x1=\"180.00000000000003\" y1=\"161\" x2=\"75.33333333333334\" y2=\"161\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" opacity=\"0.5\" marker-end=\"url(#amp-hot)\"></line><line class=\"rv-amp-flow\" x1=\"180.00000000000003\" y1=\"161\" x2=\"284.6666666666667\" y2=\"161\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" opacity=\"0.5\" marker-end=\"url(#amp-hot)\"></line><line class=\"rv-amp-flow\" x1=\"180.00000000000003\" y1=\"161\" x2=\"75.33333333333334\" y2=\"207\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" opacity=\"0.5\" marker-end=\"url(#amp-hot)\"></line><line class=\"rv-amp-flow\" x1=\"180.00000000000003\" y1=\"161\" x2=\"180.00000000000003\" y2=\"207\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" opacity=\"0.5\" marker-end=\"url(#amp-hot)\"></line><line class=\"rv-amp-flow\" x1=\"180.00000000000003\" y1=\"161\" x2=\"284.6666666666667\" y2=\"207\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" opacity=\"0.5\" marker-end=\"url(#amp-hot)\"></line><line class=\"rv-amp-flow\" x1=\"180\" y1=\"74\" x2=\"180.00000000000003\" y2=\"146\" stroke=\"#62b0ff\" stroke-width=\"1.9\" marker-end=\"url(#amp-blue)\"></line><rect x=\"136\" y=\"48\" width=\"88\" height=\"26\" rx=\"7\" fill=\"#23242b\" stroke=\"#62b0ff\" stroke-width=\"1.4\"></rect><text x=\"180\" y=\"65\" font-size=\"11\" fill=\"#62b0ff\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">publisher</text><text x=\"231\" y=\"64\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"start\" font-family=\"ui-monospace, monospace\">PUBLISH</text><g opacity=\"1\"><rect x=\"28\" y=\"146\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.7\"></rect><text x=\"75.33333333333334\" y=\"164.5\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->1</text><circle class=\"rv-amp-pulse\" cx=\"114.66666666666667\" cy=\"154\" r=\"3\" fill=\"#fe5e5e\"></circle></g><g opacity=\"1\"><rect x=\"132.66666666666669\" y=\"146\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#62b0ff\" stroke-width=\"1.7\"></rect><text x=\"180.00000000000003\" y=\"164.5\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->2</text><circle class=\"rv-amp-pulse\" cx=\"219.33333333333337\" cy=\"154\" r=\"3\" fill=\"#fe5e5e\"></circle></g><g opacity=\"1\"><rect x=\"237.33333333333334\" y=\"146\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.7\"></rect><text x=\"284.6666666666667\" y=\"164.5\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->3</text><circle class=\"rv-amp-pulse\" cx=\"324\" cy=\"154\" r=\"3\" fill=\"#fe5e5e\"></circle></g><g opacity=\"1\"><rect x=\"28\" y=\"192\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.7\"></rect><text x=\"75.33333333333334\" y=\"210.5\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->4</text><circle class=\"rv-amp-pulse\" cx=\"114.66666666666667\" cy=\"200\" r=\"3\" fill=\"#fe5e5e\"></circle></g><g opacity=\"1\"><rect x=\"132.66666666666669\" y=\"192\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.7\"></rect><text x=\"180.00000000000003\" y=\"210.5\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->5</text><circle class=\"rv-amp-pulse\" cx=\"219.33333333333337\" cy=\"200\" r=\"3\" fill=\"#fe5e5e\"></circle></g><g opacity=\"1\"><rect x=\"237.33333333333334\" y=\"192\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.7\"></rect><text x=\"284.6666666666667\" y=\"210.5\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->6</text><circle class=\"rv-amp-pulse\" cx=\"324\" cy=\"200\" r=\"3\" fill=\"#fe5e5e\"></circle></g><text x=\"180\" y=\"272\" font-size=\"11\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\"><tspan fill=\"#fe5e5e\" font-weight=\"700\">6 copies</tspan><tspan fill=\"#8a8a94\"> — every node carries it</tspan></text></g><g><line class=\"rv-amp-flow\" x1=\"540\" y1=\"74\" x2=\"435.3333333333333\" y2=\"192\" stroke=\"#5bef7b\" stroke-width=\"1.9\" marker-end=\"url(#amp-green)\"></line><rect x=\"496\" y=\"48\" width=\"88\" height=\"26\" rx=\"7\" fill=\"#23242b\" stroke=\"#62b0ff\" stroke-width=\"1.4\"></rect><text x=\"540\" y=\"65\" font-size=\"11\" fill=\"#62b0ff\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">publisher</text><text x=\"591\" y=\"64\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"start\" font-family=\"ui-monospace, monospace\">SPUBLISH</text><g opacity=\"0.42\"><rect x=\"388\" y=\"146\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#2c2c34\" stroke-width=\"1.2\"></rect><text x=\"435.3333333333333\" y=\"164.5\" font-size=\"10\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->1</text></g><g opacity=\"0.42\"><rect x=\"492.6666666666667\" y=\"146\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#2c2c34\" stroke-width=\"1.2\"></rect><text x=\"540\" y=\"164.5\" font-size=\"10\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->2</text></g><g opacity=\"0.42\"><rect x=\"597.3333333333334\" y=\"146\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#2c2c34\" stroke-width=\"1.2\"></rect><text x=\"644.6666666666667\" y=\"164.5\" font-size=\"10\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->3</text></g><g opacity=\"1\"><rect x=\"388\" y=\"192\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"rgba(91,239,123,0.1)\" stroke=\"#62b0ff\" stroke-width=\"1.7\"></rect><text x=\"435.3333333333333\" y=\"210.5\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->4</text><circle class=\"rv-amp-pulse\" cx=\"474.6666666666667\" cy=\"200\" r=\"3\" fill=\"#5bef7b\"></circle></g><g opacity=\"0.42\"><rect x=\"492.6666666666667\" y=\"192\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#2c2c34\" stroke-width=\"1.2\"></rect><text x=\"540\" y=\"210.5\" font-size=\"10\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->5</text></g><g opacity=\"0.42\"><rect x=\"597.3333333333334\" y=\"192\" width=\"94.66666666666667\" height=\"30\" rx=\"6\" fill=\"#23242b\" stroke=\"#2c2c34\" stroke-width=\"1.2\"></rect><text x=\"644.6666666666667\" y=\"210.5\" font-size=\"10\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">n<!-- -->6</text></g><text x=\"540\" y=\"272\" font-size=\"11\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\"><tspan fill=\"#5bef7b\" font-weight=\"700\">1 copy</tspan><tspan fill=\"#8a8a94\"> — only the owner node</tspan></text></g></svg><div class=\"rv-stats\"><div class=\"rv-stat\"><div class=\"rv-stat-label\">Copies per publish — classic</div><div class=\"rv-stat-value\" style=\"color:#fe5e5e\">6</div><div class=\"rv-stat-sub\">1,200,000 msg/s of internal traffic</div></div><div class=\"rv-stat\"><div class=\"rv-stat-label\">Copies per publish — sharded</div><div class=\"rv-stat-value\" style=\"color:#5bef7b\">1</div><div class=\"rv-stat-sub\">200,000 msg/s of internal traffic</div></div><div class=\"rv-stat\"><div class=\"rv-stat-label\">Amplification</div><div class=\"rv-stat-value\" style=\"color:#fe5e5e\">6×</div><div class=\"rv-stat-sub\">1,000,000 msg/s carried for nothing</div></div></div><div class=\"rv-caption\">Classic Pub/Sub copies every message to every node, so adding nodes lowers throughput instead of raising it — in the benchmark a 3-node cluster bogged down to seconds of latency near <b style=\"color:#fe5e5e\">400k msg/s</b>, below a single instance's 650k, and worse with more nodes. Sharded Pub/Sub sends one copy to the owning node, so it scales like independent instances: about <b style=\"color:#5bef7b\">650k × N</b>.</div></div>\n<p>For some applications, moving to sharded Pub/Sub is easy — just call the new methods in the Redis client driver. For Centrifugo's workloads it was not that simple, here is why.</p>\n<p>With classic Pub/Sub, subscribing was easy: take a Pub/Sub connection and send <code>SUBSCRIBE</code> commands over it, without caring which Redis node it points to. With sharded Pub/Sub, the connection returned by the <code>rueidis</code> driver points to the node that owns the slot of the first channel subscribed to. <code>SSUBSCRIBE</code> also supports a batch of channels, but all must belong to the same hash slot.</p>\n<p>Subscribing to a second channel can't reuse the existing Pub/Sub connection — it needs another one. With only a handful of channels that's fine: open a few sharded Pub/Sub connections and you're done. But with millions of channels that approach no longer works — a million connections to Redis isn't something anyone would attempt. So there's a problem with connection count, and also it's not obvious how to keep the subscribe batching that helped to keep the system effective.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"app-level-partitions\">App-level partitions<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#app-level-partitions\" class=\"hash-link\" aria-label=\"Direct link to App-level partitions\" title=\"Direct link to App-level partitions\" translate=\"no\">​</a></h2>\n<p>The way around it is to stop treating channels individually and group them on application level. Each channel is hashed into a small, fixed number (<strong>N</strong>) of <strong>partitions</strong>, far smaller than the channel count. And the number of partitions must be larger than the Redis Cluster size for better distribution. By default, Centrifugo uses 128. Every channel in a partition gets the same hash tag, so they all land in the same slot, on the same Redis node. A channel key ends up shaped like <code>{&lt;partition&gt;}.&lt;channel&gt;</code>:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">SSUBSCRIBE news sports            # rejected if they hash to different slots</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">SSUBSCRIBE {42}.news {42}.sports  # shared {42} tag → one slot → one connection, batched</span><br></div></code></pre></div></div>\n<p>A single partition then uses a single connection: when the first channel in a partition is subscribed, the client opens one connection to that partition's node, and every other channel in the partition reuses it. Because those channels share a slot, their subscriptions batch and pipeline on that connection, the way classic Pub/Sub batched.</p>\n<p>And because the partitions are spread across the cluster, the connections spread with them instead of piling onto one node. So instead of a connection per channel there's <strong>one per partition</strong> — N per node — and subscribing is back to cheap, batched commands over a handful of steady connections per node, even as channels come and go. That settles the count on a single node; multiplied across a large fleet the cluster-wide total is another matter — more on that below.</p>\n<svg viewBox=\"0 0 720 322\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b;margin:1.2rem 0\"><defs><marker id=\"cp-arr\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#888\"></polygon></marker><marker id=\"cp-green\" markerWidth=\"9\" markerHeight=\"7\" refX=\"8\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 9 3.5, 0 7\" fill=\"#5bef7b\"></polygon></marker></defs><text x=\"20\" y=\"26\" font-size=\"13\" font-weight=\"bold\" fill=\"#ccc\" font-family=\"system-ui, sans-serif\">Group channels into a fixed number of partitions</text><g><rect x=\"20\" y=\"58\" width=\"120\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.2\"></rect><text x=\"80\" y=\"75\" font-size=\"10.5\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">user:1042</text><line x1=\"140\" y1=\"71\" x2=\"214\" y2=\"150\" stroke=\"#5b8def\" stroke-width=\"1\" opacity=\"0.5\" marker-end=\"url(#cp-arr)\"></line></g><g><rect x=\"20\" y=\"92\" width=\"120\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.2\"></rect><text x=\"80\" y=\"109\" font-size=\"10.5\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">doc:88</text><line x1=\"140\" y1=\"105\" x2=\"214\" y2=\"150\" stroke=\"#5b8def\" stroke-width=\"1\" opacity=\"0.5\" marker-end=\"url(#cp-arr)\"></line></g><g><rect x=\"20\" y=\"126\" width=\"120\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.2\"></rect><text x=\"80\" y=\"143\" font-size=\"10.5\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">chat:42</text><line x1=\"140\" y1=\"139\" x2=\"214\" y2=\"150\" stroke=\"#5b8def\" stroke-width=\"1\" opacity=\"0.5\" marker-end=\"url(#cp-arr)\"></line></g><g><rect x=\"20\" y=\"160\" width=\"120\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.2\"></rect><text x=\"80\" y=\"177\" font-size=\"10.5\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">game:7</text><line x1=\"140\" y1=\"173\" x2=\"214\" y2=\"150\" stroke=\"#5b8def\" stroke-width=\"1\" opacity=\"0.5\" marker-end=\"url(#cp-arr)\"></line></g><g><rect x=\"20\" y=\"194\" width=\"120\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#5b8def\" stroke-width=\"1.2\"></rect><text x=\"80\" y=\"211\" font-size=\"10.5\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">room:5</text><line x1=\"140\" y1=\"207\" x2=\"214\" y2=\"150\" stroke=\"#5b8def\" stroke-width=\"1\" opacity=\"0.5\" marker-end=\"url(#cp-arr)\"></line></g><text x=\"80\" y=\"246\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">…and millions more</text><circle cx=\"250\" cy=\"150\" r=\"34\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.6\"></circle><text x=\"250\" y=\"148\" font-size=\"11\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\" font-weight=\"bold\">hash</text><text x=\"250\" y=\"163\" font-size=\"8.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">→ partition</text><g><line x1=\"284\" y1=\"150\" x2=\"404\" y2=\"102\" stroke=\"#f5c451\" stroke-width=\"1.1\" opacity=\"0.6\" marker-end=\"url(#cp-arr)\"></line><rect x=\"408\" y=\"84\" width=\"152\" height=\"36\" rx=\"7\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.3\"></rect><text x=\"484\" y=\"99\" font-size=\"10.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">partition <!-- -->0</text><text x=\"484\" y=\"113\" font-size=\"8.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">1 connection</text><line x1=\"560\" y1=\"102\" x2=\"616\" y2=\"102\" stroke=\"#5bef7b\" stroke-width=\"1.4\" marker-end=\"url(#cp-green)\"></line></g><g><line x1=\"284\" y1=\"150\" x2=\"404\" y2=\"156\" stroke=\"#f5c451\" stroke-width=\"1.1\" opacity=\"0.6\" marker-end=\"url(#cp-arr)\"></line><rect x=\"408\" y=\"138\" width=\"152\" height=\"36\" rx=\"7\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.3\"></rect><text x=\"484\" y=\"153\" font-size=\"10.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">partition <!-- -->1</text><text x=\"484\" y=\"167\" font-size=\"8.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">1 connection</text><line x1=\"560\" y1=\"156\" x2=\"616\" y2=\"156\" stroke=\"#5bef7b\" stroke-width=\"1.4\" marker-end=\"url(#cp-green)\"></line></g><g><line x1=\"284\" y1=\"150\" x2=\"404\" y2=\"210\" stroke=\"#f5c451\" stroke-width=\"1.1\" opacity=\"0.6\" marker-end=\"url(#cp-arr)\"></line><rect x=\"408\" y=\"192\" width=\"152\" height=\"36\" rx=\"7\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.3\"></rect><text x=\"484\" y=\"207\" font-size=\"10.5\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">partition <!-- -->2</text><text x=\"484\" y=\"221\" font-size=\"8.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"monospace\">1 connection</text><line x1=\"560\" y1=\"210\" x2=\"616\" y2=\"210\" stroke=\"#5bef7b\" stroke-width=\"1.4\" marker-end=\"url(#cp-green)\"></line></g><text x=\"484\" y=\"262\" font-size=\"10\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">…N partitions in total (N is small)</text><rect x=\"620\" y=\"84\" width=\"82\" height=\"144\" rx=\"8\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.5\"></rect><text x=\"661\" y=\"150\" font-size=\"11\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Redis</text><text x=\"661\" y=\"166\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node(s)</text><text x=\"360\" y=\"296\" font-size=\"10.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Each channel hashes to one of N partitions — a small, fixed number — and each partition uses one connection.</text><text x=\"360\" y=\"313\" font-size=\"10.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">So the connection count is N, far smaller than the number of channels (which can be millions).</text></svg>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"fixing-uneven-slot-distribution\">Fixing uneven slot distribution<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#fixing-uneven-slot-distribution\" class=\"hash-link\" aria-label=\"Direct link to Fixing uneven slot distribution\" title=\"Direct link to Fixing uneven slot distribution\" translate=\"no\">​</a></h2>\n<p>The partition scheme skips over one important detail: what to use as each partition's tag. The obvious answer is the partition's own index — <code>{0}</code>, <code>{1}</code>, <code>{2}</code>, … <code>{N}</code> — the natural starting point. It works, but it has a hidden problem. Run those short strings through CRC16 and the slots they hash to aren't evenly spread, so some Redis nodes end up with many more partitions than others.</p>\n<p>Take, for example, a 6-node cluster with N = 128 partitions: with naive <code>{0}</code>…<code>{127}</code> tags the partitions land <code>[27, 15, 22, 23, 16, 25]</code> across the six nodes — the busiest node gets nearly twice as many as the lightest, and the gap only grows on bigger clusters. That skew has a real cost: Pub/Sub throughput is capped by the busiest node, so it saturates first while the lightest still has headroom — you pay for all six nodes but leave capacity unused, and that's the opposite of what you add nodes for.</p>\n<p>So the tags need to be chosen more carefully. For a given number of partitions, we've implemented a small <a href=\"https://github.com/centrifugal/centrifuge/tree/master/internal/redispartition\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">simulated-annealing search script</a> — it finds a set of short tag strings whose CRC16 slots stay balanced across <em>every</em> cluster size from one node up to the partition count, not just a few sizes picked in advance. It runs once and bakes the result into a static table of string <strong>precomputed tags</strong> Centrifugo can then use, so at runtime it's just a lookup in that table.</p>\n<p>On that same 6-node cluster the 128 partitions now land <code>[21, 22, 21, 21, 22, 21]</code> — near-even, instead of the uneven spread the naive tags gave. Move to 12 nodes in Redis Cluster, and the tags still hold — <code>[11, 10, 11, 11, 10, 11, 11, 10, 11, 11, 10, 11]</code>, while naive tags would give <code>[10, 17, 5, 10, 16, 6, 8, 15, 9, 7, 16, 9]</code>. Past 128 nodes you'd have more nodes than partitions, so some would sit empty — but by then you'd raise the partition count.</p>\n<p>Here is a visualization of that:</p>\n<div class=\"rv-card\"><div class=\"rv-head\"><div><div class=\"rv-title\">Where partitions land: naive tags vs precomputed tags</div><div class=\"rv-subtitle\">Each partition is a hash tag; its CRC16 slot decides which Redis node owns it. Real tags, real CRC16 — the same table Centrifugo ships.</div></div><div class=\"rv-toggle\" style=\"--rv-accent:#f5c451\"><button type=\"button\" class=\"rv-toggle-btn\" data-active=\"true\">128</button><button type=\"button\" class=\"rv-toggle-btn\" data-active=\"false\">256</button><button type=\"button\" class=\"rv-toggle-btn\" data-active=\"false\">512</button><button type=\"button\" class=\"rv-toggle-btn\" data-active=\"false\">1024</button></div></div><div class=\"rv-controls\"><div class=\"rv-control\" style=\"--rv-accent:#f5c451\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Redis cluster nodes</span><span class=\"rv-control-value\" style=\"color:#f5c451\">6</span></div><input class=\"rv-range\" type=\"range\" min=\"2\" max=\"24\" step=\"1\" style=\"--rv-accent:#f5c451\" value=\"6\"></div><div class=\"rv-control\" style=\"flex:2 1 320px;align-self:flex-end;padding-bottom:2px\"><div class=\"rv-control-label\" style=\"line-height:1.5\">128<!-- --> partitions across <!-- -->6<!-- --> nodes · ideal ≈ <!-- -->21.3<!-- --> per node</div></div></div><svg viewBox=\"0 0 720 250\" class=\"rv-svg\" xmlns=\"http://www.w3.org/2000/svg\" style=\"margin:2px 0 6px\"><g transform=\"translate(30,26)\"><text x=\"0\" y=\"-10\" font-size=\"11.5\" font-weight=\"700\" fill=\"#fe5e5e\" font-family=\"system-ui, sans-serif\">Naive tags  {0} {1} … {127}</text><line x1=\"0\" y1=\"31.718769617074713\" x2=\"624\" y2=\"31.718769617074713\" stroke=\"#8a8a94\" stroke-width=\"1\" stroke-dasharray=\"3,4\" opacity=\"0.6\"></line><text x=\"632\" y=\"35.21876961707471\" font-size=\"9\" fill=\"#8a8a94\" text-anchor=\"start\" font-family=\"ui-monospace, monospace\">ideal</text><text x=\"632\" y=\"45.71876961707471\" font-size=\"9\" fill=\"#8a8a94\" text-anchor=\"start\" font-family=\"ui-monospace, monospace\">21.3</text><line x1=\"0\" y1=\"96\" x2=\"624\" y2=\"96\" stroke=\"#2c2c34\" stroke-width=\"1\"></line><g><rect x=\"0\" y=\"14.644067796610173\" width=\"101.5\" height=\"81.35593220338983\" rx=\"2\" fill=\"#fe5e5e\"></rect><text x=\"50.75\" y=\"10.644067796610173\" font-size=\"9.5\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"700\">27</text></g><g><rect x=\"104.5\" y=\"50.80225988700565\" width=\"101.5\" height=\"45.19774011299435\" rx=\"2\" fill=\"rgba(254,94,94,0.45)\"></rect><text x=\"155.25\" y=\"46.80225988700565\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">15</text></g><g><rect x=\"209\" y=\"29.70998116760829\" width=\"101.5\" height=\"66.29001883239171\" rx=\"2\" fill=\"rgba(254,94,94,0.78)\"></rect><text x=\"259.75\" y=\"25.70998116760829\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">22</text></g><g><rect x=\"313.5\" y=\"26.696798493408664\" width=\"101.5\" height=\"69.30320150659134\" rx=\"2\" fill=\"rgba(254,94,94,0.78)\"></rect><text x=\"364.25\" y=\"22.696798493408664\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">23</text></g><g><rect x=\"418\" y=\"47.78907721280602\" width=\"101.5\" height=\"48.21092278719398\" rx=\"2\" fill=\"rgba(254,94,94,0.78)\"></rect><text x=\"468.75\" y=\"43.78907721280602\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">16</text></g><g><rect x=\"522.5\" y=\"20.67043314500941\" width=\"101.5\" height=\"75.32956685499059\" rx=\"2\" fill=\"rgba(254,94,94,0.78)\"></rect><text x=\"573.25\" y=\"16.67043314500941\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">25</text></g></g><g transform=\"translate(30,154)\"><text x=\"0\" y=\"-10\" font-size=\"11.5\" font-weight=\"700\" fill=\"#5bef7b\" font-family=\"system-ui, sans-serif\">Precomputed tags</text><line x1=\"0\" y1=\"31.718769617074713\" x2=\"624\" y2=\"31.718769617074713\" stroke=\"#8a8a94\" stroke-width=\"1\" stroke-dasharray=\"3,4\" opacity=\"0.6\"></line><text x=\"632\" y=\"35.21876961707471\" font-size=\"9\" fill=\"#8a8a94\" text-anchor=\"start\" font-family=\"ui-monospace, monospace\">ideal</text><text x=\"632\" y=\"45.71876961707471\" font-size=\"9\" fill=\"#8a8a94\" text-anchor=\"start\" font-family=\"ui-monospace, monospace\">21.3</text><line x1=\"0\" y1=\"96\" x2=\"624\" y2=\"96\" stroke=\"#2c2c34\" stroke-width=\"1\"></line><g><rect x=\"0\" y=\"32.72316384180791\" width=\"101.5\" height=\"63.27683615819209\" rx=\"2\" fill=\"rgba(91,239,123,0.45)\"></rect><text x=\"50.75\" y=\"28.72316384180791\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">21</text></g><g><rect x=\"104.5\" y=\"29.70998116760829\" width=\"101.5\" height=\"66.29001883239171\" rx=\"2\" fill=\"#5bef7b\"></rect><text x=\"155.25\" y=\"25.70998116760829\" font-size=\"9.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"700\">22</text></g><g><rect x=\"209\" y=\"32.72316384180791\" width=\"101.5\" height=\"63.27683615819209\" rx=\"2\" fill=\"rgba(91,239,123,0.45)\"></rect><text x=\"259.75\" y=\"28.72316384180791\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">21</text></g><g><rect x=\"313.5\" y=\"32.72316384180791\" width=\"101.5\" height=\"63.27683615819209\" rx=\"2\" fill=\"rgba(91,239,123,0.45)\"></rect><text x=\"364.25\" y=\"28.72316384180791\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">21</text></g><g><rect x=\"418\" y=\"29.70998116760829\" width=\"101.5\" height=\"66.29001883239171\" rx=\"2\" fill=\"#5bef7b\"></rect><text x=\"468.75\" y=\"25.70998116760829\" font-size=\"9.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"700\">22</text></g><g><rect x=\"522.5\" y=\"32.72316384180791\" width=\"101.5\" height=\"63.27683615819209\" rx=\"2\" fill=\"rgba(91,239,123,0.45)\"></rect><text x=\"573.25\" y=\"28.72316384180791\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">21</text></g></g></svg><div class=\"rv-stats\"><div class=\"rv-stat\"><div class=\"rv-stat-label\">Naive — busiest vs lightest</div><div class=\"rv-stat-value\" style=\"color:#fe5e5e\">27 vs 15</div><div class=\"rv-stat-sub\">1.80× imbalance</div></div><div class=\"rv-stat\"><div class=\"rv-stat-label\">Precomputed — busiest vs lightest</div><div class=\"rv-stat-value\" style=\"color:#5bef7b\">22 vs 21</div><div class=\"rv-stat-sub\">perfectly even (max−min ≤ 1)</div></div></div><div class=\"rv-caption\">Drag the cluster size: the naive tags swing wildly at some sizes while the precomputed set stays flat at <b style=\"color:#5bef7b\">every</b> size from 1 node up to the partition count. Same partitions, same connection count — only the tag string each partition uses changes.</div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"reducing-connections-to-cluster\">Reducing connections to cluster<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#reducing-connections-to-cluster\" class=\"hash-link\" aria-label=\"Direct link to Reducing connections to cluster\" title=\"Direct link to Reducing connections to cluster\" translate=\"no\">​</a></h2>\n<p>Sharded Pub/Sub, combined with app-level partitions, solved the throughput problem — each message reaches a single node, and the cluster scales the way client-side sharding does. Redis nodes get balanced load. But a new problem shows up as the system grows: the connection count. There's a fixed number of partitions and many subscriber nodes, each opening its own connection per partition — and those two counts multiply together.</p>\n<p>Here's the math. Under the partition scheme each node opens one subscribe connection per partition, so 128 partitions across 200 nodes works out to <code>200 × 128 = 25,600</code> connections into the cluster, and spread over a 6-node cluster that's about 4,267 of them landing on each Redis node. That's the point where you start hitting real limits — <code>maxclients</code>, ephemeral ports, file descriptors, the overhead Redis and the app carry for every connection. This comes from a real setup — a Centrifugo user running 20 million WebSocket connections on AWS.</p>\n<p>Most of those connections are unnecessary. Each Redis node owns a whole <em>range</em> of slots, so 20-21 of the 128 partitions live on any given node — and a single connection to that node can subscribe to all of them. So it makes sense to <strong>group subscribe connections by Redis node</strong>.</p>\n<svg viewBox=\"0 0 760 326\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b;margin:1.2rem 0\"><line x1=\"380\" y1=\"44\" x2=\"380\" y2=\"300\" stroke=\"#2c2c34\" stroke-width=\"1\" stroke-dasharray=\"3,4\"></line><text x=\"20\" y=\"28\" font-size=\"12.5\" font-weight=\"bold\" fill=\"#fe5e5e\" font-family=\"system-ui, sans-serif\">Per-partition</text><rect x=\"20\" y=\"140\" width=\"84\" height=\"44\" rx=\"8\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.5\"></rect><text x=\"62\" y=\"166\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">consumer</text><g><line x1=\"104\" y1=\"158\" x2=\"280\" y2=\"65\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line><line x1=\"104\" y1=\"166\" x2=\"280\" y2=\"65\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line></g><g><line x1=\"104\" y1=\"158\" x2=\"280\" y2=\"101\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line><line x1=\"104\" y1=\"166\" x2=\"280\" y2=\"101\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line></g><g><line x1=\"104\" y1=\"158\" x2=\"280\" y2=\"137\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line><line x1=\"104\" y1=\"166\" x2=\"280\" y2=\"137\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line></g><g><line x1=\"104\" y1=\"158\" x2=\"280\" y2=\"173\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line><line x1=\"104\" y1=\"166\" x2=\"280\" y2=\"173\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line></g><g><line x1=\"104\" y1=\"158\" x2=\"280\" y2=\"209\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line><line x1=\"104\" y1=\"166\" x2=\"280\" y2=\"209\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line></g><g><line x1=\"104\" y1=\"158\" x2=\"280\" y2=\"245\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line><line x1=\"104\" y1=\"166\" x2=\"280\" y2=\"245\" stroke=\"#fe5e5e\" stroke-width=\"0.8\" opacity=\"0.55\"></line></g><g><rect x=\"280\" y=\"54\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"28042\" y=\"69\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->1</text></g><g><rect x=\"280\" y=\"90\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"28042\" y=\"105\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->2</text></g><g><rect x=\"280\" y=\"126\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"28042\" y=\"141\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->3</text></g><g><rect x=\"280\" y=\"162\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"28042\" y=\"177\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->4</text></g><g><rect x=\"280\" y=\"198\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"28042\" y=\"213\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->5</text></g><g><rect x=\"280\" y=\"234\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"28042\" y=\"249\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->6</text></g><text x=\"150\" y=\"270\" font-size=\"10.5\" fill=\"#fe5e5e\" font-family=\"monospace\">128 conns × 200 nodes</text><text x=\"150\" y=\"288\" font-size=\"11\" fill=\"#fe5e5e\" font-family=\"system-ui, sans-serif\" font-weight=\"bold\">≈ 25,600 conns · ~4,267 / node</text><text x=\"404\" y=\"28\" font-size=\"12.5\" font-weight=\"bold\" fill=\"#5bef7b\" font-family=\"system-ui, sans-serif\">Node-grouped</text><rect x=\"404\" y=\"140\" width=\"84\" height=\"44\" rx=\"8\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.5\"></rect><text x=\"446\" y=\"166\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">consumer</text><line x1=\"488\" y1=\"162\" x2=\"660\" y2=\"65\" stroke=\"#5bef7b\" stroke-width=\"1.3\" opacity=\"0.85\"></line><line x1=\"488\" y1=\"162\" x2=\"660\" y2=\"101\" stroke=\"#5bef7b\" stroke-width=\"1.3\" opacity=\"0.85\"></line><line x1=\"488\" y1=\"162\" x2=\"660\" y2=\"137\" stroke=\"#5bef7b\" stroke-width=\"1.3\" opacity=\"0.85\"></line><line x1=\"488\" y1=\"162\" x2=\"660\" y2=\"173\" stroke=\"#5bef7b\" stroke-width=\"1.3\" opacity=\"0.85\"></line><line x1=\"488\" y1=\"162\" x2=\"660\" y2=\"209\" stroke=\"#5bef7b\" stroke-width=\"1.3\" opacity=\"0.85\"></line><line x1=\"488\" y1=\"162\" x2=\"660\" y2=\"245\" stroke=\"#5bef7b\" stroke-width=\"1.3\" opacity=\"0.85\"></line><g><rect x=\"660\" y=\"54\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"66042\" y=\"69\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->1</text></g><g><rect x=\"660\" y=\"90\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"66042\" y=\"105\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->2</text></g><g><rect x=\"660\" y=\"126\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"66042\" y=\"141\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->3</text></g><g><rect x=\"660\" y=\"162\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"66042\" y=\"177\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->4</text></g><g><rect x=\"660\" y=\"198\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"66042\" y=\"213\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->5</text></g><g><rect x=\"660\" y=\"234\" width=\"84\" height=\"22\" rx=\"5\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"66042\" y=\"249\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"monospace\">node <!-- -->6</text></g><text x=\"534\" y=\"270\" font-size=\"10.5\" fill=\"#5bef7b\" font-family=\"monospace\">6 conns × 200 nodes</text><text x=\"534\" y=\"288\" font-size=\"11\" fill=\"#5bef7b\" font-family=\"system-ui, sans-serif\" font-weight=\"bold\">= 1,200 conns · 200 / node</text><text x=\"380\" y=\"316\" font-size=\"10.5\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Group by the cluster node that owns the slots: connections per consumer drop from num_partitions to num_redis_nodes.</text></svg>\n<p>With the same 200 nodes and the same 6-node cluster, the count then drops to <code>200 × 6 = 1,200</code> connections in total, just 200 per Redis node instead of 4,267.</p>\n<div class=\"rv-card\"><div class=\"rv-head\"><div><div class=\"rv-title\">Connections into the cluster: per-partition vs node-grouped</div><div class=\"rv-subtitle\">A connection per partition multiplies by every subscriber node. Grouping subscriptions by the Redis node that owns the slots collapses that count.</div></div></div><div class=\"rv-controls\"><div class=\"rv-control\" style=\"--rv-accent:#62b0ff\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Subscriber nodes</span><span class=\"rv-control-value\" style=\"color:#62b0ff\">200</span></div><input class=\"rv-range\" type=\"range\" min=\"10\" max=\"400\" step=\"10\" style=\"--rv-accent:#62b0ff\" value=\"200\"></div><div class=\"rv-control\" style=\"--rv-accent:#fe5e5e\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Partitions</span><span class=\"rv-control-value\" style=\"color:#fe5e5e\">128</span></div><input class=\"rv-range\" type=\"range\" min=\"64\" max=\"1024\" step=\"64\" style=\"--rv-accent:#fe5e5e\" value=\"128\"></div><div class=\"rv-control\" style=\"--rv-accent:#f5c451\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Redis nodes (cluster)</span><span class=\"rv-control-value\" style=\"color:#f5c451\">6</span></div><input class=\"rv-range\" type=\"range\" min=\"3\" max=\"24\" step=\"1\" style=\"--rv-accent:#f5c451\" value=\"6\"></div><div class=\"rv-control\" style=\"--rv-accent:#5bef7b\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Pipeline conns / target</span><span class=\"rv-control-value\" style=\"color:#5bef7b\">1</span></div><input class=\"rv-range\" type=\"range\" min=\"1\" max=\"4\" step=\"1\" style=\"--rv-accent:#5bef7b\" value=\"1\"></div></div><div class=\"rv-bars\"><div class=\"rv-bar-row\"><span class=\"rv-bar-name\" style=\"color:#fe5e5e\">Per-partition</span><div class=\"rv-bar-track\"><div class=\"rv-bar-fill\" style=\"width:100%;background:#fe5e5e\"></div><span class=\"rv-bar-value\" style=\"left:calc(100% - 8px);transform:translate(-100%, -50%);color:#11140f\">25,600<!-- --> conns</span></div></div><div class=\"rv-bar-row\"><span class=\"rv-bar-name\" style=\"color:#5bef7b\">Node-grouped</span><div class=\"rv-bar-track\"><div class=\"rv-bar-fill\" style=\"width:4.6875%;background:#5bef7b\"></div><span class=\"rv-bar-value\" style=\"left:calc(4.6875% + 8px);transform:translateY(-50%);color:#5bef7b\">1,200<!-- --> conns</span></div></div></div><div style=\"display:flex;justify-content:center;margin:16px 0 14px\"><span class=\"rv-reduce\"><b>21<!-- -->×</b>fewer connections</span></div><div class=\"rv-stats\"><div class=\"rv-stat\"><div class=\"rv-stat-label\">Per-partition · per Redis node</div><div class=\"rv-stat-value\" style=\"color:#f5c451\">4,267</div><div class=\"rv-stat-sub\">vs maxclients 10,000</div></div><div class=\"rv-stat\"><div class=\"rv-stat-label\">Node-grouped · per Redis node</div><div class=\"rv-stat-value\" style=\"color:#5bef7b\">200</div><div class=\"rv-stat-sub\">= subscriber nodes × pipeline conns</div></div></div><div class=\"rv-caption\"><span class=\"rv-formula\"><span class=\"rv-chip\" style=\"color:#fe5e5e\">200<!-- --> nodes</span><span class=\"rv-eq\">×</span><span class=\"rv-chip\" style=\"color:#fe5e5e\">128<!-- --> partitions</span><span class=\"rv-eq\">=</span><span class=\"rv-chip\" style=\"color:#fe5e5e\">25,600</span><span class=\"rv-eq\" style=\"margin:0 6px\">→ group by node →</span><span class=\"rv-chip\" style=\"color:#5bef7b\">200<!-- --> × <!-- -->6<!-- --> = <!-- -->1,200</span></span><div style=\"margin-top:8px\">Node-grouped connections don't depend on the partition count — drag <b style=\"color:#fe5e5e\">Partitions</b> and only the red bar moves. That's what lets the partition count rise for finer slot balance without adding any connections.</div></div></div>\n<p>With this decision, the partition count can then be raised — 1024 or 4096 instead of 128 — without adding connections. A higher count spreads channels across more slots, for a finer, more even distribution on larger clusters.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"keeping-reconnect-storms-cheap\">Keeping reconnect storms cheap<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#keeping-reconnect-storms-cheap\" class=\"hash-link\" aria-label=\"Direct link to Keeping reconnect storms cheap\" title=\"Direct link to Keeping reconnect storms cheap\" translate=\"no\">​</a></h2>\n<p>Connections are under control now. But the partitions earn their keep a second time, in a different place — reconnect storms.</p>\n<p>Partition grouping makes no difference during normal work: subscribing to one new channel is a single <code>SSUBSCRIBE</code>, no matter how channels are grouped. A network glitch or a failover, though, drops the connections, and across a large fleet many nodes resubscribe everything they hold at the same moment — that's when replaying all those subscriptions in as few commands as possible starts to count.</p>\n<p>There's a catch in how a node sends those resubscribes, and it's easy to get wrong. To resubscribe quickly the node splits its channels across several workers that run at the same time — say 16 of them. The obvious way to split the work is to give each worker a share of the channels. But one <code>SSUBSCRIBE</code> can only carry channels from a single partition, and if each worker holds a mix of channels from every partition, each worker has to send a separate <code>SSUBSCRIBE</code> per partition. With 21 partitions and 16 workers that's 16 × 21 = 336 commands, each carrying a thin slice of a partition.</p>\n<p>Split the work by partition instead — give each worker a few whole partitions to subscribe — and every partition is one <code>SSUBSCRIBE</code> again: 21 commands, not 336. The channels and the workers are the same — only the way the work is divided changes. On a real cluster this one change took a node's resubscribe from a few hundred commands down to a couple dozen.</p>\n<svg viewBox=\"0 0 760 250\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b;margin:1.2rem 0\"><defs><marker id=\"rws-green\" markerWidth=\"8\" markerHeight=\"6\" refX=\"6.5\" refY=\"3\" orient=\"auto\"><polygon points=\"0 0, 8 3, 0 6\" fill=\"#5bef7b\"></polygon></marker></defs><text x=\"380\" y=\"24\" font-size=\"12.5\" font-weight=\"bold\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">One reconnect, N parallel resubscribe workers</text><line x1=\"380\" y1=\"40\" x2=\"380\" y2=\"208\" stroke=\"#2c2c34\" stroke-width=\"1\" stroke-dasharray=\"3,4\"></line><text x=\"184\" y=\"44\" font-size=\"11.5\" font-weight=\"bold\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Workers shard by channel hash</text><g><line x1=\"88\" y1=\"73\" x2=\"272\" y2=\"87\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"73\" x2=\"272\" y2=\"127\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"73\" x2=\"272\" y2=\"167\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"109\" x2=\"272\" y2=\"87\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"109\" x2=\"272\" y2=\"127\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"109\" x2=\"272\" y2=\"167\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"145\" x2=\"272\" y2=\"87\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"145\" x2=\"272\" y2=\"127\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"145\" x2=\"272\" y2=\"167\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"181\" x2=\"272\" y2=\"87\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"181\" x2=\"272\" y2=\"127\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><line x1=\"88\" y1=\"181\" x2=\"272\" y2=\"167\" stroke=\"#fe5e5e\" stroke-width=\"1\" opacity=\"0.42\"></line><g opacity=\"1\"><rect x=\"28\" y=\"60\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.3\"></rect><text x=\"58\" y=\"77\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">w<!-- -->1</text></g><g opacity=\"1\"><rect x=\"28\" y=\"96\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.3\"></rect><text x=\"58\" y=\"113\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">w<!-- -->2</text></g><g opacity=\"1\"><rect x=\"28\" y=\"132\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.3\"></rect><text x=\"58\" y=\"149\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">w<!-- -->3</text></g><g opacity=\"1\"><rect x=\"28\" y=\"168\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#fe5e5e\" stroke-width=\"1.3\"></rect><text x=\"58\" y=\"185\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">w<!-- -->4</text></g><rect x=\"264\" y=\"62\" width=\"76\" height=\"130\" rx=\"9\" fill=\"none\" stroke=\"#2c2c34\" stroke-dasharray=\"4,4\" stroke-width=\"1\"></rect><text x=\"302\" y=\"56\" font-size=\"9\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Redis node</text><g><rect x=\"272\" y=\"74\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"302\" y=\"91\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">p<!-- -->1</text></g><g><rect x=\"272\" y=\"114\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"302\" y=\"131\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">p<!-- -->2</text></g><g><rect x=\"272\" y=\"154\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"302\" y=\"171\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">p<!-- -->3</text></g></g><text x=\"184\" y=\"208\" font-size=\"10\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">every partition's channels hit every worker</text><text x=\"184\" y=\"226\" font-size=\"11\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\" font-weight=\"bold\">N × partitions partial SSUBSCRIBEs</text><text x=\"576\" y=\"44\" font-size=\"11.5\" font-weight=\"bold\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Workers grouped by partition</text><g><line x1=\"488\" y1=\"73\" x2=\"672\" y2=\"87\" stroke=\"#5bef7b\" stroke-width=\"1.6\" opacity=\"0.9\" marker-end=\"url(#rws-green)\"></line><line x1=\"488\" y1=\"109\" x2=\"672\" y2=\"127\" stroke=\"#5bef7b\" stroke-width=\"1.6\" opacity=\"0.9\" marker-end=\"url(#rws-green)\"></line><line x1=\"488\" y1=\"145\" x2=\"672\" y2=\"167\" stroke=\"#5bef7b\" stroke-width=\"1.6\" opacity=\"0.9\" marker-end=\"url(#rws-green)\"></line><g opacity=\"1\"><rect x=\"428\" y=\"60\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"458\" y=\"77\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">w<!-- -->1</text></g><g opacity=\"1\"><rect x=\"428\" y=\"96\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"458\" y=\"113\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">w<!-- -->2</text></g><g opacity=\"1\"><rect x=\"428\" y=\"132\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#5bef7b\" stroke-width=\"1.3\"></rect><text x=\"458\" y=\"149\" font-size=\"10\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">w<!-- -->3</text></g><g opacity=\"0.4\"><rect x=\"428\" y=\"168\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#2c2c34\" stroke-width=\"1.3\"></rect><text x=\"458\" y=\"185\" font-size=\"10\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">w<!-- -->4</text></g><rect x=\"664\" y=\"62\" width=\"76\" height=\"130\" rx=\"9\" fill=\"none\" stroke=\"#2c2c34\" stroke-dasharray=\"4,4\" stroke-width=\"1\"></rect><text x=\"702\" y=\"56\" font-size=\"9\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Redis node</text><g><rect x=\"672\" y=\"74\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"702\" y=\"91\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">p<!-- -->1</text></g><g><rect x=\"672\" y=\"114\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"702\" y=\"131\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">p<!-- -->2</text></g><g><rect x=\"672\" y=\"154\" width=\"60\" height=\"26\" rx=\"6\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.2\"></rect><text x=\"702\" y=\"171\" font-size=\"9.5\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\">p<!-- -->3</text></g></g><text x=\"576\" y=\"208\" font-size=\"10\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">each worker owns whole partitions</text><text x=\"576\" y=\"226\" font-size=\"11\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\" font-weight=\"bold\">partitions full SSUBSCRIBEs — up to N× fewer</text></svg>\n<p>Partitions are why that command count is 21 and not thousands in the first place. On a 6-node cluster with 128 partitions each Redis node owns about 21 partitions. Drop partitions and that node owns about 2,731 of the 16,384 slots instead. So when a node resubscribes, its connection to one Redis node sends about <strong>21 <code>SSUBSCRIBE</code>s with partitions, or about 2,731 without</strong> — 130 times more. The count never goes past 16,384, and it never grows with the number of channels. (Each <code>SSUBSCRIBE</code> carries at most the batch size — 512 in Centrifugo's case — so once a partition holds more than that, both cases send about <code>channels / 512</code> commands and the gap closes.) That's nothing on its own, but in a fleet-wide reconnect every subscriber node fires its commands at each Redis node at once — right when the cluster is busy recovering — so keeping the per-node count low matters most exactly then.</p>\n<div class=\"rv-card\"><div class=\"rv-head\"><div><div class=\"rv-title\">Reconnect storm: SSUBSCRIBE commands per Redis node</div><div class=\"rv-subtitle\">When a node reconnects it replays every subscription. Channels are resubscribed per partition, batched 512 at a time — so partitions turn a node's whole subscription set into a handful of fat commands instead of thousands.</div></div><div class=\"rv-toggle\" style=\"--rv-accent:#5bef7b\"><button type=\"button\" class=\"rv-toggle-btn\" data-active=\"true\">128</button><button type=\"button\" class=\"rv-toggle-btn\" data-active=\"false\">256</button><button type=\"button\" class=\"rv-toggle-btn\" data-active=\"false\">512</button><button type=\"button\" class=\"rv-toggle-btn\" data-active=\"false\">1024</button></div></div><div class=\"rv-controls\"><div class=\"rv-control\" style=\"--rv-accent:#62b0ff\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Channels per subscriber node</span><span class=\"rv-control-value\" style=\"color:#62b0ff\">20k</span></div><input class=\"rv-range\" type=\"range\" min=\"0\" max=\"1000\" step=\"1\" style=\"--rv-accent:#62b0ff\" value=\"394\"></div><div class=\"rv-control\" style=\"--rv-accent:#f5c451\"><div class=\"rv-control-head\"><span class=\"rv-control-label\">Redis cluster nodes</span><span class=\"rv-control-value\" style=\"color:#f5c451\">6</span></div><input class=\"rv-range\" type=\"range\" min=\"3\" max=\"24\" step=\"1\" style=\"--rv-accent:#f5c451\" value=\"6\"></div></div><div class=\"rv-bars\"><div class=\"rv-bar-row\"><span class=\"rv-bar-name\" style=\"color:#fe5e5e\">Raw slots<span style=\"display:block;font-size:9.5px;color:#8a8a94;font-weight:400\">channels scatter over ~2,731 slots</span></span><div class=\"rv-bar-track\"><div class=\"rv-bar-fill\" style=\"width:100%;background:#fe5e5e\"></div><span class=\"rv-bar-value\" style=\"left:calc(100% - 8px);transform:translate(-100%, -50%);color:#11140f\">2,731<!-- --> cmds</span></div></div><div class=\"rv-bar-row\"><span class=\"rv-bar-name\" style=\"color:#5bef7b\">Partitioned<span style=\"display:block;font-size:9.5px;color:#8a8a94;font-weight:400\">channels land on ~21 partition-slots</span></span><div class=\"rv-bar-track\"><div class=\"rv-bar-fill\" style=\"width:1.4%;background:#5bef7b\"></div><span class=\"rv-bar-value\" style=\"left:calc(1.4% + 8px);transform:translateY(-50%);color:#5bef7b\">21<!-- --> cmds</span></div></div></div><div style=\"display:flex;justify-content:center;margin:16px 0 4px\"><span class=\"rv-reduce\"><b>130<!-- -->×</b>fewer SSUBSCRIBE commands</span></div><div class=\"rv-caption\">On reconnect the node groups its channels by partition and sends one <span class=\"rv-mono\">SSUBSCRIBE</span> per partition (chunked at<!-- --> <!-- -->512<!-- -->). A node owns ~<!-- -->21<!-- --> partitions, so a reconnect is on the order of ~<!-- -->21<!-- --> commands per Redis node — climbing only once a single partition holds more than <!-- -->512<!-- --> channels. Drop partitions and those channels scatter across the ~<!-- -->2,731<!-- --> raw slots the node owns, so the same resubscribe needs many times more commands. Across a whole fleet reconnecting at once, every command lands on each Redis node, times every subscriber node — so keeping the count low matters most exactly when the cluster is busy recovering.</div></div>\n<p>So partitions stay — not for the connection count any more, but to keep these big resubscribes cheap. Dropping them is a fair choice too: you get the finest, most even distribution, but each resubscribe costs more. That's the trade-off you control.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"following-the-cluster-as-it-moves\">Following the cluster as it moves<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#following-the-cluster-as-it-moves\" class=\"hash-link\" aria-label=\"Direct link to Following the cluster as it moves\" title=\"Direct link to Following the cluster as it moves\" translate=\"no\">​</a></h2>\n<p>Grouping subscriptions by node isn't free — it adds complexity, mostly around tracking the cluster topology. This mechanism won't be available out of the box in most Redis drivers. Luckily the <a href=\"https://github.com/redis/rueidis\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">rueidis</a> library Centrifugo uses to talk to Redis exposes enough low-level Redis Cluster knobs to build it.</p>\n<p>Because the application node decides which Redis node each subscription goes to, it has to track the cluster's layout itself. It keeps a small map — each partition to the Redis node that owns its slot — built from <code>CLUSTER SLOTS</code> and the client's node list. Each Redis node gets one connection carrying the partitions it owns. The cluster can reshuffle this at any time — a node joins, a node leaves, or slots move to rebalance — and the app has to notice each change and rewire.</p>\n<p>There are two ways it knows about changes. A background loop re-runs <code>CLUSTER SLOTS</code> every 30 seconds (in Centrifugo's case) and rebuilds the map; if the node count has changed, or any partition now points at a different node, the layout has changed. But the poll isn't the only signal. When the cluster moves a slot off a node, Redis sends a <code>sunsubscribe</code> for the sharded channels held there on its own — the node announcing it no longer owns them. The subscriber node catches that signal on the subscribe connection and triggers a rebuild right away, so a removed node or a moved slot doesn't wait up to 30 seconds to take effect.</p>\n<p>A rebuild recomputes the map and rewires the connections: open one to a Redis node that appeared, drop the one to a node that left, and resubscribe each partition on its new owner. Publishes need no help — a cluster-aware client already sends each <code>SPUBLISH</code> to whichever node owns the slot — so once the subscribe side re-points, publisher and subscriber meet on the new node again.</p>\n<svg viewBox=\"0 0 760 250\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b;margin:1.2rem 0\"><text x=\"380\" y=\"24\" font-size=\"13\" font-weight=\"bold\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">When a slot moves, the app rewires the subscription</text><line x1=\"380\" y1=\"38\" x2=\"380\" y2=\"206\" stroke=\"#2c2c34\" stroke-width=\"1\" stroke-dasharray=\"3,4\"></line><g><text x=\"180\" y=\"44\" font-size=\"11.5\" font-weight=\"700\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Before</text><text x=\"180\" y=\"59\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">p4's slot lives on node B</text><line x1=\"180\" y1=\"176\" x2=\"74\" y2=\"122\" stroke=\"#2c2c34\" stroke-width=\"1.1\" opacity=\"0.6\"></line><line x1=\"180\" y1=\"176\" x2=\"182\" y2=\"122\" stroke=\"#5bef7b\" stroke-width=\"1.8\" opacity=\"0.95\"></line><line x1=\"180\" y1=\"176\" x2=\"290\" y2=\"122\" stroke=\"#2c2c34\" stroke-width=\"1.1\" opacity=\"0.6\"></line><g><rect x=\"26\" y=\"72\" width=\"96\" height=\"50\" rx=\"7\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.3\"></rect><text x=\"74\" y=\"89\" font-size=\"10\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->A</text><text x=\"60\" y=\"109\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">p1</text><text x=\"88\" y=\"109\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">p2</text></g><g><rect x=\"134\" y=\"72\" width=\"96\" height=\"50\" rx=\"7\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.3\"></rect><text x=\"182\" y=\"89\" font-size=\"10\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->B</text><text x=\"168\" y=\"109\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">p3</text><text x=\"196\" y=\"109\" font-size=\"10.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"700\">p4</text></g><g><rect x=\"242\" y=\"72\" width=\"96\" height=\"50\" rx=\"7\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.3\"></rect><text x=\"290\" y=\"89\" font-size=\"10\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->C</text><text x=\"276\" y=\"109\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">p5</text><text x=\"304\" y=\"109\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">p6</text></g><rect x=\"118\" y=\"176\" width=\"124\" height=\"28\" rx=\"7\" fill=\"#23242b\" stroke=\"#62b0ff\" stroke-width=\"1.4\"></rect><text x=\"180\" y=\"194\" font-size=\"10\" fill=\"#62b0ff\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">application node</text></g><g><text x=\"580\" y=\"44\" font-size=\"11.5\" font-weight=\"700\" fill=\"#d6d6db\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">After rebalance</text><text x=\"580\" y=\"59\" font-size=\"9.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">p4's slot moved to node C</text><line x1=\"580\" y1=\"176\" x2=\"474\" y2=\"122\" stroke=\"#2c2c34\" stroke-width=\"1.1\" opacity=\"0.6\"></line><line x1=\"580\" y1=\"176\" x2=\"582\" y2=\"122\" stroke=\"#2c2c34\" stroke-width=\"1.1\" opacity=\"0.6\"></line><line x1=\"580\" y1=\"176\" x2=\"690\" y2=\"122\" stroke=\"#5bef7b\" stroke-width=\"1.8\" opacity=\"0.95\"></line><g><rect x=\"426\" y=\"72\" width=\"96\" height=\"50\" rx=\"7\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.3\"></rect><text x=\"474\" y=\"89\" font-size=\"10\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->A</text><text x=\"460\" y=\"109\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">p1</text><text x=\"488\" y=\"109\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">p2</text></g><g><rect x=\"534\" y=\"72\" width=\"96\" height=\"50\" rx=\"7\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.3\"></rect><text x=\"582\" y=\"89\" font-size=\"10\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->B</text><text x=\"582\" y=\"109\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">p3</text></g><g><rect x=\"642\" y=\"72\" width=\"96\" height=\"50\" rx=\"7\" fill=\"#23242b\" stroke=\"#f5c451\" stroke-width=\"1.3\"></rect><text x=\"690\" y=\"89\" font-size=\"10\" fill=\"#f5c451\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">node <!-- -->C</text><text x=\"662\" y=\"109\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">p5</text><text x=\"690\" y=\"109\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"400\">p6</text><text x=\"718\" y=\"109\" font-size=\"10.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"700\">p4</text></g><rect x=\"518\" y=\"176\" width=\"124\" height=\"28\" rx=\"7\" fill=\"#23242b\" stroke=\"#62b0ff\" stroke-width=\"1.4\"></rect><text x=\"580\" y=\"194\" font-size=\"10\" fill=\"#62b0ff\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">application node</text></g><rect x=\"350\" y=\"80\" width=\"60\" height=\"20\" rx=\"10\" fill=\"#17171b\" stroke=\"#5bef7b\" stroke-width=\"1\"></rect><text x=\"380\" y=\"93\" font-size=\"9.5\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"ui-monospace, monospace\" font-weight=\"700\">p4 ▸ C</text><text x=\"380\" y=\"234\" font-size=\"10.5\" fill=\"#8a8a94\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">The app spots the move — an unsolicited sunsubscribe, or the 30-second CLUSTER SLOTS poll — and resubscribes p4 on node C's connection.</text></svg>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>One catch: just after a node joins, the rueidis client may not have a connection to it yet. Until it does, the slots that new node owns don't map to any node the client knows about, so the partition-to-node map can't be completed. To push the client to update, the subscriber node issues a throwaway <code>GET</code> for a probe key to provoke a <code>MOVED</code> redirect — that makes rueidis re-read the cluster layout and pick up the new node in time for the next rebuild.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"cluster-or-client-side-sharding\">Cluster or client-side sharding?<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#cluster-or-client-side-sharding\" class=\"hash-link\" aria-label=\"Direct link to Cluster or client-side sharding?\" title=\"Direct link to Cluster or client-side sharding?\" translate=\"no\">​</a></h2>\n<p>With node grouping in place, a Redis Cluster ends up working like client-side sharding: every Redis node is an independent single instance, owning its own slots and carrying no cross-node Pub/Sub traffic. Each node's ceiling is a single core — about 650k messages a second in these benchmarks — and throughput scaled linearly as cluster nodes were added, each one giving about another 650k, the same as adding client-side shards. Run side by side, the two were hard to tell apart: low latency, with the load split evenly and each node carrying its 1/N share.</p>\n<p>Here are the two approaches compared:</p>\n<table><thead><tr><th></th><th>Client-side sharding</th><th>Node-grouped Redis Cluster</th></tr></thead><tbody><tr><td><strong>Throughput</strong></td><td>N × single-Redis</td><td>the same</td></tr><tr><td><strong>Connections</strong></td><td>nodes × Redis instances</td><td>nodes × Redis instances (similar)</td></tr><tr><td><strong>Membership &amp; failover</strong></td><td>you build it yourself</td><td>the cluster handles it</td></tr><tr><td><strong>Adding capacity</strong></td><td>edit the instance list by hand, every node reloads</td><td>add a node, slots rebalance on their own</td></tr><tr><td><strong>What you maintain</strong></td><td>a Redis list in config</td><td>extra code for node-grouping</td></tr></tbody></table>\n<p>Client-side sharding is simpler — it's a list of Redis addresses and a hash function, nothing more — and for a lot of deployments that's plenty, as long as you don't mind editing that list and resharding by hand as you grow. Redis Cluster is worth its extra complexity when you'd rather not do that by hand: it manages membership, failover, and resharding itself, and node grouping is what keeps its Pub/Sub scaling while it does.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"summing-up\">Summing up<a href=\"https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub#summing-up\" class=\"hash-link\" aria-label=\"Direct link to Summing up\" title=\"Direct link to Summing up\" translate=\"no\">​</a></h2>\n<p>A single Redis Pub/Sub instance scales well until one core saturates. Past that you need more Redis nodes, either through client-side sharding or Redis Cluster.</p>\n<p>Redis Cluster, though, broadcasts every published message to all nodes by default — so adding nodes lowers throughput instead of raising it, and subscriptions can pile unevenly onto one node.</p>\n<p>Sharded Pub/Sub fixes that by turning the cluster into a set of independent Redis instances, just like client-side sharding — so capacity scales linearly as you add nodes, but without you having to manage the instance list yourself.</p>\n<p>For Centrifugo's specific situation — a large number of active channels, a high subscribe/unsubscribe rate, reconnect storms, and lots of subscriber nodes — a few more tricks were needed to make it actually work:</p>\n<ul>\n<li class=\"\">App-level partitions using hash tags to restore efficient batching and control connection growth.</li>\n<li class=\"\">Precomputed tags that keep slot balance even across any cluster size without retuning.</li>\n<li class=\"\">Node-grouped subscriptions that cut connections from nodes × partitions down to nodes × Redis nodes — one connection per Redis node instead of one per partition.</li>\n<li class=\"\">Partition-grouped resubscribes that keep reconnect storms cheap: after a drop, each node replays its subscriptions in a handful of <code>SSUBSCRIBE</code>s per Redis node instead of hundreds.</li>\n<li class=\"\">Topology tracking that follows slot moves and resubscribes affected partitions on their new owner.</li>\n</ul>\n<p>None of this is a recipe for endless scale. What it gives is a good state to grow from: the Pub/Sub layer stays balanced as the cluster gets larger, and the connection count no longer grows out of control. So you can move to a bigger Redis Cluster and run more subscriber nodes without the number of connections becoming the thing you hit first. Where the real ceiling lands after that depends on the rest of the system — the publishers, the application nodes, the traffic per channel — not on the Pub/Sub itself.</p>\n<p>Pub/Sub is not the only load an application generates. You usually also have plain GET/SET operations, which scale well in Redis Cluster without extra engineering. It's often worth running Pub/Sub and GET/SET on separate Redis setups, so each one can be tuned independently.</p>\n<p>Thank you for your attention!</p>",
            "url": "https://centrifugal.dev/blog/2026/06/29/scaling-redis-pub-sub",
            "title": "Scaling Redis Pub/Sub to Millions of Channels and Hundreds of Subscriber Nodes",
            "summary": "How Centrifugo scaled Redis Pub/Sub — talking to Redis efficiently, sharding across isolated Redis instances, and making Pub/Sub work on Redis Cluster: sharded Pub/Sub, slot balance, connection count, and efficient resubscribes.",
            "date_modified": "2026-06-29T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "redis",
                "pubsub",
                "scalability"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2026/05/25/pg-controller-multi-node",
            "content_html": "<p>For most of <a href=\"https://centrifugal.dev/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo</a>'s history, scaling past one node meant adding Redis. The new PostgreSQL controller makes Redis optional. Together with the <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits\">stream broker</a> and <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2\">map broker</a> shipped earlier in this release cycle, Centrifugo's full OSS messaging cluster now runs on the PostgreSQL only.</p>\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>New and evolving</div><div class=\"admonitionContent_BuS1\"><p>Available in Centrifugo v6.8.0+. The PostgreSQL controller is a recent addition — we're eager for feedback. Configuration keys, schema, and outbox internals may still adjust before they're considered stable.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"why-redis-was-the-oss-reality\">Why Redis was the OSS reality<a href=\"https://centrifugal.dev/blog/2026/05/25/pg-controller-multi-node#why-redis-was-the-oss-reality\" class=\"hash-link\" aria-label=\"Direct link to Why Redis was the OSS reality\" title=\"Direct link to Why Redis was the OSS reality\" translate=\"no\">​</a></h2>\n<p>A single Centrifugo node is self-contained — clients connect, the node tracks subscriptions in memory, publishes go straight to subscribers. Once a second node added, that breaks: a publish that arrives at node A needs to reach the clients connected to node B; subscribes, unsubscribes, and disconnects need to propagate; each node needs to know the live cluster topology. Centrifugo solves this with a <code>Controller</code> interface that distributes control messages between nodes.</p>\n<p>Before v6.8.0, that interface had two implementations: Redis and NATS. NATS, however, is a Centrifugo PRO option. So the OSS reality was effectively <em>Redis or single-node</em>. Teams already running Redis for caching paid no extra operational cost. Teams running PostgreSQL as their primary store and nothing else had to provision and operate a Redis instance just to scale Centrifugo horizontally — even small deployments where PG could have handled the messaging traffic just fine.</p>\n<p>That gap is what the PostgreSQL controller closes.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"how-the-controller-works\">How the controller works<a href=\"https://centrifugal.dev/blog/2026/05/25/pg-controller-multi-node#how-the-controller-works\" class=\"hash-link\" aria-label=\"Direct link to How the controller works\" title=\"Direct link to How the controller works\" translate=\"no\">​</a></h2>\n<p>The controller uses the same outbox pattern that powers the <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits\">PG stream broker</a> and the <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2\">PG map broker</a>:</p>\n<ul>\n<li class=\"\">Control messages are written to a partitioned PostgreSQL table — one row per message.</li>\n<li class=\"\">Each Centrifugo node polls the table for new entries and processes them.</li>\n<li class=\"\"><code>LISTEN/NOTIFY</code> provides low-latency wakeup, so polling cadence stays low under quiet load while latency stays in the low single-digit milliseconds when traffic exists.</li>\n<li class=\"\">Old partitions are dropped whole — vacuum-free cleanup that keeps long-lived deployments tidy without any manual maintenance.</li>\n</ul>\n<p>The shape simply mirrors PostgreSQL brokers we described in recent blog posts.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"what-flows-through-the-controller\">What flows through the controller<a href=\"https://centrifugal.dev/blog/2026/05/25/pg-controller-multi-node#what-flows-through-the-controller\" class=\"hash-link\" aria-label=\"Direct link to What flows through the controller\" title=\"Direct link to What flows through the controller\" translate=\"no\">​</a></h2>\n<p>The controller carries cross-node operations Centrifugo needs to coordinate:</p>\n<ul>\n<li class=\"\"><strong>Subscribe propagation</strong> — when a node subscribes a connected client to a channel, peer nodes need to know so future publications addressed to that client reach it even if they originate elsewhere.</li>\n<li class=\"\"><strong>Unsubscribe and disconnect</strong> — server-issued disconnects targeting a specific user or client fan out across the cluster so the right connection terminates regardless of which node holds it.</li>\n<li class=\"\"><strong>Presence pings</strong> — periodic heartbeats so each node knows the live cluster topology and can detect peer failures.</li>\n<li class=\"\"><strong>Surveys</strong> — broadcast queries (e.g. \"how many connections to this channel cluster-wide\") and their responses.</li>\n</ul>\n<p>Publication delivery itself does <strong>not</strong> flow through the controller — that's the broker's job. Stream and map brokers each have their own outbox tables and their own delivery paths. The controller is purely the cross-node coordination layer.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"the-messaging-plane-on-postgresql-alone\">The messaging plane on PostgreSQL alone<a href=\"https://centrifugal.dev/blog/2026/05/25/pg-controller-multi-node#the-messaging-plane-on-postgresql-alone\" class=\"hash-link\" aria-label=\"Direct link to The messaging plane on PostgreSQL alone\" title=\"Direct link to The messaging plane on PostgreSQL alone\" translate=\"no\">​</a></h2>\n<p>With the stream broker, the map broker, and the controller all on PostgreSQL, an OSS cluster can run with one infrastructure dependency for the entire messaging plane:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json — multi-node, PG-only messaging</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"broker\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"enabled\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"postgres\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"postgres\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token property\">\"dsn\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"...\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token property\">\"use_notify\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"map_broker\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"postgres\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"postgres\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token property\">\"dsn\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"...\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token property\">\"use_notify\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"controller\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"enabled\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"postgres\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"postgres\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token property\">\"dsn\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"...\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token property\">\"use_notify\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>The same DSN can be reused across all three components, or split across separate PostgreSQL instances if you want to isolate the data plane from the control plane. All three create their own tables, partitions, and SQL functions on startup — there's no manual migration step.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"when-to-choose-what\">When to choose what<a href=\"https://centrifugal.dev/blog/2026/05/25/pg-controller-multi-node#when-to-choose-what\" class=\"hash-link\" aria-label=\"Direct link to When to choose what\" title=\"Direct link to When to choose what\" translate=\"no\">​</a></h2>\n<p>The PostgreSQL controller doesn't replace Redis or NATS for everyone. A rough way to decide:</p>\n<ul>\n<li class=\"\"><strong>PostgreSQL controller</strong> — best for OSS deployments that already run PG, expect cluster sizes in the small-to-mid range (a handful of nodes), and want fewer services to manage. Latency under typical load is low single-digit milliseconds with <code>use_notify</code>.</li>\n<li class=\"\"><strong>Redis controller</strong> — best when sub-millisecond control-plane coordination matters more than running fewer services, or when the deployment runs Redis for other reasons. Centrifugo's Redis controller has been the production default for years and remains the right pick for high-throughput control planes.</li>\n<li class=\"\"><strong>NATS controller</strong> (PRO) — best for very large clusters where Redis pub/sub fan-out becomes the bottleneck, or for deployments already standardized on NATS.</li>\n</ul>\n<p>For OSS deployments where Redis was added only to support multi-node Centrifugo, the PG controller is the cleaner option going forward. For deployments already running Redis as a cache or session store, there's no need to migrate off it — no functional reason to switch if Redis is already there for other purposes.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"getting-started\">Getting started<a href=\"https://centrifugal.dev/blog/2026/05/25/pg-controller-multi-node#getting-started\" class=\"hash-link\" aria-label=\"Direct link to Getting started\" title=\"Direct link to Getting started\" translate=\"no\">​</a></h2>\n<p>Enable the PostgreSQL controller alongside any existing broker configuration:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"controller\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"enabled\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"postgres\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"postgres\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"dsn\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"postgres://user:pass@localhost:5432/app?sslmode=disable\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"use_notify\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"partition_retention_days\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">7</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>The controller creates its required tables, partitions, and SQL functions on startup. Bring up two or more Centrifugo nodes pointing at the same PostgreSQL, and they'll coordinate without further setup.</p>\n<p>For background on the outbox pattern shared with the brokers, see the <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits\">PG stream broker post</a> and the <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2\">PG map broker post</a>. The full configuration reference lives in the <a class=\"\" href=\"https://centrifugal.dev/docs/server/engines\">engines documentation</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"a-runnable-demo\">A runnable demo<a href=\"https://centrifugal.dev/blog/2026/05/25/pg-controller-multi-node#a-runnable-demo\" class=\"hash-link\" aria-label=\"Direct link to A runnable demo\" title=\"Direct link to A runnable demo\" translate=\"no\">​</a></h2>\n<p>A working three-node example lives in <a href=\"https://github.com/centrifugal/examples/tree/master/v6/pg_cluster_demo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>examples/v6/pg_cluster_demo</code></a>. It boots a single PostgreSQL container, three local Centrifugo nodes pointing at the same DSN, and a static page that exercises all three components at once:</p>\n<img src=\"https://centrifugal.dev/img/demo_pg_only.jpg\">\n<br>\n<br>\n<ul>\n<li class=\"\">The <strong>cluster topology</strong> panel polls <code>/api/info</code> and lists every node — that list is built from the heartbeats flowing through the <strong>PG controller</strong>.</li>\n<li class=\"\">The <strong>online here</strong> panel is a <code>map_clients</code> map subscription whose state lives in PostgreSQL via the <strong>PG map broker</strong>, so every tab sees the same presence rows regardless of which node served it. We generally don't recommend PostgreSQL for presence — it's lightweight ephemeral data that fits Redis better. But for use cases with a reasonable number of concurrent clients, it works fine.</li>\n<li class=\"\">The <strong>chat</strong> panel is a stream subscription on a channel served by the <strong>PG stream broker</strong> — a publish on one node arrives at subscribers on the other two within milliseconds via the <code>LISTEN/NOTIFY</code> wakeup.</li>\n</ul>\n<p>Each browser tab connects to a specific node via <code>?n=1|2|3</code>, which makes the cross-node fan-out visible: type a message in the tab on <code>node-1</code> and watch it appear in the tabs on <code>node-2</code> and <code>node-3</code>.</p>",
            "url": "https://centrifugal.dev/blog/2026/05/25/pg-controller-multi-node",
            "title": "Multi-node Centrifugo on PostgreSQL alone",
            "summary": "A new PostgreSQL controller in Centrifugo OSS lets multi-node clusters run without Redis or NATS. If your application already runs PostgreSQL, the messaging plane has everything it needs — one infrastructure dependency for the whole real-time tier.",
            "date_modified": "2026-05-25T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "postgresql",
                "scaling",
                "controller"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits",
            "content_html": "<p>In <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2\">Part 2 of the map subscriptions series</a>, we introduced a PostgreSQL map broker that lets your application publish real-time map updates inside a database transaction — removing the dual-write problem when you publish via the broker's SQL function from your own transactions. That capability applied only to map subscriptions — keyed state like leaderboards, collaborative boards, and inventories.</p>\n<p>Today we're extending the same shape to <strong>stream subscriptions</strong> — the ordered-event primitive that powers notifications, activity feeds, chat messages, audit logs, and order updates. If you have a database row and you want to announce a change in real time, you can now do it atomically with your write — same <code>BEGIN / COMMIT</code>, same outbox architecture, same \"no Redis\" simplicity.</p>\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>New and evolving</div><div class=\"admonitionContent_BuS1\"><p>Available in Centrifugo v6.8.0+. The PostgreSQL stream broker is a recent addition — we're eager for feedback. SQL function shapes, configuration keys, and outbox internals may still adjust before they're considered stable.</p></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>TL;DR</div><div class=\"admonitionContent_BuS1\"><ul>\n<li class=\"\">Call <code>cf_stream_publish(...)</code> inside the same SQL transaction as your row write — both commit atomically. No outbox table to manage in your app, no CDC pipeline, no dual-write gap.</li>\n<li class=\"\">The broker shares its outbox infrastructure with the <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2\">PG map broker</a> — partitioned table, <code>LISTEN/NOTIFY</code> for low-latency wakeup, vacuum-free retention.</li>\n<li class=\"\">Worked through below: per-tenant channels — one per restaurant over a shared <code>orders</code> table — with a runnable kitchen-orders demo.</li>\n<li class=\"\">The client side of this (loading app-owned state, staying in sync across reconnects via the SDK's <code>getState</code> callback) is covered in <a class=\"\" href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions\">App-owned state with stream subscriptions</a>.</li>\n</ul></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"the-dual-write-problem-revisited\">The dual-write problem, revisited<a href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits#the-dual-write-problem-revisited\" class=\"hash-link\" aria-label=\"Direct link to The dual-write problem, revisited\" title=\"Direct link to The dual-write problem, revisited\" translate=\"no\">​</a></h2>\n<p>Integrating a real-time system with a relational database creates the same gap: the backend writes to the database, then publishes to the real-time layer as a separate operation. If the process crashes between them — or if the publish fails — the database and subscribers fall out of sync. Users see stale data until they refresh.</p>\n<p>We <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2\">covered this in depth</a> for map subscriptions. The same problem applies — even more broadly — to stream subscriptions. Every notification system, every audit trail, every order-status feed has the same shape: write a row to your database, then announce the change over WebSocket. The PostgreSQL stream broker lets you combine both into one transaction.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"publishing-inside-your-transaction\">Publishing inside your transaction<a href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits#publishing-inside-your-transaction\" class=\"hash-link\" aria-label=\"Direct link to Publishing inside your transaction\" title=\"Direct link to Publishing inside your transaction\" translate=\"no\">​</a></h2>\n<p>Centrifugo creates a <code>cf_stream_publish</code> SQL function when the PostgreSQL stream broker initializes. Your application calls it inside its own transaction:</p>\n<div class=\"language-sql codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-sql codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">BEGIN</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">-- Business logic: update order status</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">UPDATE</span><span class=\"token plain\"> orders </span><span class=\"token keyword\" style=\"font-style:italic\">SET</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">status</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'shipped'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> updated_at </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NOW</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">WHERE</span><span class=\"token plain\"> id </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">42</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">-- Publish to real-time channel (same transaction)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">SELECT</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">FROM</span><span class=\"token plain\"> cf_stream_publish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    p_channel :</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'orders:42'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    p_data    :</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'{\"status\": \"shipped\"}'</span><span class=\"token plain\">::jsonb</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">COMMIT</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>If the transaction rolls back, the real-time update never happened. No outbox table to manage in your application, no CDC pipeline, no eventual consistency — just a single transaction. The architecture is the same outbox pattern we use for map subscriptions: all writes land in PostgreSQL tables atomically, and Centrifugo's outbox workers pick up new entries and deliver them to subscribers. When <code>use_notify</code> is enabled, delivery latency drops to low single-digit milliseconds.</p>\n<p>The transactional guarantee applies to <strong>callers using the SQL function path</strong> — i.e. your backend code calling <code>cf_stream_publish</code> directly inside its own SQL transaction alongside the row write. Publishes that go through Centrifugo's HTTP/GRPC API remain a separate operation from your DB write (the historic dual-write shape) — same as before. The SQL function path is what removes that gap; it's an additional integration option, not a change to existing publish APIs.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"a-concrete-example-per-tenant-channels\">A concrete example: per-tenant channels<a href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits#a-concrete-example-per-tenant-channels\" class=\"hash-link\" aria-label=\"Direct link to A concrete example: per-tenant channels\" title=\"Direct link to A concrete example: per-tenant channels\" translate=\"no\">​</a></h2>\n<p>A kitchen-orders system shows the shape well: a single <code>orders</code> table shared by all restaurants, and one channel per restaurant — <code>kitchen:{restaurant_id}</code> — so each kitchen display only sees its own. The demo below is runnable from the <a href=\"https://github.com/centrifugal/examples/tree/master/v6/pg_stream_broker\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">pg_stream_broker example</a> on GitHub:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_kitchen.mp4\"></video>\n<p>Every write to a restaurant's orders commits atomically with a publish on that restaurant's channel:</p>\n<div class=\"language-sql codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-sql codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">BEGIN</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">INSERT</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">INTO</span><span class=\"token plain\"> orders </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> restaurant_id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">status</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> items</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> updated_at</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">VALUES</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">7001</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">42</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'received'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> $</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NOW</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">SELECT</span><span class=\"token plain\"> cf_stream_publish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    p_channel :</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'kitchen:42'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    p_data    :</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'{\"order_id\":7001,\"status\":\"received\",...}'</span><span class=\"token plain\">::jsonb</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">COMMIT</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Status transitions (<code>received</code> → <code>preparing</code> → <code>ready</code> → <code>served</code>) do the same — <code>UPDATE orders</code> plus <code>cf_stream_publish(p_channel := 'kitchen:42', …)</code> in one transaction. The application follows one rule: every code path that mutates a row for restaurant X emits the publish for <code>kitchen:X</code> in the same transaction.</p>\n<p>Each channel has its own meta row and its own top offset; writes on <code>kitchen:99</code> never block or interfere with <code>kitchen:42</code>. This scales to thousands of tenants on a single PostgreSQL — each channel is an independent append-only stream, and the shared <code>cf_stream</code> partitioned table absorbs the union.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"app-owned-state-with-stream-subscriptions\">App-owned state with stream subscriptions<a href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits#app-owned-state-with-stream-subscriptions\" class=\"hash-link\" aria-label=\"Direct link to App-owned state with stream subscriptions\" title=\"Direct link to App-owned state with stream subscriptions\" translate=\"no\">​</a></h2>\n<p>The kitchen demo has a property worth naming: the <code>orders</code> table is the only source of truth. There's no duplicate state table and no broker-managed snapshot — Centrifugo streams the change events, and the stream broker keeps just a thin bridging window in <code>cf_stream</code> (the partition retention window) while the app database owns everything historical.</p>\n<p>The client side of that pattern — loading the current orders and staying in sync across reconnects — is handled by the SDK's <code>getState</code> callback for stream subscriptions, and it is a topic of its own. <a class=\"\" href=\"https://centrifugal.dev/blog/2026/07/27/app-owned-state-stream-subscriptions\">App-owned state with stream subscriptions</a> walks through it, including the read path of this kitchen demo and a second shape where an aggregator fronts a Kafka feed.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"performance\">Performance<a href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits#performance\" class=\"hash-link\" aria-label=\"Direct link to Performance\" title=\"Direct link to Performance\" translate=\"no\">​</a></h2>\n<p>On a local PostgreSQL 16 (Homebrew, Apple M4):</p>\n<table><thead><tr><th>Operation</th><th>Result</th></tr></thead><tbody><tr><td><strong>Publish</strong></td><td>~17,000 ops/sec</td></tr><tr><td><strong>Publish → delivery latency</strong></td><td>~2 ms</td></tr><tr><td><strong>Partition drop</strong> (10K rows)</td><td>~1 ms</td></tr></tbody></table>\n<p>These numbers are from a single Centrifugo instance running the broker's Go integration tests in benchmark mode against the same machine's PostgreSQL — small JSON payloads, default broker configuration, parallel goroutines exercising <code>cf_stream_publish</code>. They're rough estimates, not numbers you can take to production: real workloads vary with payload size, connection pool, network latency, and your PostgreSQL's own write capacity. In production, multiple Centrifugo nodes and application instances call <code>cf_stream_publish</code> concurrently — aggregate throughput scales with the number of writers up to PostgreSQL's own write capacity. For notification, audit-log, and order-update workloads this is plenty of headroom. For ultra-high-volume telemetry that doesn't need transactional publishing, the Redis broker remains the right choice.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"getting-started\">Getting started<a href=\"https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits#getting-started\" class=\"hash-link\" aria-label=\"Direct link to Getting started\" title=\"Direct link to Getting started\" translate=\"no\">​</a></h2>\n<p>Configure the PostgreSQL stream broker as your Centrifugo broker:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"broker\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"enabled\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"postgres\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"postgres\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"dsn\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"postgres://user:pass@localhost:5432/app?sslmode=disable\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"use_notify\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"partition_retention_days\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">7</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>The broker automatically creates the required tables and SQL functions on startup. Call <code>cf_stream_publish</code> from your application's SQL transactions to publish atomically.</p>\n<p>Read the full <a class=\"\" href=\"https://centrifugal.dev/docs/server/engines#postgresql-broker\">stream broker documentation</a> for configuration reference, and see the <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2\">map subscriptions Part 2</a> post for the outbox architecture deep-dive that both brokers share.</p>",
            "url": "https://centrifugal.dev/blog/2026/05/24/pg-stream-broker-benefits",
            "title": "Transactional publishing for stream subscriptions with PostgreSQL",
            "summary": "The PostgreSQL stream broker brings transactional publishing to Centrifugo's stream subscriptions. Real-time updates commit alongside the database write that triggers them — same SQL transaction, no application-side outbox, no CDC pipeline, no separate publish API call.",
            "date_modified": "2026-05-24T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "postgresql",
                "streams",
                "outbox"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2",
            "content_html": "<p>In the previous blog post we introduced Map Subscriptions. We mentioned that Centrifugo has PostgreSQL Map Broker, in this post we are providing more details about it. The PostgreSQL map broker allows publishing to a Centrifugo Map within an application SQL transaction:</p>\n<div class=\"language-sql codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-sql codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">BEGIN</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">-- Update application data</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">UPDATE</span><span class=\"token plain\"> board_items </span><span class=\"token keyword\" style=\"font-style:italic\">SET</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">data</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'{\"text\": \"Updated card\"}'</span><span class=\"token plain\">::jsonb</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">WHERE</span><span class=\"token plain\"> board_id </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">123</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">AND</span><span class=\"token plain\"> item_id </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'card_42'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">-- Publish to real-time channel (same transaction)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">SELECT</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">FROM</span><span class=\"token plain\"> cf_map_publish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    p_channel :</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'boards:123'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    p_key     :</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'card_42'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    p_data    :</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'{\"text\": \"Updated card\"}'</span><span class=\"token plain\">::jsonb</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">COMMIT</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>If the transaction rolls back, the real-time update never happened. No outbox table to maintain, no CDC pipeline, no eventual consistency — just one PostgreSQL transaction. The rest of this post unpacks how it works.</p>\n<p><a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions\">Part 1</a> introduced map subscriptions and the memory and Redis brokers. This post focuses on the PostgreSQL broker, new in Centrifugo v6.8.0.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"how-the-sql-functions-work\">How the SQL functions work<a href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2#how-the-sql-functions-work\" class=\"hash-link\" aria-label=\"Direct link to How the SQL functions work\" title=\"Direct link to How the SQL functions work\" translate=\"no\">​</a></h2>\n<p>Centrifugo creates SQL functions (<code>cf_map_publish</code>, <code>cf_map_remove</code>) that your application calls inside its own database transactions. The map state update and any other writes you do alongside commit or rollback together — atomically (as shown in the opening example).</p>\n<p>This guarantee applies to <strong>callers using the SQL function path</strong> — your backend code calling <code>cf_map_publish</code> directly inside its own SQL transaction. Publishes that go through Centrifugo's HTTP/GRPC API are still a separate operation from your DB writes (the historic dual-write shape). The SQL function path is what makes them one transaction; it's an additional integration option, not a change to the existing publish APIs.</p>\n<svg viewBox=\"0 0 700 150\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b\"><defs><marker id=\"pgtx-arrow\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#fe5e5e\"></polygon></marker><marker id=\"pgtx-arrow-blue\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#5b8def\"></polygon></marker><linearGradient id=\"pgtx-grad-app\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#1e2430\"></stop><stop offset=\"100%\" stop-color=\"#161a22\"></stop></linearGradient><linearGradient id=\"pgtx-grad-pg\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#1e2a20\"></stop><stop offset=\"100%\" stop-color=\"#161e16\"></stop></linearGradient><linearGradient id=\"pgtx-grad-centrifugo\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#2d2020\"></stop><stop offset=\"100%\" stop-color=\"#1f1515\"></stop></linearGradient><linearGradient id=\"pgtx-grad-table\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#222830\"></stop><stop offset=\"100%\" stop-color=\"#1a1e24\"></stop></linearGradient><filter id=\"pgtx-glow-blue\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#5b8def\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"pgtx-glow-green\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#5bef7b\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"pgtx-glow-red\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"4\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#fe5e5e\" flood-opacity=\"0.15\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></defs><g transform=\"translate(20, 15)\" filter=\"url(#pgtx-glow-blue)\"><rect width=\"185\" height=\"120\" rx=\"10\" fill=\"url(#pgtx-grad-app)\" stroke=\"#5b8def\" stroke-width=\"1.5\"></rect><text x=\"92\" y=\"22\" font-size=\"10\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">YOUR TRANSACTION</text><rect x=\"10\" y=\"33\" width=\"165\" height=\"78\" rx=\"6\" fill=\"none\" stroke=\"#5b8def\" stroke-width=\"1\" stroke-dasharray=\"4,3\"></rect><text x=\"92\" y=\"50\" font-size=\"10\" font-weight=\"600\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">BEGIN</text><rect x=\"20\" y=\"56\" width=\"145\" height=\"22\" rx=\"4\" fill=\"url(#pgtx-grad-table)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"92\" y=\"71\" font-size=\"9\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">your business logic</text><rect x=\"20\" y=\"82\" width=\"145\" height=\"22\" rx=\"4\" fill=\"url(#pgtx-grad-table)\" stroke=\"#5b8def\" stroke-width=\"1\"></rect><text x=\"92\" y=\"97\" font-size=\"9\" font-weight=\"600\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">cf_map_publish(...)</text></g><line x1=\"205\" y1=\"75\" x2=\"237\" y2=\"75\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#pgtx-arrow-blue)\"></line><g transform=\"translate(243, 15)\" filter=\"url(#pgtx-glow-green)\"><rect width=\"160\" height=\"120\" rx=\"10\" fill=\"url(#pgtx-grad-pg)\" stroke=\"#5bef7b\" stroke-width=\"1.5\"></rect><text x=\"80\" y=\"22\" font-size=\"10\" font-weight=\"bold\" fill=\"#5bef7b\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">POSTGRESQL</text><rect x=\"12\" y=\"36\" width=\"136\" height=\"28\" rx=\"4\" fill=\"url(#pgtx-grad-table)\" stroke=\"#5bef7b\" stroke-width=\"1\"></rect><text x=\"80\" y=\"54\" font-size=\"9\" font-weight=\"600\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">state + outbox</text><text x=\"80\" y=\"85\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Rolls back?</text><text x=\"80\" y=\"100\" font-size=\"9\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Nothing gets sent.</text></g><line x1=\"403\" y1=\"75\" x2=\"435\" y2=\"75\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#pgtx-arrow)\"></line><g transform=\"translate(441, 15)\" filter=\"url(#pgtx-glow-red)\"><rect width=\"120\" height=\"120\" rx=\"10\" fill=\"url(#pgtx-grad-centrifugo)\" stroke=\"#fe5e5e\" stroke-width=\"1.5\"></rect><text x=\"60\" y=\"22\" font-size=\"10\" font-weight=\"bold\" fill=\"#fe5e5e\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">CENTRIFUGO</text><text x=\"60\" y=\"58\" font-size=\"9\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Picks up changes</text><text x=\"60\" y=\"73\" font-size=\"9\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">from outbox</text><text x=\"60\" y=\"100\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">WebSocket push</text></g><line x1=\"561\" y1=\"75\" x2=\"593\" y2=\"75\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#pgtx-arrow)\"></line><g transform=\"translate(599, 15)\"><rect width=\"80\" height=\"120\" rx=\"10\" fill=\"url(#pgtx-grad-table)\" stroke=\"#888\" stroke-width=\"1\"></rect><text x=\"40\" y=\"22\" font-size=\"10\" font-weight=\"bold\" fill=\"#ccc\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">CLIENTS</text><g transform=\"translate(40, 55)\"><rect x=\"-30\" y=\"-12\" width=\"60\" height=\"22\" rx=\"4\" fill=\"url(#pgtx-grad-app)\" stroke=\"#5b8def\" stroke-width=\"1\"></rect><text x=\"0\" y=\"3\" font-size=\"8\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">sync</text></g><g transform=\"translate(40, 85)\"><rect x=\"-30\" y=\"-12\" width=\"60\" height=\"22\" rx=\"4\" fill=\"url(#pgtx-grad-centrifugo)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"0\" y=\"3\" font-size=\"8\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">update</text></g></g></svg>\n<p>The following demo shows a polls feature where each vote is a PostgreSQL transaction that atomically updates the result and publishes to Centrifugo (one of <a href=\"https://github.com/centrifugal/examples/tree/master/v6/map_demo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">many examples</a> in the map demo collection):</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_polls.mp4\"></video>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"why-outbox-not-wal\">Why outbox, not WAL<a href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2#why-outbox-not-wal\" class=\"hash-link\" aria-label=\"Direct link to Why outbox, not WAL\" title=\"Direct link to Why outbox, not WAL\" translate=\"no\">​</a></h2>\n<p>A common approach to keeping a database and an external system in sync is CDC — Change Data Capture from the PostgreSQL write-ahead log. Supabase Realtime uses this model: it reads committed changes from the WAL and pushes them to clients. This approach requires either external tooling (Debezium, Kafka Connect) or specialized infrastructure that understands the WAL format. It also means CDC sees changes only after they happen — it can't be part of the write itself.</p>\n<p>We initially built a WAL-based version, but removed it. The reason: the main bottleneck in this architecture is PostgreSQL's write throughput — how fast your application can commit transactions. Reading committed changes from an outbox table is cheap by comparison, and PostgreSQL's <code>LISTEN/NOTIFY</code> keeps delivery latency low — typically under a few milliseconds. WAL-based CDC solves a read problem we don't actually have here, while adding real complexity — logical replication slots, WAL parsing, schema coupling. It also requires <code>wal_level = logical</code> — a setting not every PostgreSQL deployment has enabled, and one that some managed providers restrict or charge extra for. The outbox pattern keeps everything in regular SQL tables — no external dependencies, no additional infrastructure between PostgreSQL and Centrifugo.</p>\n<p>The outbox pattern also gives us something WAL-based CDC can't: the ability to write both the real-time state (<code>cf_map_state</code>) and the change stream (<code>cf_map_stream</code>) atomically within the same SQL function call. With CDC, the system reacts to what was written. With the outbox, Centrifugo's SQL functions control what gets written — including conditional logic like <code>if_new</code>, <code>if_exists</code>, and compare-and-swap — all inside the transaction.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"under-the-hood\">Under the hood<a href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2#under-the-hood\" class=\"hash-link\" aria-label=\"Direct link to Under the hood\" title=\"Direct link to Under the hood\" translate=\"no\">​</a></h2>\n<p>When your transaction calls <code>cf_map_publish</code>, the function does three things in a single atomic operation:</p>\n<ol>\n<li class=\"\"><strong>Upserts the entry</strong> in <code>cf_map_state</code> — the current key-value snapshot</li>\n<li class=\"\"><strong>Appends a change entry</strong> to <code>cf_map_stream</code> — the ordered change log that outbox workers read</li>\n<li class=\"\"><strong>Updates the channel position</strong> in <code>cf_map_meta</code> — the offset and epoch that track where the stream is</li>\n</ol>\n<p>Ordering is the tricky part: the outbox worker advances a simple cursor over the stream table's auto-increment id, so it must never see a higher id committed while a lower one is still in flight — that would be a gap it can't safely skip. The function handles this with a per-shard serialization lock: channels are hashed across shards (<code>num_shards</code>, default 8), and every publish first locks its shard's row and holds it until commit. Within a shard, ids are therefore assigned and committed in the same order, while different shards proceed in parallel. Without this lock, two concurrent transactions — even publishing to different channels — could insert rows with ids 5 and 6 but commit in reverse order: the worker would see id 6 appear while id 5 is still uncommitted. The channel's meta row is locked next (lock order: shard → meta → state) to assign the per-channel offset and protect against concurrent meta cleanup. The cost of this design is that writers within a shard briefly queue behind each other — the price of a gap-free cursor — and <code>num_shards</code> tunes that parallelism.</p>\n<svg viewBox=\"0 0 820 310\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b\"><defs><marker id=\"pgout-arrow\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#fe5e5e\"></polygon></marker><marker id=\"pgout-arrow-blue\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#5b8def\"></polygon></marker><marker id=\"pgout-arrow-muted\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#888\"></polygon></marker><linearGradient id=\"pgout-grad-app\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#1e2430\"></stop><stop offset=\"100%\" stop-color=\"#161a22\"></stop></linearGradient><linearGradient id=\"pgout-grad-pg\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#1e2a20\"></stop><stop offset=\"100%\" stop-color=\"#161e16\"></stop></linearGradient><linearGradient id=\"pgout-grad-centrifugo\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#2d2020\"></stop><stop offset=\"100%\" stop-color=\"#1f1515\"></stop></linearGradient><linearGradient id=\"pgout-grad-table\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#222830\"></stop><stop offset=\"100%\" stop-color=\"#1a1e24\"></stop></linearGradient><filter id=\"pgout-glow-blue\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#5b8def\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"pgout-glow-green\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#5bef7b\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"pgout-glow-red\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"4\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#fe5e5e\" flood-opacity=\"0.15\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></defs><g transform=\"translate(20, 20)\" filter=\"url(#pgout-glow-blue)\"><rect width=\"190\" height=\"270\" rx=\"10\" fill=\"url(#pgout-grad-app)\" stroke=\"#5b8def\" stroke-width=\"1.5\"></rect><text x=\"95\" y=\"24\" font-size=\"11\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">YOUR APPLICATION</text><rect x=\"12\" y=\"40\" width=\"166\" height=\"130\" rx=\"6\" fill=\"none\" stroke=\"#5b8def\" stroke-width=\"1\" stroke-dasharray=\"4,3\"></rect><text x=\"95\" y=\"58\" font-size=\"10\" font-weight=\"600\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">BEGIN</text><rect x=\"22\" y=\"68\" width=\"146\" height=\"30\" rx=\"4\" fill=\"url(#pgout-grad-table)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"95\" y=\"87\" font-size=\"9\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Business logic / SQL</text><rect x=\"22\" y=\"106\" width=\"146\" height=\"30\" rx=\"4\" fill=\"url(#pgout-grad-table)\" stroke=\"#5b8def\" stroke-width=\"1\"></rect><text x=\"95\" y=\"125\" font-size=\"9\" font-weight=\"600\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">cf_map_publish(...)</text><text x=\"95\" y=\"160\" font-size=\"10\" font-weight=\"600\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"monospace\">COMMIT</text><text x=\"95\" y=\"195\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Both your data and the</text><text x=\"95\" y=\"208\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">real-time state update</text><text x=\"95\" y=\"221\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">commit or rollback</text><text x=\"95\" y=\"234\" font-size=\"9\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">together atomically.</text><text x=\"95\" y=\"260\" font-size=\"8\" fill=\"#666\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Also: server API or client SDK</text></g><line x1=\"210\" y1=\"120\" x2=\"234\" y2=\"120\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#pgout-arrow-blue)\"></line><g transform=\"translate(240, 20)\" filter=\"url(#pgout-glow-green)\"><rect width=\"240\" height=\"270\" rx=\"10\" fill=\"url(#pgout-grad-pg)\" stroke=\"#5bef7b\" stroke-width=\"1.5\"></rect><text x=\"120\" y=\"24\" font-size=\"11\" font-weight=\"bold\" fill=\"#5bef7b\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">POSTGRESQL</text><rect x=\"15\" y=\"42\" width=\"210\" height=\"42\" rx=\"5\" fill=\"url(#pgout-grad-table)\" stroke=\"#5bef7b\" stroke-width=\"1\"></rect><text x=\"120\" y=\"60\" font-size=\"10\" font-weight=\"600\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">cf_map_state</text><text x=\"120\" y=\"74\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Current key-value snapshot</text><rect x=\"15\" y=\"94\" width=\"210\" height=\"52\" rx=\"5\" fill=\"url(#pgout-grad-table)\" stroke=\"#fe5e5e\" stroke-width=\"1.5\"></rect><text x=\"120\" y=\"114\" font-size=\"10\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"monospace\">cf_map_stream</text><text x=\"120\" y=\"128\" font-size=\"8\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Change log (outbox)</text><text x=\"120\" y=\"140\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Sharded for parallelism</text><rect x=\"15\" y=\"156\" width=\"210\" height=\"42\" rx=\"5\" fill=\"url(#pgout-grad-table)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"120\" y=\"174\" font-size=\"10\" font-weight=\"600\" fill=\"#aaa\" text-anchor=\"middle\" font-family=\"monospace\">cf_map_meta</text><text x=\"120\" y=\"188\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Epoch + offset tracking</text><rect x=\"15\" y=\"208\" width=\"210\" height=\"30\" rx=\"5\" fill=\"url(#pgout-grad-table)\" stroke=\"#e8a838\" stroke-width=\"1\"></rect><text x=\"120\" y=\"228\" font-size=\"10\" font-weight=\"600\" fill=\"#e8a838\" text-anchor=\"middle\" font-family=\"monospace\">pg_notify()</text><text x=\"120\" y=\"258\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">All updated atomically in one transaction</text></g><g><line x1=\"480\" y1=\"120\" x2=\"504\" y2=\"120\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#pgout-arrow)\"></line><text x=\"492\" y=\"112\" font-size=\"7\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">poll</text></g><g><line x1=\"480\" y1=\"223\" x2=\"504\" y2=\"185\" stroke=\"#e8a838\" stroke-width=\"1\" stroke-dasharray=\"4,3\" marker-end=\"url(#pgout-arrow-muted)\"></line></g><g transform=\"translate(510, 20)\" filter=\"url(#pgout-glow-red)\"><rect width=\"145\" height=\"270\" rx=\"10\" fill=\"url(#pgout-grad-centrifugo)\" stroke=\"#fe5e5e\" stroke-width=\"1.5\"></rect><text x=\"72\" y=\"24\" font-size=\"11\" font-weight=\"bold\" fill=\"#fe5e5e\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">CENTRIFUGO</text><rect x=\"12\" y=\"42\" width=\"121\" height=\"55\" rx=\"5\" fill=\"url(#pgout-grad-table)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"72\" y=\"62\" font-size=\"9\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Outbox Workers</text><text x=\"72\" y=\"76\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">One per shard</text><text x=\"72\" y=\"88\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Cursor-based polling</text><rect x=\"12\" y=\"108\" width=\"121\" height=\"44\" rx=\"5\" fill=\"url(#pgout-grad-table)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"72\" y=\"128\" font-size=\"9\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">PUB/SUB</text><text x=\"72\" y=\"143\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">WebSocket delivery</text><rect x=\"12\" y=\"163\" width=\"121\" height=\"44\" rx=\"5\" fill=\"url(#pgout-grad-table)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"72\" y=\"183\" font-size=\"9\" font-weight=\"600\" fill=\"#aaa\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">State Reads</text><text x=\"72\" y=\"198\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">ReadState / ReadStream</text><rect x=\"12\" y=\"218\" width=\"121\" height=\"40\" rx=\"5\" fill=\"url(#pgout-grad-table)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"72\" y=\"238\" font-size=\"9\" font-weight=\"600\" fill=\"#aaa\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">TTL / Cleanup</text><text x=\"72\" y=\"250\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Expires keys, trims stream</text></g><line x1=\"655\" y1=\"130\" x2=\"679\" y2=\"130\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#pgout-arrow)\"></line><g transform=\"translate(685, 20)\"><rect width=\"115\" height=\"270\" rx=\"10\" fill=\"url(#pgout-grad-table)\" stroke=\"#888\" stroke-width=\"1\"></rect><text x=\"57\" y=\"24\" font-size=\"11\" font-weight=\"bold\" fill=\"#ccc\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">CLIENTS</text><g transform=\"translate(57, 70)\"><rect x=\"-40\" y=\"-15\" width=\"80\" height=\"30\" rx=\"4\" fill=\"url(#pgout-grad-app)\" stroke=\"#5b8def\" stroke-width=\"1\"></rect><text x=\"0\" y=\"4\" font-size=\"9\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">sync event</text></g><g transform=\"translate(57, 115)\"><rect x=\"-40\" y=\"-15\" width=\"80\" height=\"30\" rx=\"4\" fill=\"url(#pgout-grad-centrifugo)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"0\" y=\"4\" font-size=\"9\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">update event</text></g><text x=\"57\" y=\"170\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">SDK manages</text><text x=\"57\" y=\"182\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">state, stream,</text><text x=\"57\" y=\"194\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">and live phases</text><text x=\"57\" y=\"206\" font-size=\"8\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">transparently.</text></g></svg>\n<p>Centrifugo runs a pool of outbox workers — one per shard (<code>num_shards</code>, default 8). Each channel is assigned to a shard by hash, and each worker independently polls its portion of the stream table using an in-memory cursor that tracks the last delivered stream id. Workers are stateless — on restart the cursor starts from the current tail of the stream, and no position is persisted. Delivery guarantees don't depend on the workers at all: every publication carries a per-channel offset and epoch, so a client that missed messages — whether due to its own reconnect or a Centrifugo node restart — recovers them from the stream table on resubscribe, and fresh subscribers load the current state from the snapshot table.</p>\n<p>By default, workers poll every 100ms. Enabling <code>use_notify</code> triggers PostgreSQL's <code>LISTEN/NOTIFY</code> when new entries are committed, waking the worker immediately — reducing delivery latency to low single-digit milliseconds. Every Centrifugo node runs its own set of workers, so delivery continues even if a node goes down.</p>\n<p>In effect, the broker-owned collection lives durably in PostgreSQL with the same operational story as the rest of your data — backups, monitoring, <code>psql</code> access — and clients see it live over WebSocket. No additional message broker, no new data pipeline.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"partitioning-and-retention\">Partitioning and retention<a href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2#partitioning-and-retention\" class=\"hash-link\" aria-label=\"Direct link to Partitioning and retention\" title=\"Direct link to Partitioning and retention\" translate=\"no\">​</a></h2>\n<p>The stream table is automatically partitioned by day. Old partitions are dropped entirely — instant, no row-by-row deletion, no expensive vacuum operations. This is built into the open-source broker via <code>partition_retention_days</code> (default 7) and <code>partition_lookahead_days</code> (default 2). No manual maintenance needed — the broker pre-creates future partitions and drops old ones on a regular interval.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"performance\">Performance<a href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2#performance\" class=\"hash-link\" aria-label=\"Direct link to Performance\" title=\"Direct link to Performance\" translate=\"no\">​</a></h2>\n<p>On PostgreSQL 16 (Apple M4, native install — not Docker):</p>\n<table><thead><tr><th>Operation</th><th>Result</th></tr></thead><tbody><tr><td><strong>Map publish</strong></td><td>~16,000 ops/sec</td></tr><tr><td><strong>Map publish with CAS</strong></td><td>~11,000 ops/sec</td></tr><tr><td><strong>Idempotent publish</strong></td><td>~17,500 ops/sec</td></tr><tr><td><strong>Read state (full)</strong></td><td>~10,800 ops/sec</td></tr><tr><td><strong>Read state (paginated)</strong></td><td>~50,000 ops/sec</td></tr><tr><td><strong>Remove</strong></td><td>~42,000 ops/sec</td></tr><tr><td><strong>Publish → delivery latency</strong></td><td>~1.3 ms</td></tr></tbody></table>\n<p>All publish operations go through a single SQL function call (<code>cf_map_publish</code>) that atomically updates the state table, appends to the stream, and increments the channel position. ~16,000 publishes per second per broker is enough for collaborative state workloads — boards, inventories, presence.</p>\n<p>These numbers come from the broker's Go integration tests in benchmark mode against a same-machine PostgreSQL — small JSON payloads, default broker configuration, parallel goroutines exercising the SQL functions. They're rough estimates, not numbers you can take to production: real workloads vary with payload size, connection pool sizing, network latency, and your PostgreSQL's own write capacity. For context, the Redis map broker on the same hardware would be faster per-operation, but doesn't offer transactional publishing. The numbers above reflect a single Centrifugo instance with parallel goroutines. In production, multiple Centrifugo nodes and application instances publish concurrently — aggregate throughput scales with the number of writers up to PostgreSQL's own write capacity.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"whats-next\">What's next<a href=\"https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Transactional publishing is currently experimental — we may adjust the SQL function API and outbox architecture based on feedback. We've published several PostgreSQL-backed demos in the <a href=\"https://github.com/centrifugal/examples/tree/master/v6/map_demo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">map demo collection</a>, including a sprint board that demonstrates transactional publishing with Docker Compose.</p>\n<p><a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions\">Part 1</a> covers the full map subscriptions design — sync protocol, modes, and broker overview. And check out the companion post on <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions\">shared poll subscriptions</a> — the other new subscription type we're introducing alongside map subscriptions.</p>\n<p>Read the full <a class=\"\" href=\"https://centrifugal.dev/docs/server/map_subscriptions\">map subscriptions documentation</a> for configuration reference, PostgreSQL broker setup, and transactional publishing examples.</p>",
            "url": "https://centrifugal.dev/blog/2026/05/23/map-subscriptions-part-2",
            "title": "Map subscriptions (Part 2) — when your PostgreSQL transaction is your real-time publish",
            "summary": "The PostgreSQL map broker brings durable persistence to Centrifugo-owned key-value collections and lets your application publish updates atomically inside its own database transactions — no dual-write, no application-side outbox to maintain.",
            "date_modified": "2026-05-23T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "websocket",
                "state-sync",
                "postgresql"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2026/05/22/map-subscriptions",
            "content_html": "<p>A <strong>map subscription</strong> — new in Centrifugo v6.8.0 — is a real-time key-value collection that Centrifugo manages. The map broker stores the entries; the SDK keeps a live/realtime mirror on every subscribed client.</p>\n<p>Subscribe – and you get the current snapshot, then live updates as keys updated/removed, then automatic re-sync on reconnect. No separate REST endpoint to load initial state, no race window between HTTP and WebSocket. Convergence is part of the protocol — no <code>recovered: false</code> flag to handle in app code.</p>\n<p>Centrifugo's original model is <strong>stream subscriptions</strong> — channel-based pub/sub with optional <a class=\"\" href=\"https://centrifugal.dev/docs/server/history_and_recovery\">history and recovery</a> so reconnecting clients catch up on missed publications. They work well for chat, notifications, activity feeds, and anywhere clients need an ordered sequence of events, and they remain the right choice for most real-time features.</p>\n<p>For the broad case of \"I have data in my own database and want clients to see it live\", a stream subscription with a <code>getState</code> callback reading from your own tables is usually the natural fit — your schema stays the source of truth. Map subscriptions fit a different shape: collections without an obvious home in the application's database — cursors that exist for a few seconds at a time, presence sets, IoT device state, lobby members, feature flags, live dashboards. Each is conceptually just a key-value collection that should be live in the browser. Building a store, a change feed, and a snapshot endpoint for each one is a lot of infrastructure for that. Map subscriptions are that primitive, baked into Centrifugo.</p>\n<p>Centrifugo also offers a new presence implementation built on map subscriptions, with all the same benefits — automatic synchronization and a clean SDK API.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"where-state-sync-gets-tricky\">Where state sync gets tricky<a href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions#where-state-sync-gets-tricky\" class=\"hash-link\" aria-label=\"Direct link to Where state sync gets tricky\" title=\"Direct link to Where state sync gets tricky\" translate=\"no\">​</a></h2>\n<p>Consider two examples. First, shared cursors: each user publishes their cursor position, and every other user sees it move in real time. You can publish coordinates through a stream subscription, and that works — but after a page reload, the new client has no way to get the current position of all cursors. It must wait for each user to move again. Presence gives you the list of who's in the channel, but not their associated state.</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_cursors.mp4\"></video>\n<p>Second, a live scoreboard. The client doesn't care about the history of score changes — it needs the current state of all matches, and it needs updates as they happen. You can absolutely build this with stream subscriptions — we did exactly that in our <a class=\"\" href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard\">real-time leaderboard tutorial</a> using Redis, cache recovery, and delta compression. It works well. The challenge is the initial load: there's a gap between the REST response and the moment the subscription starts, and updates published during that gap can be missed.</p>\n<p>We've <a class=\"\" href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync\">explored this problem before</a> and even provided a <code>RealTimeDocument</code> helper class that manages versioning, re-fetches state on gaps, and reconciles late-arriving updates. It works, but we kept seeing teams building collaborative features, live dashboards, or presence systems implementing similar logic on top. We wanted a more convenient SDK API for this pattern — one where state delivery and recovery are built in, so application code doesn't have to manage them.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"a-new-subscription-type-for-keyed-state\">A new subscription type for keyed state<a href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions#a-new-subscription-type-for-keyed-state\" class=\"hash-link\" aria-label=\"Direct link to A new subscription type for keyed state\" title=\"Direct link to A new subscription type for keyed state\" translate=\"no\">​</a></h2>\n<p>For use cases like scoreboards, presence, or collaborative state, what matters is the current collection of entries — not the ordered sequence of events that stream subscriptions provide. Map subscriptions are a new subscription type for this: instead of an append-only publication stream, a map channel maintains a <strong>synchronized collection</strong> — a set of key-value entries where each entry can be independently published, updated, or removed. When a client subscribes, it receives the full state through a paginated protocol, then transitions to live updates. The SDK handles all of this transparently — the application just reacts to <code>sync</code> (full state ready) and <code>update</code> (single entry changed) events.</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newMapSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'dashboard:main'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'sync'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">renderDashboard</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">entries</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'update'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">removed</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">?</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">removeEntry</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">upsertEntry</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>The subscription itself delivers the state — there's no separate REST call, no gap to bridge. Compare this with stream subscriptions, where the application receives individual <code>publication</code> events and must build state from the event sequence. With map subscriptions, the SDK maintains the collection internally — the application just renders what it's given. After any disconnect, clients always converge to the correct state: the SDK either catches up from the stream or re-syncs from scratch, then emits a fresh <code>sync</code> event. There's no <code>recovered: false</code> flag to handle — convergence is guaranteed by the protocol.</p>\n<p>Map subscriptions are configured through the same <a class=\"\" href=\"https://centrifugal.dev/docs/server/channels\">channel namespace</a> system as stream subscriptions — you set <code>subscription_type: \"map\"</code> on a namespace, configure the mode and options, and channels in that namespace become map channels. The same connection, the same client SDK, the same authentication and authorization flow. Existing stream namespaces are unaffected. The client SDK provides a dedicated <code>MapSubscription</code> type with narrowed methods and events — <code>sync</code>, <code>update</code>, <code>mapPublish</code>, <code>mapRemove</code> — rather than overloading the existing <code>Subscription</code> with mode flags. The same approach applies to presence variants (<code>MapClientsSubscription</code>, <code>MapUsersSubscription</code>).</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"three-phase-sync-protocol\">Three-phase sync protocol<a href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions#three-phase-sync-protocol\" class=\"hash-link\" aria-label=\"Direct link to Three-phase sync protocol\" title=\"Direct link to Three-phase sync protocol\" translate=\"no\">​</a></h2>\n<p>Underneath the simple-looking API there's a real protocol challenge: the client must paginate through potentially large state while new updates keep arriving. This is the same race condition that <code>RealTimeDocument</code> addressed — but now solved at the protocol level, so application developers don't have to think about it.</p>\n<p>The subscription goes through three phases:</p>\n<ol>\n<li class=\"\"><strong>State phase</strong> — the client paginates through the current key-value snapshot from the broker</li>\n<li class=\"\"><strong>Stream phase</strong> — the client catches up on changes that occurred during state pagination</li>\n<li class=\"\"><strong>Live phase</strong> — the client receives real-time updates via PUB/SUB</li>\n</ol>\n<p>During the transition from stream to live, the server buffers incoming publications, merges them with recovered stream entries, and checks for continuity — reusing the same buffering and merge infrastructure that powers stream recovery in regular channels. If continuity is broken, the client re-syncs from scratch automatically. The SDK handles all three phases internally — the application never sees pagination cursors or stream offsets.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"three-modes-for-different-lifetimes\">Three modes for different lifetimes<a href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions#three-modes-for-different-lifetimes\" class=\"hash-link\" aria-label=\"Direct link to Three modes for different lifetimes\" title=\"Direct link to Three modes for different lifetimes\" translate=\"no\">​</a></h2>\n<p>Not all state has the same lifecycle. Cursor positions should disappear seconds after a user disconnects. Session data should survive brief network interruptions but not persist forever. Inventory entries should persist until explicitly removed.</p>\n<p>Stream subscriptions offer fine-grained control over these concerns — <code>history_size</code>, <code>history_ttl</code>, <code>force_recovery</code> can be configured independently. Map subscriptions take a different approach, bundling them into three modes that cover the most common state sync patterns:</p>\n<ul>\n<li class=\"\">\n<p><strong>Ephemeral</strong> — no stream history, entries expire via TTL. On reconnect, the client gets a full state snapshot. This is the most lightweight option — about 35-40% less work per publish compared to modes with a stream. Best for cursors, typing indicators.</p>\n</li>\n<li class=\"\">\n<p><strong>Recoverable</strong> — stream history with TTL-expiring entries. On reconnect, the client catches up from the stream instead of re-fetching everything. Best for presence, sessions, polls, game lobbies — data that auto-expires but needs efficient recovery.</p>\n</li>\n<li class=\"\">\n<p><strong>Persistent</strong> — stream history with permanent entries. Data lives until explicitly removed. Best for inventories, collaborative documents, dashboards — permanent state with efficient reconnect.</p>\n</li>\n</ul>\n<p>The mode determines which phase of the sync protocol is used on reconnect: ephemeral always re-syncs from state, recoverable and persistent attempt stream catch-up first.</p>\n<svg viewBox=\"0 0 800 610\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b\"><defs><marker id=\"map-arrow\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#fe5e5e\"></polygon></marker><marker id=\"map-arrow-muted\" markerWidth=\"10\" markerHeight=\"7\" refX=\"9\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#888\"></polygon></marker><linearGradient id=\"map-grad-phase\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#2d2020\"></stop><stop offset=\"100%\" stop-color=\"#1f1515\"></stop></linearGradient><linearGradient id=\"map-grad-mode\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#1e2430\"></stop><stop offset=\"100%\" stop-color=\"#161a22\"></stop></linearGradient><filter id=\"map-glow-red\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"4\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#fe5e5e\" flood-opacity=\"0.15\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"map-glow-blue\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#5b8def\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"map-glow-green\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#5bef7b\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><pattern id=\"map-hatch\" width=\"12\" height=\"12\" patternTransform=\"rotate(45)\" patternUnits=\"userSpaceOnUse\"><line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"12\" stroke=\"#222224\" stroke-width=\"0.5\"></line></pattern></defs><rect x=\"20\" y=\"15\" width=\"760\" height=\"580\" rx=\"12\" fill=\"none\" stroke=\"#444\" stroke-width=\"1\" stroke-dasharray=\"8,4\"></rect><rect x=\"20\" y=\"15\" width=\"760\" height=\"580\" rx=\"12\" fill=\"url(#map-hatch)\" opacity=\"0.2\"></rect><g transform=\"translate(100, 40)\" filter=\"url(#map-glow-red)\"><rect width=\"170\" height=\"80\" rx=\"8\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1.5\"></rect><text x=\"85\" y=\"30\" font-size=\"11\" font-weight=\"bold\" fill=\"#fe5e5e\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">STATE</text><text x=\"85\" y=\"50\" font-size=\"10\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Paginate full</text><text x=\"85\" y=\"65\" font-size=\"10\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">key-value state</text></g><line x1=\"270\" y1=\"80\" x2=\"310\" y2=\"80\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#map-arrow)\"></line><g transform=\"translate(315, 40)\" filter=\"url(#map-glow-red)\"><rect width=\"170\" height=\"80\" rx=\"8\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1.5\"></rect><text x=\"85\" y=\"30\" font-size=\"11\" font-weight=\"bold\" fill=\"#fe5e5e\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">STREAM</text><text x=\"85\" y=\"50\" font-size=\"10\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Catch up on changes</text><text x=\"85\" y=\"65\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">(recoverable / persistent)</text></g><line x1=\"485\" y1=\"80\" x2=\"525\" y2=\"80\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#map-arrow)\"></line><g transform=\"translate(530, 40)\" filter=\"url(#map-glow-red)\"><rect width=\"170\" height=\"80\" rx=\"8\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1.5\"></rect><text x=\"85\" y=\"30\" font-size=\"11\" font-weight=\"bold\" fill=\"#fe5e5e\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">LIVE</text><text x=\"85\" y=\"50\" font-size=\"10\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Real-time</text><text x=\"85\" y=\"65\" font-size=\"10\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">PUB/SUB updates</text></g><g transform=\"translate(400, 155)\"><rect x=\"-40\" y=\"-10\" width=\"80\" height=\"20\" rx=\"4\" fill=\"#17171b\"></rect><text x=\"0\" y=\"5\" font-size=\"10\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">MODES</text></g><g transform=\"translate(60, 175)\"><rect width=\"680\" height=\"85\" rx=\"8\" fill=\"url(#map-grad-mode)\" stroke=\"#5b8def\" stroke-width=\"1\"></rect><text x=\"340\" y=\"20\" font-size=\"12\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Ephemeral</text><text x=\"340\" y=\"36\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Entries auto-expire. No stream history. On reconnect — full state snapshot.</text><g transform=\"translate(20, 48)\"><rect x=\"0\" y=\"0\" width=\"200\" height=\"28\" rx=\"5\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"100\" y=\"18\" font-size=\"10\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">STATE</text><line x1=\"200\" y1=\"14\" x2=\"435\" y2=\"14\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#map-arrow)\"></line><rect x=\"440\" y=\"0\" width=\"200\" height=\"28\" rx=\"5\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"540\" y=\"18\" font-size=\"10\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">LIVE</text><text x=\"320\" y=\"8\" font-size=\"8\" fill=\"#666\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">skip STREAM</text></g></g><g transform=\"translate(60, 270)\"><rect width=\"680\" height=\"85\" rx=\"8\" fill=\"url(#map-grad-mode)\" stroke=\"#5b8def\" stroke-width=\"1\"></rect><text x=\"340\" y=\"20\" font-size=\"12\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Recoverable</text><text x=\"340\" y=\"36\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Entries auto-expire. Stream-based catch-up on reconnect. Falls back to snapshot if too far behind.</text><g transform=\"translate(20, 48)\"><rect x=\"0\" y=\"0\" width=\"185\" height=\"28\" rx=\"5\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"92\" y=\"18\" font-size=\"10\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">STATE</text><line x1=\"185\" y1=\"14\" x2=\"222\" y2=\"14\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#map-arrow)\"></line><rect x=\"227\" y=\"0\" width=\"185\" height=\"28\" rx=\"5\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"320\" y=\"18\" font-size=\"10\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">STREAM</text><line x1=\"412\" y1=\"14\" x2=\"449\" y2=\"14\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#map-arrow)\"></line><rect x=\"454\" y=\"0\" width=\"186\" height=\"28\" rx=\"5\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"547\" y=\"18\" font-size=\"10\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">LIVE</text></g></g><g transform=\"translate(60, 365)\"><rect width=\"680\" height=\"85\" rx=\"8\" fill=\"url(#map-grad-mode)\" stroke=\"#5b8def\" stroke-width=\"1\"></rect><text x=\"340\" y=\"20\" font-size=\"12\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Persistent</text><text x=\"340\" y=\"36\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Entries persist until removed. Stream-based catch-up on reconnect. Falls back to snapshot if too far behind.</text><g transform=\"translate(20, 48)\"><rect x=\"0\" y=\"0\" width=\"185\" height=\"28\" rx=\"5\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"92\" y=\"18\" font-size=\"10\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">STATE</text><line x1=\"185\" y1=\"14\" x2=\"222\" y2=\"14\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#map-arrow)\"></line><rect x=\"227\" y=\"0\" width=\"185\" height=\"28\" rx=\"5\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"320\" y=\"18\" font-size=\"10\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">STREAM</text><line x1=\"412\" y1=\"14\" x2=\"449\" y2=\"14\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#map-arrow)\"></line><rect x=\"454\" y=\"0\" width=\"186\" height=\"28\" rx=\"5\" fill=\"url(#map-grad-phase)\" stroke=\"#fe5e5e\" stroke-width=\"1\"></rect><text x=\"547\" y=\"18\" font-size=\"10\" font-weight=\"600\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">LIVE</text></g></g><g transform=\"translate(400, 480)\"><rect x=\"-60\" y=\"-10\" width=\"120\" height=\"20\" rx=\"4\" fill=\"#17171b\"></rect><text x=\"0\" y=\"5\" font-size=\"10\" font-weight=\"bold\" fill=\"#5bef7b\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">SDK EVENTS</text></g><g transform=\"translate(60, 500)\"><rect width=\"330\" height=\"65\" rx=\"8\" fill=\"url(#map-grad-mode)\" stroke=\"#444\" stroke-width=\"1\"></rect><g transform=\"translate(20, 18)\"><text x=\"0\" y=\"0\" font-size=\"9\" fill=\"#5bef7b\" font-family=\"system-ui, sans-serif\">◀──</text><text x=\"32\" y=\"0\" font-size=\"12\" font-weight=\"bold\" fill=\"#5bef7b\" font-family=\"system-ui, sans-serif\">sync</text><text x=\"70\" y=\"0\" font-size=\"10\" fill=\"#888\" font-family=\"system-ui, sans-serif\">— full state snapshot</text></g><g transform=\"translate(20, 44)\"><text x=\"0\" y=\"0\" font-size=\"9\" fill=\"#5bef7b\" font-family=\"system-ui, sans-serif\">◀──</text><text x=\"32\" y=\"0\" font-size=\"12\" font-weight=\"bold\" fill=\"#5bef7b\" font-family=\"system-ui, sans-serif\">update</text><text x=\"82\" y=\"0\" font-size=\"10\" fill=\"#888\" font-family=\"system-ui, sans-serif\">— incremental change</text></g></g><g transform=\"translate(410, 500)\"><rect width=\"330\" height=\"65\" rx=\"8\" fill=\"url(#map-grad-mode)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"165\" y=\"25\" font-size=\"10\" fill=\"#aaa\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">SDK handles all phases transparently.</text><text x=\"165\" y=\"42\" font-size=\"10\" fill=\"#aaa\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">App receives only <tspan fill=\"#5bef7b\" font-weight=\"600\">sync</tspan> and <tspan fill=\"#5bef7b\" font-weight=\"600\">update</tspan> events.</text></g></svg>\n<p>Here's the protocol visualizer demo — it shows all three phases in action, including paginated state delivery, stream catch-up, and the transition to live updates:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_visualizer.mp4\"></video>\n<p>Recoverable and persistent modes support Fossil delta compression — deltas are computed per key, between successive values of the same entry, so clients receive compact patches instead of full payloads when only part of the data changes.</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_scoreboard.mp4\"></video>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conditional-writes\">Conditional writes<a href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions#conditional-writes\" class=\"hash-link\" aria-label=\"Direct link to Conditional writes\" title=\"Direct link to Conditional writes\" translate=\"no\">​</a></h2>\n<p>Some patterns need writes that can fail gracefully — two players claiming the last slot in a game lobby, two bidders placing an auction bid at the same instant. Map subscriptions support two forms of conditional writes. Key modes — <code>if_new</code> (only insert if key doesn't exist) and <code>if_exists</code> (only update if key exists) — cover slot claiming and heartbeat-only updates. For stronger guarantees, compare-and-swap via <code>ExpectedPosition</code> checks the channel's stream offset and epoch before writing — if another write happened since the client's last read, the operation is rejected and the client can retry with fresh data.</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_inventory.mp4\"></video>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"scalable-presence-on-top-of-maps\">Scalable presence on top of maps<a href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions#scalable-presence-on-top-of-maps\" class=\"hash-link\" aria-label=\"Direct link to Scalable presence on top of maps\" title=\"Direct link to Scalable presence on top of maps\" translate=\"no\">​</a></h2>\n<p>The same keyed structure applies directly to presence. Centrifugo has had <a class=\"\" href=\"https://centrifugal.dev/docs/server/presence\">presence</a> since the early days — you can query who's in a channel and receive join/leave events. This works well for channels with moderate participant counts. For channels with thousands of participants, though, returning the entire list in a single response becomes expensive. And join/leave events are delivered with at-most-once guarantee — fine for live indicators, but there's no built-in way to catch up on events missed during a disconnect.</p>\n<p>Map subscriptions work well for these larger-scale scenarios. Centrifugo provides two presence subscription types:</p>\n<ul>\n<li class=\"\"><strong><code>map_clients</code></strong> — one entry per connection (key = client ID). When a client unsubscribes or disconnects, its entry is removed immediately.</li>\n<li class=\"\"><strong><code>map_users</code></strong> — one entry per user (key = user ID). A user may have multiple connections, so entries can't be removed on a single disconnect — they expire via TTL after the last connection for that user leaves the channel.</li>\n</ul>\n<p>Because these are regular map channels, clients get paginated state on subscribe and live join/leave updates in real time. Recovery works the same way — reconnecting clients catch up from the stream instead of re-fetching everything.</p>\n<p>This is configured through channel prefixes. When a client subscribes to <code>game:abc</code>, the server can automatically publish presence entries to <code>clients:game:abc</code> (per-connection) and <code>users:game:abc</code> (per-user). These are separate map channels that other clients can subscribe to independently:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Client subscribes to game:abc</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       ├──► auto-publish to clients:game:abc  (key = client_id)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       └──► auto-publish to users:game:abc    (key = user_id)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Other clients subscribe to clients:* / users:* to see who's online</span><br></div></code></pre></div></div>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"channel\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"namespaces\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"game\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"map_clients_presence_channel_prefix\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"clients:\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"map_users_presence_channel_prefix\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"users:\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"clients\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"subscription_type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"map_clients\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"map\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token property\">\"mode\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"recoverable\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token property\">\"key_ttl\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"60s\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"users\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"subscription_type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"map_users\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"map\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token property\">\"mode\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"recoverable\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token property\">\"key_ttl\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"60s\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Here's a game lobby demo that shows presence built on top of map subscriptions — the sidebar tracks connected players in real time using <code>MapClientsSubscription</code>:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_lobby.mp4\"></video>\n<p>A handful of players is the easy case. The same primitive scales much further — here are two browser tabs subscribed to the same <code>map_clients</code> channel while the population is being filled to <strong>100,000 connected members</strong>. Each cell in the grid is one connection; green flashes mark joins, red flashes mark leaves. The tabs connect <em>during</em> the fill, so they have to reconcile a moving target — paginated initial state plus the joins still happening in real time. Once the population stabilizes both tabs show the same converged 100k. The clip ends with a reconnect: state paginates back at 10k entries per page, the full grid repaints quickly, and the tab is live again:</p>\n<video width=\"100%\" controls=\"\" preload=\"metadata\" src=\"/img/demo_map_presence.mp4\"></video>\n<p>To be clear: a hundred thousand members in a single presence channel is <strong>not</strong> a typical production shape and we don't expect most apps to need it. This is a deliberately extreme demo showing what the protocol is capable of — not a recommended pattern. Most real systems work with channels of dozens to a few thousand participants, where the main advantages of map presence are convergence on reconnect and join/leave reliability, rather than raw count. (For the curious: we ran the same setup at <strong>1,000,000 members</strong> and a single browser tab still synchronizes — initial state takes around twenty seconds to paginate, then live updates flow normally. Past that point the bottleneck shifts from the protocol to the cost of streaming the snapshot itself.) Both 100k and 1M variants are runnable from the <a href=\"https://github.com/centrifugal/examples/tree/master/v6/map_presence_demo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>v6/map_presence_demo</code></a> example.</p>\n<p>The <code>recoverable</code> mode is what makes map-based presence more capable than Centrifugo's traditional presence. With recoverable mode, reconnecting clients catch up from the stream rather than re-fetching the full participant list. With ephemeral mode, clients would get a full snapshot on every reconnect — which is the same behavior traditional presence already provides, losing the convergence advantage.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"brokers-memory-redis-postgresql\">Brokers: memory, Redis, PostgreSQL<a href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions#brokers-memory-redis-postgresql\" class=\"hash-link\" aria-label=\"Direct link to Brokers: memory, Redis, PostgreSQL\" title=\"Direct link to Brokers: memory, Redis, PostgreSQL\" translate=\"no\">​</a></h2>\n<p>Map subscriptions need a backend to store state and coordinate updates. Centrifugo supports three map brokers, each suited to different deployment scenarios.</p>\n<p><strong>Memory</strong> is the default — zero dependencies, single-node, state lost on restart. Good for development and ephemeral data on single-node deployments.</p>\n<p><strong>Redis</strong> adds distribution across nodes. We've invested years into making Centrifugo's <a class=\"\" href=\"https://centrifugal.dev/docs/server/engines\">Redis engine</a> efficient — connection pooling, pipelining, client-side consistent sharding, atomic Lua scripts. The Redis map broker builds on that same foundation, reusing the existing connection infrastructure and following the same patterns. State is stored in Redis hashes, with atomic Lua scripts ensuring that state update, stream append, and PUB/SUB broadcast happen as a single operation — the same atomicity approach we use for stream subscription history. This is the typical choice for ephemeral and recoverable modes in multi-node setups.</p>\n<p><strong>PostgreSQL</strong> goes further. In a recent survey, 86% of Centrifugo users reported having PostgreSQL in their production stack, with 76% using it as their primary database. The PostgreSQL broker stores map state in regular SQL tables and enables transactional publishing — real-time updates that commit or roll back inside your database transactions, eliminating the <a href=\"https://thorben-janssen.com/dual-writes/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">dual-write problem</a> entirely.</p>\n<p>Here's a sprint board demo where cards are moved between columns — each drag-and-drop is a PostgreSQL transaction that updates the board state and publishes to Centrifugo atomically:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_taskboard.mp4\"></video>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"where-this-fits-in-the-landscape\">Where this fits in the landscape<a href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions#where-this-fits-in-the-landscape\" class=\"hash-link\" aria-label=\"Direct link to Where this fits in the landscape\" title=\"Direct link to Where this fits in the landscape\" translate=\"no\">​</a></h2>\n<p>Most real-time messaging systems are built around PUB/SUB — Pusher, Socket.IO, NATS all center on message delivery. A growing number do offer built-in state synchronization. Firebase Realtime Database and Supabase Realtime sync data to clients directly from their managed databases. Ably has expanded beyond pub/sub with LiveSync and LiveObjects for database-to-client sync and collaborative state primitives. Liveblocks provides collaborative state with CRDTs. But these are either cloud-only services tied to a specific database — where you can only integrate as deeply as the platform allows — or specialized tools for a narrow use case like collaborative editing.</p>\n<p>Why not CRDTs? Because we want to stay consistent with what Centrifugo has always been: a generic real-time transport that doesn't interpret the data it carries. Centrifugo delivers JSON or binary payloads without interpreting their contents — it doesn't parse your data, doesn't merge it, doesn't resolve conflicts at the field level. The entire protocol — including map subscriptions — works over both JSON and <a class=\"\" href=\"https://centrifugal.dev/docs/transports/overview#protobuf-protocol\">Protobuf</a>, so latency-sensitive or bandwidth-constrained applications can use compact binary encoding end to end. CRDTs require the transport layer to understand the data structure and apply merge semantics, which ties the system to specific data types. Map subscriptions work the same way as stream subscriptions: Centrifugo synchronizes opaque key-value entries — your application decides what they contain and how to interpret them. This keeps the system generic and the design consistent across all three subscription types.</p>\n<p>Centrifugo occupies a different spot because it's self-hosted — it runs in your infrastructure, connects to your databases, and calls your backend directly. That proximity opens up features a cloud service sitting between you and your users can't really offer — from transactional publishing (where the publish <em>is</em> part of your DB transaction, not an after-the-fact CDC reaction) to the low-latency proxy system that powers subscribe authorization and shared poll refresh.</p>\n<p>For scenarios that need per-item access control within a single map channel, <a class=\"\" href=\"https://centrifugal.dev/docs/pro/server_tags_filter\">Centrifugo PRO</a> adds a server-side publication tags filter — your backend assigns tags to entries and sets a filter per subscriber via the subscribe proxy or JWT. Only matching entries are delivered, across all sync phases. This enables RBAC patterns without splitting data into separate channels per access scope.</p>\n<p>Stream subscriptions, map subscriptions, and <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions\">shared poll subscriptions</a> cover three different ways clients relate to data — ordered events, synchronized collections, and polled read-only state — while staying payload-agnostic across the board. Your application decides what the data means; Centrifugo handles delivery. Switching between primitives is a namespace configuration choice, not an architectural one.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"whats-next\">What's next<a href=\"https://centrifugal.dev/blog/2026/05/22/map-subscriptions#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Map subscriptions are currently experimental — we may adjust the API, configuration, and protocol based on feedback. At this point, only <code>centrifuge-js</code> supports map subscriptions on the client side. We plan to extend support to other SDKs.</p>\n<p>We designed map subscriptions to share everything stream subscriptions already have — not a separate system grafted on. Same namespace configuration, same Redis infrastructure, same client SDK connection, proxy system, recovery internals, and <a class=\"\" href=\"https://centrifugal.dev/docs/transports/overview\">transport layer</a>. The goal is that adopting map subscriptions should feel like switching a namespace option, not adopting a different system.</p>\n<p>And check out the companion post on <a class=\"\" href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions\">shared poll subscriptions</a> — the other new subscription type we're introducing alongside map subscriptions.</p>\n<p>We've published a <a href=\"https://github.com/centrifugal/examples/tree/master/v6/map_demo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">collection of interactive demos</a> covering different map subscription features — from ephemeral cursors to PostgreSQL-backed sprint boards. Each demo runs with Docker Compose and showcases a different aspect of the feature.</p>\n<p>Read the full <a class=\"\" href=\"https://centrifugal.dev/docs/server/map_subscriptions\">map subscriptions documentation</a> for configuration reference, broker setup, and client SDK API details.</p>",
            "url": "https://centrifugal.dev/blog/2026/05/22/map-subscriptions",
            "title": "Map subscriptions (Part 1) — synchronized key-value state and presence",
            "summary": "A new subscription type in Centrifugo — real-time key-value collections with paginated state delivery, guaranteed convergence on reconnect, per-key TTL, conditional writes, and Fossil delta compression.",
            "date_modified": "2026-05-22T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "websocket",
                "state-sync",
                "presence"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2026/05/21/two-million-particles-on-centrifugo",
            "content_html": "<p>David Gerrells wrote a blog post <a href=\"https://dgerrells.com/blog/how-fast-is-go-simulating-millions-of-particles-on-a-smart-tv\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><em>How fast is Go - simulating millions of particles on a smart TV</em></a> — describing a Go server that simulates two million particles in a 2200 × 2200 world at 60 Hz, ships frames to clients at 30 Hz over WebSocket, and lets anyone connected pull particles around with their cursor. The transport is hand-written for speed: bit-packed binary frames, manual protocol, raw WebSocket library. The live demo runs at <a href=\"https://howfastisgo.dev/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">howfastisgo.dev</a> — try it before reading on.</p>\n<div class=\"vimeo-full-width\"><iframe src=\"https://www.youtube.com/embed/cGubp-wOt7Q\" title=\"YouTube video player\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen=\"\"></iframe></div>\n<p></p>\n<p>David's goal was to explore Go performance. Once we saw the demo we immediately wanted to try reproducing it on top of Centrifugo — to see whether a generic real-time transport like Centrifugo could carry this kind of payload. At first glance it looked straightforward: Centrifugo provides a binary WebSocket transport, and the simulation already runs on the server. But along the way we ran into design differences that meant we couldn't quite match the original's per-viewer bytes on the wire. We'll show what we built, what it cost, and why the overhead is worth it for UX and scalability.</p>\n<p>Source code of our final demo: <a href=\"https://github.com/centrifugal/examples/tree/master/v6/millions_of_particles\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>v6/millions_of_particles</code></a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"recap-the-original\">Recap the original<a href=\"https://centrifugal.dev/blog/2026/05/21/two-million-particles-on-centrifugo#recap-the-original\" class=\"hash-link\" aria-label=\"Direct link to Recap the original\" title=\"Direct link to Recap the original\" translate=\"no\">​</a></h2>\n<p>The \"two million particles\" lives entirely on the server. What goes to clients is a <strong>density map</strong> — one bit per world cell, answering <em>\"is there any particle in this cell?\"</em>. Several particles in the same cell collapse to one bit. Bytes per frame scale with viewport pixels, not particle count — bumping the simulation to 4M particles wouldn't change the wire size at all, the cells would just get fuller.</p>\n<p>The server runs everything in one Go process: the simulation, the WebSocket connections, and per-client camera state. On every tick it walks the connected clients, reads each one's camera <code>(x, y, width, height)</code> shipped up from the browser, crops that rectangle from the world buffer, and writes the bit-packed bytes straight to that client's WebSocket. A typical desktop window of 1410 × 730 pixels packs to 1410 × 730 / 8 ≈ <strong>129 KB per tick</strong> — that's all a viewer ever receives. The client sees about 21% of the world and pans by changing the camera; cursor input flows back up the same WebSocket and pulls particles around in the simulation.</p>\n<p>That tight coupling — sim, sockets, and per-client cameras all in the same process — is what keeps the original lean per viewer: the server cuts a custom message for each connection because everything it needs is right there. Now let's see what changes when we put a generic broker in the middle.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"how-it-fits-with-centrifugo\">How it fits with Centrifugo<a href=\"https://centrifugal.dev/blog/2026/05/21/two-million-particles-on-centrifugo#how-it-fits-with-centrifugo\" class=\"hash-link\" aria-label=\"Direct link to How it fits with Centrifugo\" title=\"Direct link to How it fits with Centrifugo\" translate=\"no\">​</a></h2>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   ┌────────────────────────────────┐</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │  Go backend (particle sim)     │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │  60 Hz sim · 30 Hz publish     │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   └────┬──────────────────────▲────┘</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        │                      │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        │ POST /api/publish    │ HTTP RPC proxy</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        │ (frame bytes)        │ (cursor input)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        ▼                      │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   ┌────────────────────────────────┐</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │          Centrifugo            │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │  single channel · WS fan-out   │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   └────┬──────────────────────▲────┘</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        │ binary WS frames     │ WS RPC (cursor)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        ▼                      │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       ┌───┐ ┌───┐ ┌───┐ ┌───┐ │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       │ B │ │ B │ │ B │ │ B │─┘   browsers</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">       └───┘ └───┘ └───┘ └───┘</span><br></div></code></pre></div></div>\n<p>The backend publishes one binary payload per tick to a single Centrifugo channel; every subscribed browser receives the same WebSocket frame and slides a local camera over the bitmap to render its slice. Cursor input flows back via RPC over WebSocket, proxied to the backend's HTTP endpoint.</p>\n<p>The publisher's job is constant: pack the whole 2200 × 2200 world at 1 bit per pixel, send to a channel — and that's it – Centrifugo handles the fan-out. Pan works locally too: each browser has the whole world in memory and just slides a camera over the bitmap.</p>\n<video width=\"100%\" controls=\"\" preload=\"metadata\" src=\"/img/demo_particles_tiny.mp4\"></video>\n<p>See in <a href=\"https://youtu.be/VA3s0iowel4\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">better quality on YouTube</a>.</p>\n<p>It works — but at roughly 5× the bytes the original sends to each viewer (~605 KB vs. ~129 KB). It's by design: Centrifugo is a standalone broker. It doesn't know about user cameras, viewports, or which slice each viewer cares about, so a single channel has to ship bytes useful for any subscriber, and the simplest \"useful\" is the whole world.</p>\n<p>And the gap widens with world size. Bump the world to 10000 × 10000 and the naive port ships ~12.5 MB per viewer per tick, while the original would still send ~129 KB — each viewer only pays for their viewport. The naive approach scales with world size; the original scales with viewport size. So we have a real reason to find a better fit.</p>\n<p>We could try one channel per viewer, with the backend tracking each camera and packing an individual crop per tick. That would get us closer to the original's ~129 KB per viewer, but it gives up fan-out — the thing Centrifugo is built for — and turns every viewport change into RPC traffic up plus a per-client publish down. The backend would also need to track camera state per connection — easy in the original's single-process design, awkward when Centrifugo sits in between.</p>\n<p>So we want fewer bytes per viewer and keep fan-out.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"splitting-the-world-into-tiles\">Splitting the world into tiles<a href=\"https://centrifugal.dev/blog/2026/05/21/two-million-particles-on-centrifugo#splitting-the-world-into-tiles\" class=\"hash-link\" aria-label=\"Direct link to Splitting the world into tiles\" title=\"Direct link to Splitting the world into tiles\" translate=\"no\">​</a></h2>\n<p>The idea: split the world into tiles, and let each viewer subscribe only to the tiles its viewport is touching. The publisher still packs the world once per tick; Centrifugo only delivers each tile to viewers tracking it.</p>\n<p>A tile is a fixed-rectangle chunk of the world. The grid is set up front — same tiles for every viewer. Every tick the publisher packs each tile in the same bit-packed format used for the whole world before (just smaller, and many of them). Viewers subscribe to the keys for the tiles their viewport currently overlaps; Centrifugo only delivers each tile to viewers tracking it. As the camera moves, the tracked set shifts to follow.</p>\n<p>Three things to settle in this design: pan behavior at tile boundaries, what tile size to pick, and how to actually deliver tiles to viewers. In that order.</p>\n<p><strong>Pan.</strong> A viewer's camera can move at any moment, and when the viewport crosses a tile boundary the newly-revealed tile wouldn't be subscribed yet — we'd see a strip of empty pixels until the next publish landed. Visible pop-in on the leading edge of the motion.</p>\n<p>The fix is a small <strong>prefetch margin</strong>: pre-track the next ring of tiles around the viewport, so freshly-revealed tiles already have data when they slide into view. Panning stays smooth at the cost of a few extra tiles always being subscribed.</p>\n<svg viewBox=\"0 0 400 320\" style=\"max-width:100%;height:auto;display:block;margin:1.5em auto;user-select:none;touch-action:none\"><defs><linearGradient id=\"tvd-world-bg\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#fafbfc\"></stop><stop offset=\"100%\" stop-color=\"#eef2f7\"></stop></linearGradient><filter id=\"tvd-vp-shadow\" x=\"-30%\" y=\"-30%\" width=\"160%\" height=\"160%\"><fedropshadow dx=\"0\" dy=\"3\" stdDeviation=\"3\" flood-color=\"#2563eb\" flood-opacity=\"0.35\"></fedropshadow></filter></defs><rect x=\"72\" y=\"20\" width=\"256\" height=\"256\" fill=\"url(#tvd-world-bg)\" stroke=\"#cbd5e1\" stroke-width=\"1\"></rect><rect x=\"104\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"104\" y=\"100\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"104\" y=\"116\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"104\" y=\"132\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"104\" y=\"148\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"104\" y=\"164\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"104\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"120\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"120\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"120\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"120\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"120\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"120\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"120\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"136\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"136\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"136\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"136\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"136\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"136\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"136\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"152\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"152\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"152\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"152\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"152\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"152\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"152\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"168\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"168\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"168\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"168\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"168\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"168\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"168\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"184\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"184\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"184\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"184\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"184\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"184\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"184\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"200\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"200\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"200\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"200\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"200\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"200\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"200\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"216\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"216\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"216\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"216\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"216\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"216\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"216\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"232\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"232\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"232\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"232\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"232\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"232\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"232\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"248\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"248\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"248\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"248\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"248\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"248\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"248\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"264\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"264\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"264\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"264\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"264\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"264\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"264\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"280\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"280\" y=\"100\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"280\" y=\"116\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"280\" y=\"132\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"280\" y=\"148\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"280\" y=\"164\" width=\"16\" height=\"16\" fill=\"#bfdbfe\"></rect><rect x=\"280\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"296\" y=\"84\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"296\" y=\"100\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"296\" y=\"116\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"296\" y=\"132\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"296\" y=\"148\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"296\" y=\"164\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><rect x=\"296\" y=\"180\" width=\"16\" height=\"16\" fill=\"#e0ecfb\"></rect><g stroke=\"#e2e8f0\" stroke-width=\"0.5\"><line x1=\"88\" y1=\"20\" x2=\"88\" y2=\"276\"></line><line x1=\"104\" y1=\"20\" x2=\"104\" y2=\"276\"></line><line x1=\"120\" y1=\"20\" x2=\"120\" y2=\"276\"></line><line x1=\"136\" y1=\"20\" x2=\"136\" y2=\"276\"></line><line x1=\"152\" y1=\"20\" x2=\"152\" y2=\"276\"></line><line x1=\"168\" y1=\"20\" x2=\"168\" y2=\"276\"></line><line x1=\"184\" y1=\"20\" x2=\"184\" y2=\"276\"></line><line x1=\"200\" y1=\"20\" x2=\"200\" y2=\"276\"></line><line x1=\"216\" y1=\"20\" x2=\"216\" y2=\"276\"></line><line x1=\"232\" y1=\"20\" x2=\"232\" y2=\"276\"></line><line x1=\"248\" y1=\"20\" x2=\"248\" y2=\"276\"></line><line x1=\"264\" y1=\"20\" x2=\"264\" y2=\"276\"></line><line x1=\"280\" y1=\"20\" x2=\"280\" y2=\"276\"></line><line x1=\"296\" y1=\"20\" x2=\"296\" y2=\"276\"></line><line x1=\"312\" y1=\"20\" x2=\"312\" y2=\"276\"></line><line x1=\"72\" y1=\"36\" x2=\"328\" y2=\"36\"></line><line x1=\"72\" y1=\"52\" x2=\"328\" y2=\"52\"></line><line x1=\"72\" y1=\"68\" x2=\"328\" y2=\"68\"></line><line x1=\"72\" y1=\"84\" x2=\"328\" y2=\"84\"></line><line x1=\"72\" y1=\"100\" x2=\"328\" y2=\"100\"></line><line x1=\"72\" y1=\"116\" x2=\"328\" y2=\"116\"></line><line x1=\"72\" y1=\"132\" x2=\"328\" y2=\"132\"></line><line x1=\"72\" y1=\"148\" x2=\"328\" y2=\"148\"></line><line x1=\"72\" y1=\"164\" x2=\"328\" y2=\"164\"></line><line x1=\"72\" y1=\"180\" x2=\"328\" y2=\"180\"></line><line x1=\"72\" y1=\"196\" x2=\"328\" y2=\"196\"></line><line x1=\"72\" y1=\"212\" x2=\"328\" y2=\"212\"></line><line x1=\"72\" y1=\"228\" x2=\"328\" y2=\"228\"></line><line x1=\"72\" y1=\"244\" x2=\"328\" y2=\"244\"></line><line x1=\"72\" y1=\"260\" x2=\"328\" y2=\"260\"></line></g><rect x=\"72\" y=\"20\" width=\"256\" height=\"256\" fill=\"none\" stroke=\"#cbd5e1\" stroke-width=\"1\" pointer-events=\"none\"></rect><g filter=\"url(#tvd-vp-shadow)\"><rect x=\"120\" y=\"100\" width=\"176\" height=\"80\" fill=\"rgba(37, 99, 235, 0.07)\" stroke=\"#2563eb\" stroke-width=\"2\" style=\"pointer-events:all;cursor:grab;transition:stroke-width 120ms ease;touch-action:none\"></rect></g><text x=\"208\" y=\"144\" font-family=\"-apple-system, system-ui, sans-serif\" font-size=\"11\" font-weight=\"600\" fill=\"#1e3a8a\" text-anchor=\"middle\" style=\"pointer-events:none\">viewport</text><text x=\"200\" y=\"294\" font-family=\"-apple-system, system-ui, sans-serif\" font-size=\"11\" font-weight=\"500\" fill=\"#374151\" text-anchor=\"middle\">Drag the viewport. Darker tiles are visible; lighter ring is prefetch margin.</text><text x=\"200\" y=\"310\" font-family=\"-apple-system, system-ui, sans-serif\" font-size=\"10\" fill=\"#6b7280\" text-anchor=\"middle\">Tracking<!-- --> <tspan font-weight=\"700\" fill=\"#2563eb\">91</tspan> <!-- -->of <!-- -->256<!-- --> tiles (<!-- -->55<!-- --> visible + <!-- -->36<!-- --> prefetch).</text></svg>\n<p><strong>Tile size.</strong> The savings come from the tiles a viewer <em>doesn't</em> receive — so the tile count needs to be high to make a real dent. A 4 × 4 grid barely helps; a 32 × 32 grid (1024 tiles) tracks the viewport tightly enough to land within striking distance of the original's per-viewer bytes.</p>\n<p><strong>Delivery.</strong> The natural first idea is one channel per tile — <code>tile:0:0</code>, ..., <code>tile:31:31</code>. Each viewer subscribes to the tile channels its viewport currently covers; the publisher publishes to 1024 channels per tick. This works. The cost shows up every time the viewer pans: crossing a tile boundary means sending a subscribe message for each newly-entered tile and an unsubscribe for each one left behind. Each subscribe also goes through its own authorization check, because the channels are independent — there's no way to share auth across them, even though for our purposes all 1024 tiles are really parts of one thing. So even small viewport movements turn into a lot of subscribe/unsubscribe traffic.</p>\n<p>But in Centrifugo v6.8.0 we have a mechanism that seems to fit better here — Shared Poll subscriptions.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"enter-shared-poll\">Enter shared poll<a href=\"https://centrifugal.dev/blog/2026/05/21/two-million-particles-on-centrifugo#enter-shared-poll\" class=\"hash-link\" aria-label=\"Direct link to Enter shared poll\" title=\"Direct link to Enter shared poll\" translate=\"no\">​</a></h2>\n<p>A subscriber on a <a href=\"https://centrifugal.dev/docs/server/shared_poll\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Shared Poll</a> channel calls <code>track(keys)</code> to declare which keys it cares about. A key is just an identifier tied to some application entity — in our case, a single tile.</p>\n<p>What makes Shared Poll fit here is its tracking model, not the polling itself: one channel, many keys, with each viewer declaring which keys it cares about. The name comes from its main delivery mode — Centrifugo aggregates tracked keys and asks the backend for updates on a timer — but it also has a direct publish path (<code>shared_poll_publish</code>), which is what we will use here to have low latency world updates. Centrifugo only delivers a key's bytes to viewers currently tracking that key. That's the property we need.</p>\n<p>On pan, the client doesn't subscribe to anything new — it just updates its track set with <code>untrack(leaving)</code> and <code>track(entering)</code>. Centrifugo handles the rest.</p>\n<p>For our particle demo it maps cleanly:</p>\n<ul>\n<li class=\"\">Split the 2200 × 2200 world into a 32 × 32 tile grid. Each tile covers 69 × 69 world pixels (⌈2200/32⌉ = 69; the rightmost and bottom tile columns overhang the world by 8 pixels of off-world margin), packed at 1 bpp into 9 bytes per row × 69 rows = <strong>621 bytes per tile</strong>.</li>\n<li class=\"\">One channel <code>tiles:world</code> with shared-poll subscription type, 1024 keys (<code>t_&lt;tx&gt;_&lt;ty&gt;</code>).</li>\n<li class=\"\">The simulation runs as before, but every tick the publisher packs <strong>all 1024 tiles</strong> and sends them in a single Centrifugo <code>/api/batch</code> request — 1024 <code>shared_poll_publish</code> commands, one HTTP round-trip instead of 1024.</li>\n<li class=\"\">Each viewer tracks the tiles its viewport intersects, plus a 1-tile prefetch margin so freshly-entered tiles already have data when they slide into view.</li>\n</ul>\n<p>The publisher side. A single monotonic version counter for all tiles, and a per-process epoch generated on startup so that a backend restart triggers Centrifugo to invalidate connected subscribers (they auto-resubscribe and pick up fresh state without a page reload):</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">channelEpoch </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> uuid</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewString</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> globalVersion </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">uint64</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// On every tick:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">v </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> atomic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">AddUint64</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">globalVersion</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">items </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">make</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">SharedPollItem</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">len</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">tilePayloads</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> i</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> payload </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">range</span><span class=\"token plain\"> tilePayloads </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    items </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">append</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">items</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> SharedPollItem</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        Key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">     </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">TileKey</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">i </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">%</span><span class=\"token plain\"> TilesPerSide</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> i </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\"> TilesPerSide</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        Data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">    payload</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        Version</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> v</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">api</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">BatchSharedPollPublish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"tiles:world\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> channelEpoch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> items</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>The batch wrapper turns that into one POST <code>/api/batch</code> carrying 1024 commands:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">c </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">CentrifugoAPI</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">BatchSharedPollPublish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> epoch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> items </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">SharedPollItem</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    commands </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">make</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">any</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">len</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">items</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> it </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">range</span><span class=\"token plain\"> items </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        commands </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">append</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">commands</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">any</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"shared_poll_publish\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">any</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"channel\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"key\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">     it</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"b64data\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> base64</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">StdEncoding</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">EncodeToString</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">it</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"version\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> it</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Version</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"epoch\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">   epoch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> c</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">call</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"batch\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">any</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"commands\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> commands</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>For per-key authorization the SDK calls a <code>getSignature</code> callback whenever the tracked key set changes. The callback hits a backend endpoint that returns an HMAC over <code>(channel, keys, expiry)</code> — the same shape the <a href=\"https://github.com/centrifugal/examples/tree/master/v6/shared_poll_demo/drones\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">drones demo</a> uses:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">MakeTrackSignature</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">secret</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> channel </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> keys </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> user </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ttlSec </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">int</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    now </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Now</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Unix</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    expiry </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> now </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">int64</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ttlSec</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    keysHash </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> sha256</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Sum256</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">strings</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Join</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">keys</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"\\x00\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    payload </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> fmt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Sprintf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"%d:%d:%s:%s:%x\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> now</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> expiry</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> user</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> keysHash</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    mac </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> hmac</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">sha256</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">secret</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    mac</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Write</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">payload</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> fmt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Sprintf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"%d:%d:%x\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> now</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> expiry</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> mac</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Sum</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>The viewer ties it together. One subscription, a track set that follows the camera, and pan reduces to changing the set:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSharedPollSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'tiles:world'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">getSignature</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> resp </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">fetch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'/api/track_refresh'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token literal-property property\">method</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'POST'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token literal-property property\">headers</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token string-property property\">'Content-Type'</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'application/json'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token literal-property property\">body</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token known-class-name class-name\" style=\"color:rgb(255, 203, 107)\">JSON</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">stringify</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">keys</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">keys</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">channel</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'tiles:world'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> resp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'update'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> m </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token regex regex-delimiter\">/</span><span class=\"token regex regex-source language-regex anchor function\" style=\"color:rgb(130, 170, 255)\">^</span><span class=\"token regex regex-source language-regex\">t_</span><span class=\"token regex regex-source language-regex group punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token regex regex-source language-regex char-set class-name\" style=\"color:rgb(255, 203, 107)\">\\d</span><span class=\"token regex regex-source language-regex quantifier number\" style=\"color:rgb(247, 140, 108)\">+</span><span class=\"token regex regex-source language-regex group punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token regex regex-source language-regex\">_</span><span class=\"token regex regex-source language-regex group punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token regex regex-source language-regex char-set class-name\" style=\"color:rgb(255, 203, 107)\">\\d</span><span class=\"token regex regex-source language-regex quantifier number\" style=\"color:rgb(247, 140, 108)\">+</span><span class=\"token regex regex-source language-regex group punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token regex regex-source language-regex anchor function\" style=\"color:rgb(130, 170, 255)\">$</span><span class=\"token regex regex-delimiter\">/</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">exec</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> tx </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\">m</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ty </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\">m</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">2</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">applyTile</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">tx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ty</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// decode this tile's bytes into the world buffer</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">updateTracking</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> want </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">visibleTileKeys</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\">   </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// viewport ∩ tile grid + 1-tile margin</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> toTrack </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\">want</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">filter</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">k</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token plain\">trackedKeys</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">has</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">k</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> toUntrack </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\">trackedKeys</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">filter</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">k</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token plain\">want</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">has</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">k</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">toUntrack</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">length</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">untrack</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">toUntrack</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">toTrack</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">length</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\">   sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">track</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">toTrack</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">k</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">key</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> k</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">version</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p><code>applyTile</code> decodes a single tile's packed bitmap into the client's world buffer at the tile's world position. The buffer is allocated for the full 2200 × 2200 world (<code>Uint32Array(2200 * 2200)</code>), but only the regions covered by tracked tiles ever get filled — everywhere else stays zero. A <code>requestAnimationFrame</code> loop copies the camera-cropped slice from this buffer onto a window-sized display canvas every paint. Pan handlers (right-mouse drag, arrow keys, WASD) update the camera, call <code>updateTracking()</code>, and the broker adjusts which tiles flow to this viewer.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"the-result\">The result<a href=\"https://centrifugal.dev/blog/2026/05/21/two-million-particles-on-centrifugo#the-result\" class=\"hash-link\" aria-label=\"Direct link to The result\" title=\"Direct link to The result\" translate=\"no\">​</a></h2>\n<p>The result where we also show movement over the world:</p>\n<video width=\"100%\" controls=\"\" preload=\"metadata\" src=\"/img/demo_particles_2_tiny.mp4\"></video>\n<p>See in <a href=\"https://youtu.be/R32qXLu8GOU\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">better quality on YouTube</a>.</p>\n<p>Same simulation, same MacBook 1410 × 730 viewport. Now let's compare:</p>\n<table><thead><tr><th>Approach</th><th>Data Size Per tick</th><th>Pan &amp; delivery</th></tr></thead><tbody><tr><td>Original (per-client crop, raw WS)</td><td>~129 KB</td><td>viewport-only; pan tied to 30 Hz publish rate</td></tr><tr><td>Single channel <code>fanout</code> (whole world)</td><td>~605 KB</td><td>whole world cached client-side; pan local at any frame rate</td></tr><tr><td><strong><code>shared_poll</code> tiles</strong></td><td>~186 KB</td><td>viewport tiles + prefetch margin; pan local at browser frame rate</td></tr></tbody></table>\n<p>Each tile is its own WebSocket message (Centrifugo can batch them into one frame); the <strong>~186 KB</strong> is the average sum across the tile messages a single viewer receives per 30 Hz tick.</p>\n<p>The shared-poll variant lands at 1.44× the original's per-tick bytes on a standard MacBook screen, at full resolution, with full panning support, and with fan-out preserved. Worth unpacking why.</p>\n<p><strong>Where the extra bytes come from.</strong> Three things stack up:</p>\n<table><thead><tr><th>Component</th><th>Bytes/tick</th><th>Ratio</th></tr></thead><tbody><tr><td>Pure viewport (1410 × 730 / 8)</td><td>~129 KB</td><td>1.00×</td></tr><tr><td>+ Tile alignment (21 × 11 tiles, no prefetch)</td><td>~143 KB</td><td>1.11×</td></tr><tr><td><strong>+ 1-tile prefetch margin (23 × 13 tiles tracked)</strong></td><td><strong>~186 KB</strong></td><td><strong>1.44×</strong></td></tr></tbody></table>\n<p>Each tile is 69 px (⌈2200/32⌉). A 1410 × 730 viewport touches ⌈1410/69⌉ × ⌈730/69⌉ = <strong>21 × 11 = 231 tiles</strong>; with a one-tile prefetch ring, <strong>23 × 13 = 299 tiles</strong>. At 621 bytes per tile: 231 × 621 ≈ <strong>143 KB</strong> without prefetch, 299 × 621 ≈ <strong>186 KB</strong> with.</p>\n<p>Tile alignment is the small overhead: your viewport rarely lines up with tile edges, so the tiles around its border load in full even when only a strip of each is actually visible (that's the 1.11× row). Prefetch is the bigger cost, and it's a UX choice: without it, panning into a fresh tile would render zeros until <code>track()</code> lands and the next publish arrives — visible pop-in on the leading edge.</p>\n<p><strong>Why smaller tiles don't help.</strong> Two effects pull in opposite directions as tile size shrinks:</p>\n<ul>\n<li class=\"\"><strong>The prefetch ring shrinks in pixels.</strong> A one-tile margin on each side gets smaller as tiles do, so prefetch waste drops linearly with tile size.</li>\n<li class=\"\"><strong>Per-tile fixed overhead grows in total.</strong> Each tile is its own <code>shared_poll_publish</code> frame with key, version, epoch, and framing bytes — plus byte-alignment loss inside the 1bpp packing (a 35-px row still needs 5 bytes, with most of the last byte wasted). Halving tile size yields 4× more tiles, so those fixed costs scale up 4×.</li>\n</ul>\n<p>The two roughly cancel — the ratio stays near the same value across a wide range of grid sizes. Going larger makes prefetch waste dominate; going smaller makes per-tile overhead dominate. That ceiling is built into the tile model whenever you also want panning to stay smooth — it's not something you can tune away.</p>\n<p><strong>Why prefetch exists at all — and why pans feel smoother because of it.</strong> The two designs differ in <em>where the world lives</em> on the client.</p>\n<p>The original has no client-side world cache. Every frame is the server-packed bitmap of exactly the current viewport. Pan is a server roundtrip — new camera up, new viewport down — so pan latency is 1 RTT + 1 tick, and pan smoothness is bounded by the 30 Hz publish rate. Quick pans visibly stutter.</p>\n<p>Tile mode puts the world <em>partially</em> on the client. Each viewer keeps a local buffer of its tracked tiles and re-crops it on every <code>requestAnimationFrame</code> (60+ Hz, well above the publish rate). Pans inside already-loaded tiles paint instantly from this buffer — that's why pan feels smooth, not anything happening on the wire.</p>\n<p>The catch: the cache has edges. Pan into a tile you weren't tracking and you'd render zeros until <code>track()</code> lands and the next publish delivers the bytes — ~33–66 ms of pop-in. Prefetch fills those edges by keeping the next ring of tiles always subscribed.</p>\n<p>So prefetch is the price of having a local cache, and the local cache is what makes pans feel smoother. They come together.</p>\n<p><strong>What the broker model gives you.</strong> That overhead isn't what you pay for the broker — it's the cost of prefetch + alignment <em>within</em> the broker model. What the broker actually brings doesn't show up in bytes per viewer at all:</p>\n<ul>\n<li class=\"\"><strong>Publish work is flat in viewer count.</strong> The publisher packs tiles once per tick and sends them once. One viewer or a thousand, same job; Centrifugo fans out from there. Backend → Centrifugo stays at ~19 MB/s regardless of who's connected.</li>\n<li class=\"\"><strong>Multi-node is just configuration.</strong> Switch the broker to <a href=\"https://centrifugal.dev/docs/server/engines\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Redis or NATS</a> and the same setup runs across many Centrifugo nodes — tiles published anywhere reach viewers anywhere, viewers connect to whichever node is least busy, and the publisher doesn't know how many nodes exist. Application code unchanged.</li>\n</ul>\n<p>The trade: 1.44× bytes per viewer, in exchange for fan-out that doesn't grow with viewer count and multi-node by configuration. The original was built for a different goal — exploring Go performance in a single-process design — and it's excellent at that. A project that needed both 1.0× per-viewer bytes <em>and</em> multi-node would have to build the broker layer itself.</p>\n<p>Both modes are wired into a single docker-compose; pick one with the <code>MODE</code> env var:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker compose up --build                          # default: fanout (whole world)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">MODE=shared_poll docker compose up --build         # shared-poll tiles, full-res, panable</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"what-this-taught-us\">What this taught us<a href=\"https://centrifugal.dev/blog/2026/05/21/two-million-particles-on-centrifugo#what-this-taught-us\" class=\"hash-link\" aria-label=\"Direct link to What this taught us\" title=\"Direct link to What this taught us\" translate=\"no\">​</a></h2>\n<p>We re-implemented the demo in Centrifugo. We didn't match the original's WebSocket bytes — but the trade buys better publish scalability and smoother panning.</p>\n<p>The breakdown above pins almost all of the per-viewer overhead on prefetch — drop it and you're at 1.11×, with visible pop-in on the leading edge during fast pans. So the byte overhead is essentially the price of a UX choice we made, not the price of using Centrifugo.</p>\n<p>Shared Poll subscriptions are a clean way to track many objects with a single subscription — <code>track</code>/<code>untrack</code> is one protocol frame for multiple keys, no per-object subscribe handshake. The name comes from the polling delivery mode, but versioned mode also exposes a fast push path (<code>shared_poll_publish</code>), which is what gives us low-latency world updates here.</p>\n<p>The full source, both modes selectable via env var, is in <a href=\"https://github.com/centrifugal/examples/tree/master/v6/millions_of_particles\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>v6/millions_of_particles</code></a>. Again, see David Gerrells' <a href=\"https://dgerrells.com/blog/how-fast-is-go-simulating-millions-of-particles-on-a-smart-tv\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">original post</a> and <a href=\"https://github.com/dgerrells/how-fast-is-it\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">repo</a>.</p>",
            "url": "https://centrifugal.dev/blog/2026/05/21/two-million-particles-on-centrifugo",
            "title": "Recreating a Two Million Particle World at 30 Hz with Centrifugo",
            "summary": "Recreating David Gerrells' 2M-particle multiplayer simulation on top of Centrifugo WebSocket protocol. The naive single channel fanout balloons each tick from 129KB to 605KB. We explore how to improve that given Centrifugo nature and good UX in mind.",
            "date_modified": "2026-05-21T18:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "websocket",
                "demo",
                "performance"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions",
            "content_html": "<p>What if 10,000 clients could stay up to date with just one backend request per second? That's the idea behind shared poll subscriptions, new in Centrifugo v6.8.0.</p>\n<p>Centrifugo's core model is push-based: your backend publishes to a channel, Centrifugo delivers it over persistent WebSocket connections, and <a class=\"\" href=\"https://centrifugal.dev/docs/server/history_and_recovery\">history and recovery</a> catch clients up after a reconnect. This is what we call <strong>stream subscriptions</strong> — the original and still the most common subscription type. They work well when your backend owns the writes, and they'll remain the foundation of most Centrifugo deployments. But for some use cases a different shape fits better — and gives you things push alone can't.</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_drones.mp4\"></video>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>TL;DR</div><div class=\"admonitionContent_BuS1\"><ul>\n<li class=\"\">Each Centrifugo node aggregates interest across all its connected clients and polls your backend once per cycle for the <strong>union</strong> of tracked keys — backend load scales with <code>O(unique items)</code>, not <code>O(clients)</code>.</li>\n<li class=\"\">Clients use a single <code>SharedPollSubscription</code> and call <code>track()</code> / <code>untrack()</code> as items come into view. Per-key access is enforced via fast HMAC signatures verified locally on every track request.</li>\n<li class=\"\">Three delivery tiers cover the latency spectrum: timer polling (zero integration), <code>shared_poll_publish</code> for instant push from the write path, and the PRO <strong>notification fast path</strong> (instant trigger, polled fetch).</li>\n<li class=\"\">No inter-node PUB/SUB — each node polls and serves its own clients. A cluster that only uses shared poll needs no Redis or NATS.</li>\n</ul></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"when-polling-makes-more-sense-than-push\">When polling makes more sense than push<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#when-polling-makes-more-sense-than-push\" class=\"hash-link\" aria-label=\"Direct link to When polling makes more sense than push\" title=\"Direct link to When polling makes more sense than push\" translate=\"no\">​</a></h2>\n<p>Consider a social feed where each post displays a live vote count. The data already lives in your database — the question is how to keep it fresh on every client's screen.</p>\n<p>You could use stream subscriptions: publish to Centrifugo on every vote and subscribers see the update in real time. This works well for one feature, but it means every write path touching vote counts — every API endpoint, every background job, every migration script — has to call the publish API. As the number of live-updating fields grows (view counts, comment counts, stock levels, configuration flags), the integration surface grows with it. And it assumes you control the writes — third-party APIs, legacy systems, and databases shared across services often don't expose a hook.</p>\n<p>In these cases teams reach for polling. Each client hits an endpoint every few seconds — simple, decoupled from writes, works with any data source. But at scale 10,000 clients per second is 10,000 requests per second, mostly returning unchanged data. That overhead is what usually pushes teams toward push-based systems in the first place.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"the-inversion-server-side-polling\">The inversion: server-side polling<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#the-inversion-server-side-polling\" class=\"hash-link\" aria-label=\"Direct link to The inversion: server-side polling\" title=\"Direct link to The inversion: server-side polling\" translate=\"no\">​</a></h2>\n<p>Shared poll subscriptions move polling from clients to Centrifugo. Clients hold a persistent WebSocket connection (or HTTP-streaming/SSE fallback) and tell Centrifugo which items they care about. Centrifugo asks your backend on a configurable schedule, detects what changed, and pushes updates to interested clients.</p>\n<p>Each client sees a different subset of items — different pages, scroll positions, search results — but many subsets overlap. Centrifugo aggregates interest across all connected clients on a node and polls only the union of tracked keys once per cycle. If 10,000 clients are watching overlapping sets that cover 200 unique posts, that's one request for 200 items, fanned out — O(unique items), not O(clients).</p>\n<svg viewBox=\"0 0 820 430\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b\"><defs><marker id=\"sp-arrow-blue\" markerWidth=\"10\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#5b8def\"></polygon></marker><marker id=\"sp-arrow-red\" markerWidth=\"10\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#fe5e5e\"></polygon></marker><marker id=\"sp-arrow-green\" markerWidth=\"10\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#5bef7b\"></polygon></marker><linearGradient id=\"sp-grad-client\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#1e2430\"></stop><stop offset=\"100%\" stop-color=\"#161a22\"></stop></linearGradient><linearGradient id=\"sp-grad-centrifugo\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#2d2020\"></stop><stop offset=\"100%\" stop-color=\"#1f1515\"></stop></linearGradient><linearGradient id=\"sp-grad-backend\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#1e2a20\"></stop><stop offset=\"100%\" stop-color=\"#161e16\"></stop></linearGradient><linearGradient id=\"sp-grad-inner\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#222830\"></stop><stop offset=\"100%\" stop-color=\"#1a1e24\"></stop></linearGradient><filter id=\"sp-glow-blue\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#5b8def\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"sp-glow-red\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"4\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#fe5e5e\" flood-opacity=\"0.15\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"sp-glow-green\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#5bef7b\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><pattern id=\"sp-hatch\" width=\"12\" height=\"12\" patternTransform=\"rotate(45)\" patternUnits=\"userSpaceOnUse\"><line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"12\" stroke=\"#222224\" stroke-width=\"0.5\"></line></pattern></defs><rect x=\"15\" y=\"10\" width=\"790\" height=\"410\" rx=\"12\" fill=\"none\" stroke=\"#444\" stroke-width=\"1\" stroke-dasharray=\"8,4\"></rect><rect x=\"15\" y=\"10\" width=\"790\" height=\"410\" rx=\"12\" fill=\"url(#sp-hatch)\" opacity=\"0.2\"></rect><g transform=\"translate(35, 35)\" filter=\"url(#sp-glow-blue)\"><rect width=\"152\" height=\"72\" rx=\"8\" fill=\"url(#sp-grad-client)\" stroke=\"#5b8def\" stroke-width=\"1.5\"></rect><text x=\"76\" y=\"23\" font-size=\"13\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Client A</text><text x=\"76\" y=\"42\" font-size=\"11\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">tracking:</text><text x=\"76\" y=\"60\" font-size=\"11\" font-weight=\"600\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"monospace\">post_1, post_2</text></g><g transform=\"translate(35, 132)\" filter=\"url(#sp-glow-blue)\"><rect width=\"152\" height=\"72\" rx=\"8\" fill=\"url(#sp-grad-client)\" stroke=\"#5b8def\" stroke-width=\"1.5\"></rect><text x=\"76\" y=\"23\" font-size=\"13\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Client B</text><text x=\"76\" y=\"42\" font-size=\"11\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">tracking:</text><text x=\"76\" y=\"60\" font-size=\"11\" font-weight=\"600\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"monospace\">post_2, post_3</text></g><g transform=\"translate(35, 229)\" filter=\"url(#sp-glow-blue)\"><rect width=\"152\" height=\"72\" rx=\"8\" fill=\"url(#sp-grad-client)\" stroke=\"#5b8def\" stroke-width=\"1.5\"></rect><text x=\"76\" y=\"23\" font-size=\"13\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Client C</text><text x=\"76\" y=\"42\" font-size=\"11\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">tracking:</text><text x=\"76\" y=\"60\" font-size=\"11\" font-weight=\"600\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"monospace\">post_1, post_5</text></g><text x=\"111\" y=\"322\" font-size=\"16\" fill=\"#555\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">⋮</text><text x=\"111\" y=\"340\" font-size=\"11\" fill=\"#555\" font-style=\"italic\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">N clients</text><line x1=\"187\" y1=\"57\" x2=\"310\" y2=\"57\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#sp-arrow-blue)\"></line><line x1=\"187\" y1=\"154\" x2=\"310\" y2=\"154\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#sp-arrow-blue)\"></line><line x1=\"187\" y1=\"251\" x2=\"310\" y2=\"251\" stroke=\"#5b8def\" stroke-width=\"1.5\" marker-end=\"url(#sp-arrow-blue)\"></line><text x=\"248\" y=\"148\" font-size=\"11\" font-weight=\"600\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">track</text><line x1=\"313\" y1=\"85\" x2=\"192\" y2=\"85\" stroke=\"#5bef7b\" stroke-width=\"1.5\" stroke-dasharray=\"5,3\" marker-end=\"url(#sp-arrow-green)\"></line><line x1=\"313\" y1=\"182\" x2=\"192\" y2=\"182\" stroke=\"#5bef7b\" stroke-width=\"1.5\" stroke-dasharray=\"5,3\" marker-end=\"url(#sp-arrow-green)\"></line><line x1=\"313\" y1=\"279\" x2=\"192\" y2=\"279\" stroke=\"#5bef7b\" stroke-width=\"1.5\" stroke-dasharray=\"5,3\" marker-end=\"url(#sp-arrow-green)\"></line><text x=\"248\" y=\"198\" font-size=\"11\" font-weight=\"600\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">updates</text><g transform=\"translate(315, 25)\" filter=\"url(#sp-glow-red)\"><rect width=\"195\" height=\"295\" rx=\"10\" fill=\"url(#sp-grad-centrifugo)\" stroke=\"#fe5e5e\" stroke-width=\"1.5\"></rect><text x=\"98\" y=\"26\" font-size=\"14\" font-weight=\"bold\" fill=\"#fe5e5e\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">CENTRIFUGO</text><rect x=\"12\" y=\"38\" width=\"171\" height=\"72\" rx=\"6\" fill=\"url(#sp-grad-inner)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"98\" y=\"58\" font-size=\"11\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Aggregate tracked keys</text><text x=\"98\" y=\"80\" font-size=\"14\" font-weight=\"bold\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"monospace\">{ 1, 2, 3, 5 }</text><text x=\"98\" y=\"100\" font-size=\"10\" fill=\"#666\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">4 unique from 6 tracked</text><rect x=\"12\" y=\"120\" width=\"171\" height=\"38\" rx=\"6\" fill=\"url(#sp-grad-inner)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"98\" y=\"144\" font-size=\"11\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Poll backend once per cycle</text><rect x=\"12\" y=\"168\" width=\"171\" height=\"38\" rx=\"6\" fill=\"url(#sp-grad-inner)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"98\" y=\"192\" font-size=\"11\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Compare versions per client</text><rect x=\"12\" y=\"216\" width=\"171\" height=\"38\" rx=\"6\" fill=\"url(#sp-grad-inner)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"98\" y=\"240\" font-size=\"11\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Push only changed items</text><text x=\"98\" y=\"278\" font-size=\"12\" font-weight=\"bold\" fill=\"#fe5e5e\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">1 request / cycle</text></g><line x1=\"510\" y1=\"164\" x2=\"605\" y2=\"164\" stroke=\"#fe5e5e\" stroke-width=\"1.5\" marker-end=\"url(#sp-arrow-red)\"></line><text x=\"560\" y=\"156\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">poll</text><line x1=\"608\" y1=\"212\" x2=\"515\" y2=\"212\" stroke=\"#5bef7b\" stroke-width=\"1.5\" marker-end=\"url(#sp-arrow-green)\"></line><text x=\"560\" y=\"230\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">data + versions</text><g transform=\"translate(610, 75)\" filter=\"url(#sp-glow-green)\"><rect width=\"170\" height=\"220\" rx=\"10\" fill=\"url(#sp-grad-backend)\" stroke=\"#5bef7b\" stroke-width=\"1.5\"></rect><text x=\"85\" y=\"26\" font-size=\"14\" font-weight=\"bold\" fill=\"#5bef7b\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">YOUR BACKEND</text><text x=\"85\" y=\"56\" font-size=\"11\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Return current data</text><text x=\"85\" y=\"74\" font-size=\"11\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">for requested keys</text><rect x=\"12\" y=\"85\" width=\"146\" height=\"55\" rx=\"6\" fill=\"url(#sp-grad-inner)\" stroke=\"#5bef7b\" stroke-width=\"1\" stroke-dasharray=\"4,3\"></rect><text x=\"85\" y=\"105\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">post_1: v6, data: ...</text><text x=\"85\" y=\"122\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">post_3: v12, data: ...</text><text x=\"85\" y=\"136\" font-size=\"9\" fill=\"#666\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">items + versions</text><text x=\"85\" y=\"168\" font-size=\"11\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">No publish hooks</text><text x=\"85\" y=\"186\" font-size=\"11\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Just a read endpoint</text><text x=\"85\" y=\"209\" font-size=\"10\" fill=\"#666\" font-style=\"italic\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Called once per cycle</text></g><g transform=\"translate(35, 355)\"><rect width=\"750\" height=\"52\" rx=\"8\" fill=\"url(#sp-grad-centrifugo)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"375\" y=\"22\" font-size=\"13\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Client-side polling: 10,000 clients = <tspan fill=\"#fe5e5e\" font-weight=\"bold\">10,000 req/s</tspan></text><text x=\"375\" y=\"42\" font-size=\"13\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Shared poll: 10,000 clients = <tspan fill=\"#5bef7b\" font-weight=\"bold\">1 req / cycle</tspan></text></g></svg>\n<p>Your backend just needs one endpoint that answers \"here is the current data for these keys.\" It's a standard Centrifugo <a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy\">proxy</a> — same shape as subscribe or publish proxies. No publish calls, no event hooks, no coupling to the write path: if you can read the data, you can serve it through shared poll.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"per-item-tracking-without-per-item-channels\">Per-item tracking without per-item channels<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#per-item-tracking-without-per-item-channels\" class=\"hash-link\" aria-label=\"Direct link to Per-item tracking without per-item channels\" title=\"Direct link to Per-item tracking without per-item channels\" translate=\"no\">​</a></h2>\n<p>Before shared poll, the natural Centrifugo approach would be a channel per item — one for each post's vote count. Channels are lightweight and ephemeral, so creating many is fine server-side. The cost lands on the client: a feed with 50–100 visible posts means 50–100 active subscriptions, each with its own subscribe/unsubscribe frame, signature, recovery state, and position tracking. Across thousands of concurrent users that adds up on both ends of the wire.</p>\n<p>A single channel for \"all vote updates\" solves the overhead problem but creates a different one: every client receives every update, regardless of which posts they're actually viewing. With millions of posts and thousands of clients, most of the data delivered is irrelevant.</p>\n<p>Shared poll subscriptions give per-item granularity without per-item channels. The client creates one subscription, then tracks and untracks specific keys as the user scrolls:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSharedPollSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'post_votes:feed1'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">getSignature</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> resp </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">fetch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'/api/sign-poll'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token literal-property property\">method</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'POST'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token literal-property property\">body</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token known-class-name class-name\" style=\"color:rgb(255, 203, 107)\">JSON</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">stringify</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">keys</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">keys</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> resp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'update'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">removed</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">?</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">hideVoteWidget</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">updateVoteWidget</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">track</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">getVisiblePostIds</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>One subscription, arbitrary number of tracked keys; the server only polls items that at least one client is watching. The SDK exposes a dedicated <code>SharedPollSubscription</code> type with <code>track</code>, <code>untrack</code>, and <code>trackedKeys</code> — purpose-built rather than flags on the regular subscription. Server-side, shared poll is configured through the standard <a class=\"\" href=\"https://centrifugal.dev/docs/server/channels\">channel namespace</a> system: set <code>subscription_type: \"shared_poll\"</code> on a namespace and configure the polling interval, batch size, and backend proxy. Per-key routing is handled internally — hidden behind the same <code>track()</code> call.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"authorization-with-hmac-signatures\">Authorization with HMAC signatures<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#authorization-with-hmac-signatures\" class=\"hash-link\" aria-label=\"Direct link to Authorization with HMAC signatures\" title=\"Direct link to Authorization with HMAC signatures\" translate=\"no\">​</a></h2>\n<p>Per-item tracking raises an authorization question: how do you control which items a client can track? Centrifugo's existing <a class=\"\" href=\"https://centrifugal.dev/docs/server/authentication\">connection</a> and <a class=\"\" href=\"https://centrifugal.dev/docs/server/channel_token_auth\">channel</a> tokens operate at channel level — but a shared poll client subscribes to one channel and tracks many keys within it.</p>\n<p>Shared poll uses HMAC signatures rather than JWTs. Tracked keys are first-class values in the protocol (not buried in a token payload), and HMAC is an order of magnitude faster to generate and verify — which matters when thousands of clients refresh signatures every poll cycle. When the client calls <code>track()</code>, the SDK invokes a <code>getSignature</code> callback; your backend decides which of the requested keys the user is allowed to see and signs that subset. Centrifugo verifies the HMAC on every track request.</p>\n<p>The signature binds user, channel, key set, and a time window — it can't be reused for different keys or different users, and it expires. If <code>getSignature</code> returns fewer keys than the client requested, the omitted keys are treated as revoked: the SDK emits removal events and stops tracking them, so revoking access takes effect on the next refresh without any server push. Apply your existing RBAC or row-level security inside <code>getSignature</code> — Centrifugo enforces whatever decisions your backend makes.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"versionless-and-versioned-modes\">Versionless and versioned modes<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#versionless-and-versioned-modes\" class=\"hash-link\" aria-label=\"Direct link to Versionless and versioned modes\" title=\"Direct link to Versionless and versioned modes\" translate=\"no\">​</a></h2>\n<p>The backend refresh endpoint can be as simple or as sophisticated as your data allows. The simplest integration is versionless mode: your endpoint receives a list of keys and returns <code>{key, data}</code> pairs. Centrifugo detects changes by comparing content hashes internally. The backend doesn't need to track versions — it just returns current state.</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"result\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"items\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token property\">\"key\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"post_123\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token property\">\"data\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token property\">\"votes\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">42</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token property\">\"key\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"post_456\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token property\">\"data\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token property\">\"votes\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">7</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>This is enough for most use cases. But if your data already has versions (a database sequence number, a timestamp, a monotonic counter), versioned mode lets you do more. Centrifugo includes the last known version in each request, allowing your backend to skip unchanged items — reducing response size and database load. Versioned mode also pairs cleanly with <a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#direct-publish-instant-when-you-have-it-polled-when-you-dont\" class=\"\">direct publish</a> for instant delivery, since versions coordinate which keys are already fresh.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"direct-publish-instant-when-you-have-it-polled-when-you-dont\">Direct publish: instant when you have it, polled when you don't<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#direct-publish-instant-when-you-have-it-polled-when-you-dont\" class=\"hash-link\" aria-label=\"Direct link to Direct publish: instant when you have it, polled when you don't\" title=\"Direct link to Direct publish: instant when you have it, polled when you don't\" translate=\"no\">​</a></h2>\n<p>Polling has a latency cost: updates arrive within the polling interval, not instantly. For vote counts and view counts, seconds of latency are fine. But sometimes your backend already has the data right after a database write and wants instant delivery.</p>\n<p>Your backend can use the <code>shared_poll_publish</code> server API to deliver data directly to tracking clients, skipping the poll cycle entirely. Centrifugo delivers the data immediately and marks the key as \"fresh\" so the next poll cycle skips it, avoiding a redundant backend call.</p>\n<p>This gives shared poll two delivery speeds: timer-based polling (seconds of latency, zero integration effort) and direct publish (instant, requires a publish call on the write path). The two complement each other — direct publish for speed, polling as the safety net. If a direct publish is missed (process crash, network issue), the next poll cycle picks it up.</p>\n<svg viewBox=\"0 0 820 430\" xmlns=\"http://www.w3.org/2000/svg\" style=\"width:100%;height:auto;border-radius:10px;background-color:#17171b\"><defs><marker id=\"spp-arrow-blue\" markerWidth=\"10\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#5b8def\"></polygon></marker><marker id=\"spp-arrow-red\" markerWidth=\"10\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#fe5e5e\"></polygon></marker><marker id=\"spp-arrow-green\" markerWidth=\"10\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#5bef7b\"></polygon></marker><marker id=\"spp-arrow-amber\" markerWidth=\"10\" markerHeight=\"7\" refX=\"7\" refY=\"3.5\" orient=\"auto\"><polygon points=\"0 0, 10 3.5, 0 7\" fill=\"#f0b060\"></polygon></marker><linearGradient id=\"spp-grad-client\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#1e2430\"></stop><stop offset=\"100%\" stop-color=\"#161a22\"></stop></linearGradient><linearGradient id=\"spp-grad-centrifugo\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#2d2020\"></stop><stop offset=\"100%\" stop-color=\"#1f1515\"></stop></linearGradient><linearGradient id=\"spp-grad-backend\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#1e2a20\"></stop><stop offset=\"100%\" stop-color=\"#161e16\"></stop></linearGradient><linearGradient id=\"spp-grad-inner\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\"><stop offset=\"0%\" stop-color=\"#222830\"></stop><stop offset=\"100%\" stop-color=\"#1a1e24\"></stop></linearGradient><filter id=\"spp-glow-blue\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#5b8def\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"spp-glow-red\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"4\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#fe5e5e\" flood-opacity=\"0.15\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"spp-glow-green\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#5bef7b\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><filter id=\"spp-glow-amber\" x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\"><feGaussianBlur stdDeviation=\"3\" result=\"blur\"></feGaussianBlur><feFlood flood-color=\"#f0b060\" flood-opacity=\"0.12\"></feFlood><feComposite in2=\"blur\" operator=\"in\"></feComposite><feMerge><feMergeNode></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter><pattern id=\"spp-hatch\" width=\"12\" height=\"12\" patternTransform=\"rotate(45)\" patternUnits=\"userSpaceOnUse\"><line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"12\" stroke=\"#222224\" stroke-width=\"0.5\"></line></pattern></defs><rect x=\"15\" y=\"10\" width=\"790\" height=\"410\" rx=\"12\" fill=\"none\" stroke=\"#444\" stroke-width=\"1\" stroke-dasharray=\"8,4\"></rect><rect x=\"15\" y=\"10\" width=\"790\" height=\"410\" rx=\"12\" fill=\"url(#spp-hatch)\" opacity=\"0.2\"></rect><g transform=\"translate(35, 28)\" filter=\"url(#spp-glow-green)\"><rect width=\"175\" height=\"270\" rx=\"10\" fill=\"url(#spp-grad-backend)\" stroke=\"#5bef7b\" stroke-width=\"1.5\"></rect><text x=\"88\" y=\"26\" font-size=\"14\" font-weight=\"bold\" fill=\"#5bef7b\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">YOUR BACKEND</text><rect x=\"12\" y=\"40\" width=\"151\" height=\"48\" rx=\"6\" fill=\"url(#spp-grad-inner)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"88\" y=\"58\" font-size=\"10\" fill=\"#666\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Step 1</text><text x=\"88\" y=\"78\" font-size=\"12\" font-weight=\"600\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Update database</text><rect x=\"12\" y=\"98\" width=\"151\" height=\"48\" rx=\"6\" fill=\"url(#spp-grad-inner)\" stroke=\"#5bef7b\" stroke-width=\"1\" stroke-dasharray=\"4,3\"></rect><text x=\"88\" y=\"116\" font-size=\"10\" fill=\"#666\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Step 2</text><text x=\"88\" y=\"137\" font-size=\"11\" font-weight=\"600\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">shared_poll_publish</text><rect x=\"12\" y=\"156\" width=\"151\" height=\"98\" rx=\"6\" fill=\"url(#spp-grad-inner)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"88\" y=\"176\" font-size=\"10\" fill=\"#666\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Payload</text><text x=\"88\" y=\"194\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">channel: post_votes</text><text x=\"88\" y=\"210\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">key: post_123</text><text x=\"88\" y=\"226\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">version: 7</text><text x=\"88\" y=\"246\" font-size=\"10\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">data: {votes: 43}</text></g><line x1=\"210\" y1=\"155\" x2=\"298\" y2=\"155\" stroke=\"#5bef7b\" stroke-width=\"1.5\" marker-end=\"url(#spp-arrow-green)\"></line><text x=\"254\" y=\"146\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">API call</text><g transform=\"translate(303, 28)\" filter=\"url(#spp-glow-red)\"><rect width=\"215\" height=\"270\" rx=\"10\" fill=\"url(#spp-grad-centrifugo)\" stroke=\"#fe5e5e\" stroke-width=\"1.5\"></rect><text x=\"108\" y=\"26\" font-size=\"14\" font-weight=\"bold\" fill=\"#fe5e5e\" text-anchor=\"middle\" letter-spacing=\"1px\" font-family=\"system-ui, sans-serif\">CENTRIFUGO</text><rect x=\"12\" y=\"40\" width=\"191\" height=\"44\" rx=\"6\" fill=\"url(#spp-grad-inner)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"108\" y=\"58\" font-size=\"11\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Receive publish</text><text x=\"108\" y=\"76\" font-size=\"10\" fill=\"#666\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Compare version (7 &gt; 6 ✓)</text><rect x=\"12\" y=\"94\" width=\"191\" height=\"44\" rx=\"6\" fill=\"url(#spp-grad-inner)\" stroke=\"#fe5e5e\" stroke-width=\"1\" stroke-dasharray=\"4,3\"></rect><text x=\"108\" y=\"112\" font-size=\"11\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Distribute via Broker PUB/SUB</text><text x=\"108\" y=\"130\" font-size=\"10\" fill=\"#666\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">when publish_enabled: true</text><rect x=\"12\" y=\"148\" width=\"191\" height=\"34\" rx=\"6\" fill=\"url(#spp-grad-inner)\" stroke=\"#f0b060\" stroke-width=\"1\"></rect><text x=\"108\" y=\"170\" font-size=\"11\" font-weight=\"600\" fill=\"#f0b060\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Mark key \"fresh\"</text><rect x=\"12\" y=\"192\" width=\"191\" height=\"34\" rx=\"6\" fill=\"url(#spp-grad-inner)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"108\" y=\"214\" font-size=\"11\" fill=\"#ccc\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Fan out to tracking clients</text><text x=\"108\" y=\"252\" font-size=\"11\" font-weight=\"bold\" fill=\"#f0b060\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Next poll → key skipped</text><text x=\"108\" y=\"267\" font-size=\"10\" fill=\"#666\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">0 redundant backend calls</text></g><line x1=\"518\" y1=\"105\" x2=\"618\" y2=\"60\" stroke=\"#5b8def\" stroke-width=\"1.5\" stroke-dasharray=\"5,3\" marker-end=\"url(#spp-arrow-blue)\"></line><line x1=\"518\" y1=\"155\" x2=\"618\" y2=\"155\" stroke=\"#5b8def\" stroke-width=\"1.5\" stroke-dasharray=\"5,3\" marker-end=\"url(#spp-arrow-blue)\"></line><line x1=\"518\" y1=\"210\" x2=\"618\" y2=\"257\" stroke=\"#444\" stroke-width=\"1.2\" stroke-dasharray=\"5,3\"></line><text x=\"572\" y=\"100\" font-size=\"10\" font-weight=\"600\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\" transform=\"rotate(-16, 572, 100)\">instant</text><text x=\"572\" y=\"147\" font-size=\"10\" font-weight=\"600\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">instant</text><text x=\"582\" y=\"250\" font-size=\"9\" fill=\"#555\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">different key</text><g transform=\"translate(623, 30)\" filter=\"url(#spp-glow-blue)\"><rect width=\"160\" height=\"66\" rx=\"8\" fill=\"url(#spp-grad-client)\" stroke=\"#5b8def\" stroke-width=\"1.5\"></rect><text x=\"80\" y=\"20\" font-size=\"13\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Client A</text><text x=\"80\" y=\"37\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">tracking:</text><text x=\"80\" y=\"56\" font-size=\"11\" font-weight=\"600\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">post_123 → v7 ✓</text></g><g transform=\"translate(623, 125)\" filter=\"url(#spp-glow-blue)\"><rect width=\"160\" height=\"66\" rx=\"8\" fill=\"url(#spp-grad-client)\" stroke=\"#5b8def\" stroke-width=\"1.5\"></rect><text x=\"80\" y=\"20\" font-size=\"13\" font-weight=\"bold\" fill=\"#5b8def\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Client B</text><text x=\"80\" y=\"37\" font-size=\"10\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">tracking:</text><text x=\"80\" y=\"56\" font-size=\"11\" font-weight=\"600\" fill=\"#5bef7b\" text-anchor=\"middle\" font-family=\"monospace\">post_123 → v7 ✓</text></g><g transform=\"translate(623, 227)\"><rect width=\"160\" height=\"66\" rx=\"8\" fill=\"url(#spp-grad-client)\" stroke=\"#5b8def\" stroke-width=\"1.5\" opacity=\"0.4\"></rect><text x=\"80\" y=\"20\" font-size=\"13\" font-weight=\"bold\" fill=\"#5b8def\" opacity=\"0.5\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Client C</text><text x=\"80\" y=\"37\" font-size=\"10\" fill=\"#888\" opacity=\"0.5\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">tracking:</text><text x=\"80\" y=\"56\" font-size=\"11\" font-weight=\"600\" fill=\"#666\" text-anchor=\"middle\" font-family=\"monospace\">post_456</text></g><g transform=\"translate(35, 320)\"><rect width=\"750\" height=\"88\" rx=\"8\" fill=\"url(#spp-grad-centrifugo)\" stroke=\"#444\" stroke-width=\"1\"></rect><text x=\"375\" y=\"23\" font-size=\"13\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Polling only: updates arrive within <tspan fill=\"#fe5e5e\" font-weight=\"bold\">refresh_interval</tspan></text><text x=\"375\" y=\"46\" font-size=\"13\" fill=\"#888\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Direct publish: data delivered <tspan fill=\"#5bef7b\" font-weight=\"bold\">instantly</tspan>, next poll skips the key</text><text x=\"375\" y=\"72\" font-size=\"11\" fill=\"#555\" text-anchor=\"middle\" font-family=\"system-ui, sans-serif\">Polling continues as safety net — if a publish is missed, the next cycle catches up</text></g></svg>\n<p><a class=\"\" href=\"https://centrifugal.dev/docs/pro/shared_poll#notification-fast-path\">Centrifugo PRO</a> adds a third option: a <strong>notification fast path</strong>. Your backend sends a lightweight notification — just the channel and the keys that changed, no data — and Centrifugo immediately polls those specific keys from the backend, outside the regular cycle. This is useful when your backend knows <em>which</em> keys changed (e.g., from a database trigger or webhook) but doesn't have the data ready to publish directly. The three tiers — timer polling, notification-triggered polling, direct publish — range from zero integration to instant delivery.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"quick-initial-data-and-reconnect-resilience\">Quick initial data and reconnect resilience<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#quick-initial-data-and-reconnect-resilience\" class=\"hash-link\" aria-label=\"Direct link to Quick initial data and reconnect resilience\" title=\"Direct link to Quick initial data and reconnect resilience\" translate=\"no\">​</a></h2>\n<p>Two design decisions make shared poll feel responsive despite being poll-based.</p>\n<p><strong>Cold key auto-poll.</strong> When a client tracks a key with version 0 (\"I have no data\") and no other connection on the same node is tracking that key, Centrifugo triggers an immediate backend poll for it — without waiting for the next scheduled cycle. Data arrives within milliseconds. This means the first user to view a post gets its vote count almost instantly, and subsequent users on the same node benefit from the cached state. No additional configuration is needed.</p>\n<p><strong>Reconnect resilience.</strong> Centrifugo has always been designed for graceful reconnection — stream subscriptions catch up from history, and the client SDKs handle the entire reconnect lifecycle transparently. Shared poll works the same way. When a client disconnects and reconnects, the SDK automatically replays all tracked keys using the existing signature and sends the last-known version for each key. Centrifugo compares versions and only pushes data that changed while the client was offline — avoiding a full data re-delivery. The <code>getSignature</code> callback is only invoked when the signature actually expires, not on every reconnect — preventing a mass backend request storm when thousands of clients reconnect simultaneously (e.g., after a load balancer restart).</p>\n<p><strong>Publisher-restart resilience.</strong> Versioned mode relies on monotonic versions — but a publisher process that resets its in-memory counter on restart would emit \"stale\" versions and connected clients would freeze on their last-seen state. To handle this, the publisher attaches a per-process <strong>epoch</strong> to each publish and refresh response. When Centrifugo sees the epoch change, it treats the channel as fully reset: per-key versions are wiped, current subscribers are unsubscribed with an insufficient-state code, and SDK auto-resubscribe machinery picks up the new epoch and fresh state — typically within milliseconds. The publisher just needs a fresh string at startup (UUID, a timestamp, anything unique per process lifetime). Empty epoch is also valid and means \"pure version comparison, no restart protection\" — fine when the publisher's lifecycle outlives all subscribers.</p>\n<p>The polling cycle guarantees an <strong>eventually consistent view</strong> of your backend data — clients always converge to the latest state. The delivery tiers above only change how fast they converge, not whether they do. Timer polling alone caps latency at the refresh interval; add direct publish or notifications and updates land in milliseconds. Same correctness, different latency.</p>\n<p>A use case that highlights this well is <strong>configuration sync</strong>. Track a single key like <code>app_settings</code> with a long refresh interval (say, 30 seconds). All connected clients catch up to the current configuration through the regular poll cycle. When an admin changes a setting, your backend calls <code>shared_poll_publish</code> — every client receives the update instantly. New clients connecting later get the configuration immediately via cold key auto-poll. You get configuration distribution with just one tracked key and a publish call on write — no Kafka, no extra infrastructure.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"batch-timing-predictable-backend-load\">Batch timing: predictable backend load<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#batch-timing-predictable-backend-load\" class=\"hash-link\" aria-label=\"Direct link to Batch timing: predictable backend load\" title=\"Direct link to Batch timing: predictable backend load\" translate=\"no\">​</a></h2>\n<p>When a channel tracks thousands of items, polling them all in a single backend request would create periodic load spikes — and Centrifugo has always prioritized <a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy\">protecting backends</a> from thundering herds. Shared poll splits tracked items into batches and spreads them evenly across the polling interval:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Refresh cycle (interval=1s, 3000 tracked keys, batch_size=1000)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"> Clients            Centrifugo                        Backend</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"> ───────            ──────────                        ───────</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │  track(keys)        │                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   ├────────────────────►│                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │  collect all tracked keys     │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │  split into batches           │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │              t=0    │── batch 1 (keys 1-1000) ─────►│</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │            t=333ms  │── batch 2 (keys 1001-2000) ──►│</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │            t=666ms  │── batch 3 (keys 2001-3000) ──►│</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │◄── responses ─────────────────│</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │  compare versions             │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │  per client                   │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │  update(key, data)  │                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │◄────────────────────│  push only changed items      │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │                               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │              t≈1s   │  next cycle starts            │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   │                     │                               │</span><br></div></code></pre></div></div>\n<p>The backend sees steady load rather than a spike every second. A global concurrency limit (default 64) caps the number of simultaneous backend calls across all channels, preventing Centrifugo from overwhelming a shared data source when many channels refresh simultaneously.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"scaling-with-centrifugo-pro\">Scaling with Centrifugo PRO<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#scaling-with-centrifugo-pro\" class=\"hash-link\" aria-label=\"Direct link to Scaling with Centrifugo PRO\" title=\"Direct link to Scaling with Centrifugo PRO\" translate=\"no\">​</a></h2>\n<p>The OSS shape we've described works for most deployments. Three patterns start to bite at higher scale, and <a class=\"\" href=\"https://centrifugal.dev/docs/pro/shared_poll\">Centrifugo PRO</a> addresses each:</p>\n<ul>\n<li class=\"\"><strong>First-paint latency on cold keys.</strong> With long refresh intervals — think a 30-second configuration channel — a new client arriving between cycles waits seconds for first data. PRO's <code>keep_latest_data</code> keeps the latest value and version per key in memory; new clients tracking a known key get data straight from cache, with no backend call. The wait drops from \"until next cycle\" to milliseconds.</li>\n<li class=\"\"><strong>Bandwidth on slow-changing payloads.</strong> When only part of a tracked entry changes each cycle but the whole payload is re-sent, you pay for the unchanged bytes on every fan-out. Paired with <code>keep_latest_data</code>, PRO computes <a class=\"\" href=\"https://centrifugal.dev/docs/server/delta_compression\">fossil deltas</a> between successive values per key — clients that negotiated delta receive the patch instead of the full payload. The savings scale with payload size and how rarely it fully changes.</li>\n<li class=\"\"><strong>Backend load that grows with cluster size.</strong> Each Centrifugo node polls the backend independently — fine at a few nodes, visible at a dozen. The PRO <strong>shared poll relay</strong> is a standalone process that polls once per cycle and serves cached results to all nodes, so backend load stays at one poll per cycle regardless of how many Centrifugo nodes are behind it. The relay also retains version history, providing <code>prev_data</code> for delta compression with no changes on your backend side.</li>\n<li class=\"\"><strong>Cascading failure when the backend lags.</strong> If a refresh cycle takes longer than the configured interval — during a traffic spike, a slow query, a degraded downstream — independent nodes keep stacking calls on top of each other and make recovery harder. PRO's <strong>adaptive backpressure</strong> automatically stretches the interval when responses slow down and shrinks it back as the backend recovers, so a slow backend stays slow but doesn't get pushed over the edge.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"why-this-isnt-just-another-pubsub-feature\">Why this isn't just another PUB/SUB feature<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#why-this-isnt-just-another-pubsub-feature\" class=\"hash-link\" aria-label=\"Direct link to Why this isn't just another PUB/SUB feature\" title=\"Direct link to Why this isn't just another PUB/SUB feature\" translate=\"no\">​</a></h2>\n<p>The server-side polling inversion makes shared poll work for use cases push-only systems can't serve: live counters over legacy databases, configuration sync from third-party APIs, data feeds from systems your team doesn't own. These never really fit a WebSocket server before.</p>\n<p>It depends on Centrifugo being <strong>self-hosted</strong>. The refresh proxy hits your backend over the local network — single-digit milliseconds, not a public-internet round-trip — so per-key HMAC verification stays cheap on the hot path, and you can authorize via the same internal services you already trust. A cloud real-time provider can't poll your backend on your behalf without you exposing it publicly or running a tunnel.</p>\n<p>One practical scaling note: shared poll channels don't use inter-node PUB/SUB. Each node polls the backend independently and delivers to its own connected clients. A cluster running only shared poll needs no Redis or NATS for messaging coordination.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"whats-next\">What's next<a href=\"https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions#whats-next\" class=\"hash-link\" aria-label=\"Direct link to What's next\" title=\"Direct link to What's next\" translate=\"no\">​</a></h2>\n<p>Shared poll subscriptions are currently experimental — we may adjust configuration, client SDK API, and proxy protocol based on feedback. At this point, only <code>centrifuge-js</code> supports shared poll subscriptions on the client side.</p>\n<p>We've published two interactive demos.</p>\n<p><a href=\"https://github.com/centrifugal/examples/tree/master/v6/shared_poll_demo/votes\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Votes</a> — live vote results with dynamic tracking as posts scroll into view:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_votes.mp4\"></video>\n<p><a href=\"https://github.com/centrifugal/examples/tree/master/v6/shared_poll_demo/drones\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Drones</a> — real-time geospatial tracking of 500 simulated drones over a San Francisco map using cell-based spatial partitioning:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/demo_drones.mp4\"></video>\n<p>Read the full <a class=\"\" href=\"https://centrifugal.dev/docs/server/shared_poll\">shared poll documentation</a> for configuration reference, proxy protocol details, and backend signature generation examples in six languages.</p>",
            "url": "https://centrifugal.dev/blog/2026/05/21/shared-poll-subscriptions",
            "title": "Shared poll subscriptions: O(unique items) polling with low-latency updates",
            "summary": "Shared poll subscriptions move polling from clients to Centrifugo. Instead of 10,000 clients each hitting your backend every second, Centrifugo asks once per cycle for the union of items everyone is watching and fans the changes back out.",
            "date_modified": "2026-05-21T10:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "websocket",
                "state-sync",
                "polling"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo",
            "content_html": "<p>In a <a class=\"\" href=\"https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo\">previous post</a> we showed how to stream LLM responses through Centrifugo — backend receives tokens from an LLM API, publishes them to a channel, browser subscribes and renders text as it arrives.</p>\n<p>That post covered the basics. This one looks at what Centrifugo adds beyond just delivering tokens from point A to point B — automatic recovery after disconnects, horizontal scaling, transport flexibility, and multi-tab synchronization backed by a database. We built an interactive playground demo that demonstrates the concepts – you can run it locally and see every feature in action.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"the-playground\">The playground<a href=\"https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo#the-playground\" class=\"hash-link\" aria-label=\"Direct link to The playground\" title=\"Direct link to The playground\" translate=\"no\">​</a></h2>\n<p>The source code is on <a href=\"https://github.com/centrifugal/examples/tree/master/v6/scale-ai\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">GitHub</a>. Run it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">git clone https://github.com/centrifugal/examples.git</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">cd examples/v6/scale-ai</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker compose up --build</span><br></div></code></pre></div></div>\n<p>Open <a href=\"http://localhost:9000/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:9000</a>.</p>\n<p>The playground simulates an AI token stream without requiring an actual LLM API. You control the token rate, total token count, and other parameters. The backend picks a random AI-related question, generates random words as the \"answer\", and publishes them to Centrifugo — the delivery path is identical to a real LLM integration.</p>\n<p>The architecture:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                 ┌─────────────────────┐</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                 │    nginx  :9000     │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                 └──┬───────────────┬──┘</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                    │               │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                    ▼               ▼</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">┌──────────┐   ┌─────────┐   ┌────────────┐   ┌───────┐</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│ postgres │◀──│ backend │──▶│ centrifugo │──▶│ redis │</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">└──────────┘   │  :5000  │   │   :8000    │   └───────┘</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               └─────────┘   └────────────┘</span><br></div></code></pre></div></div>\n<p>Nginx serves the frontend and proxies <code>/api</code> to the backend, <code>/connection</code> and <code>/emulation</code> to Centrifugo. The backend publishes tokens via Centrifugo HTTP API and persists stream state in PostgreSQL. The frontend subscribes to channels using centrifuge-js.</p>\n<p>Let's walk through each feature.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"1-token-streaming-and-aggregation\">1. Token streaming and aggregation<a href=\"https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo#1-token-streaming-and-aggregation\" class=\"hash-link\" aria-label=\"Direct link to 1. Token streaming and aggregation\" title=\"Direct link to 1. Token streaming and aggregation\" translate=\"no\">​</a></h2>\n<p>Start a stream with default settings: 30 tokens/sec, 100 tokens total. Tokens appear in real-time in the output area. The stats bar shows messages and tokens incrementing.</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/scale_ai_baseline.mp4\"></video>\n<p>This is the simplest case — one token per message, one message per publish call to Centrifugo HTTP API:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">publish_to_centrifugo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">str</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">dict</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">await</span><span class=\"token plain\"> http_client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        CENTRIFUGO_API_URL</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        json</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"channel\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"data\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        headers</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"X-API-Key\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> CENTRIFUGO_API_KEY</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>On the client side, the subscription receives each token:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">subscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> msg </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">msg</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">text</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">appendText</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">msg</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">text</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">' '</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>With default settings, 100 tokens = 100 messages. You could achieve this with plain SSE — one endpoint, one event stream. But production AI streaming hits problems that SSE alone doesn't solve: what happens when the connection drops mid-stream? When the user switches networks? When you need to scale beyond one server? When multiple tabs need the same stream? The rest of this post walks through how Centrifugo handles each of these at the infrastructure layer.</p>\n<p>Before moving on — try toggling \"Publisher-side aggregation\" in the controls, set it to 5, and start a new stream.</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/scale_ai_aggregation.mp4\"></video>\n<p>The token count still reaches 100, but the message count drops to ~20. The backend buffers N tokens and publishes them as a batch. At 80 tokens/sec with aggregation of 5, you go from 80 to 16 publish calls per second per stream — fewer syscalls at every layer, and the text still flows naturally. This is a general technique, not Centrifugo-specific, but it composes well with Centrifugo's delivery.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"2-stream-recovery\">2. Stream recovery<a href=\"https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo#2-stream-recovery\" class=\"hash-link\" aria-label=\"Direct link to 2. Stream recovery\" title=\"Direct link to 2. Stream recovery\" translate=\"no\">​</a></h2>\n<p>Start a stream and click \"Simulate Disconnect\" while tokens are flowing. You'll see:</p>\n<ol>\n<li class=\"\"><code>[DISCONNECTED]</code> marker — the client called <code>centrifuge.disconnect()</code></li>\n<li class=\"\">Then 2.5 second gap — tokens keep being published by the backend, but the client isn't connected</li>\n<li class=\"\"><code>[RECONNECTING...]</code> marker — the client calls <code>centrifuge.connect()</code></li>\n<li class=\"\"><code>[RECOVERED]</code> marker — all missed tokens arrive at once, the stream continues</li>\n</ol>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/scale_ai_recovery.mp4\"></video>\n<p>The recovery detection on the client:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">subscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'subscribed'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">wasRecovering</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;&amp;</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">recovered</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">appendMarker</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">' [RECOVERED] '</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'text-green-400'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>This is Centrifugo's <a class=\"\" href=\"https://centrifugal.dev/docs/server/history_and_recovery\">history-based recovery</a>. The channel is configured with <code>history_size: 500</code> and <code>force_recovery: true</code>. When the client reconnects, Centrifugo sends all publications that were missed during the disconnect. The client SDK handles this transparently — the <code>publication</code> event fires for each missed message in order.</p>\n<p>In the playground we call <code>disconnect()</code> and <code>connect()</code> manually to make the gap visible. In a real application you wouldn't do this — Centrifugo SDKs have built-in reconnect with exponential backoff. When the network drops, the client reconnects automatically and recovery happens without any application code involved. The SDK handles the full cycle: detect disconnect, reconnect, re-subscribe, recover missed messages.</p>\n<p>Building this from scratch is a real project: reconnect logic, offset tracking on the client, message buffering on the server, a catch-up protocol. With Centrifugo it's a config option and SDK behavior you get out of the box.</p>\n<p>Mobile networks drop, laptops sleep, WiFi switches. Users won't notice a brief gap if the response continues from where it left off.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"3-redis-for-scaling-and-persistence\">3. Redis for scaling and persistence<a href=\"https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo#3-redis-for-scaling-and-persistence\" class=\"hash-link\" aria-label=\"Direct link to 3. Redis for scaling and persistence\" title=\"Direct link to 3. Redis for scaling and persistence\" translate=\"no\">​</a></h2>\n<p>The playground already runs Centrifugo with Redis as the engine:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"engine\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redis\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"redis\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"address\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redis:6379\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>This is what makes recovery work — channel history is stored in Redis, not in Centrifugo's process memory. If you restart Centrifugo while a stream is active, recovery still works because the history survives in Redis.</p>\n<p>Redis also decouples the real-time layer from the backend. The backend publishes tokens to Centrifugo via HTTP API, then moves on — it doesn't hold WebSocket connections, doesn't track who's listening, doesn't buffer messages. Centrifugo and Redis handle all of that. This means you can scale each layer independently: add more backend instances to handle more concurrent LLM calls, add more Centrifugo nodes to handle more subscribers. They share state through Redis — the backend publishes to any Centrifugo node, Redis routes messages to the node that holds the subscriber's connection.</p>\n<p>The playground uses a single Redis and single Centrifugo instance, but the architecture is ready for horizontal scaling — just add more Centrifugo nodes pointing to the same Redis. In production, Centrifugo supports Redis Cluster and Redis Sentinel for high availability.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"4-transport-fallbacks\">4. Transport fallbacks<a href=\"https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo#4-transport-fallbacks\" class=\"hash-link\" aria-label=\"Direct link to 4. Transport fallbacks\" title=\"Direct link to 4. Transport fallbacks\" translate=\"no\">​</a></h2>\n<p>The playground has a transport selector: WebSocket, HTTP Streaming, SSE. Try starting a stream with each — the token delivery works identically regardless of the transport underneath. If you switch transport mid-stream, recovery kicks in and delivers missed tokens, but the real point is that the application code doesn't change at all.</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/scale_ai_transport.mp4\"></video>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">buildTransports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">type</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">switch</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">type</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'websocket'</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'websocket'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token template-string interpolation\">proto</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token template-string string\" style=\"color:rgb(195, 232, 141)\">//</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token template-string interpolation\">host</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token template-string string\" style=\"color:rgb(195, 232, 141)\">/connection/websocket</span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'http_stream'</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'http_stream'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token template-string interpolation\">httpProto</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token template-string string\" style=\"color:rgb(195, 232, 141)\">//</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token template-string interpolation\">host</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token template-string string\" style=\"color:rgb(195, 232, 141)\">/connection/http_stream</span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'sse'</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'sse'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token template-string interpolation\">httpProto</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token template-string string\" style=\"color:rgb(195, 232, 141)\">//</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token template-string interpolation\">host</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token template-string string\" style=\"color:rgb(195, 232, 141)\">/connection/sse</span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Why this matters: corporate networks often run TLS-intercepting proxies that terminate HTTPS, inspect traffic, and re-encrypt it. These proxies may not forward the HTTP <code>Upgrade</code> handshake that WebSocket requires — even when the original connection is over TLS. If your only transport is WebSocket, those users silently fail to connect. With Centrifugo, you configure SSE and HTTP-streaming as fallbacks. The client SDK can try transports in order and use the first one that connects.</p>\n<p>From the application's perspective, nothing changes — the subscription API, recovery, channel multiplexing all work identically regardless of the underlying transport. The Centrifugo config to enable these:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"sse\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token property\">\"enabled\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"http_stream\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token property\">\"enabled\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>For SSE and HTTP-streaming, Centrifugo uses an emulation layer for the bidirectional part (subscribe/unsubscribe commands). Nginx needs to proxy the <code>/emulation</code> endpoint in addition to <code>/connection</code>.</p>\n<p>A related point worth mentioning: services like OpenAI use SSE for streaming, partly because SSE over HTTP/2 lets multiple streams share a single TCP connection. Centrifugo supports <a class=\"\" href=\"https://centrifugal.dev/docs/transports/websocket#websocket-over-http2-rfc-8441\">WebSocket over HTTP/2 (RFC 8441)</a>, where each WebSocket connection becomes an HTTP/2 stream inside a shared HTTP/2 connection. You get the multiplexing benefits of SSE/HTTP/2 while keeping bidirectional communication and Centrifugo features like recovery.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"5-multi-tab-sync-with-postgresql\">5. Multi-tab sync with PostgreSQL<a href=\"https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo#5-multi-tab-sync-with-postgresql\" class=\"hash-link\" aria-label=\"Direct link to 5. Multi-tab sync with PostgreSQL\" title=\"Direct link to 5. Multi-tab sync with PostgreSQL\" translate=\"no\">​</a></h2>\n<p>The features above work within a single tab's lifecycle. But what happens when a user opens a second tab? Or navigates back to a page while a stream is still running? The tab needs to discover the active stream, catch up on tokens it missed, and continue receiving live updates.</p>\n<p>The playground demonstrates this. Start a stream in one tab, then open a fresh tab — it automatically picks up the same stream, shows accumulated tokens, and continues with live ones.</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/scale_ai_multi_tabs.mp4\"></video>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"how-it-works\">How it works<a href=\"https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo#how-it-works\" class=\"hash-link\" aria-label=\"Direct link to How it works\" title=\"Direct link to How it works\" translate=\"no\">​</a></h3>\n<p>The backend persists each stream in PostgreSQL — the question, accumulated answer, and status. Only two writes happen: an <code>INSERT</code> when the stream starts and an <code>UPDATE</code> when it finishes (with the complete answer and <code>status='done'</code>). No database writes during streaming — Centrifugo handles real-time delivery.</p>\n<p><strong>Discovering streams on page load.</strong> When a tab opens, it calls <code>GET /api/stream/active</code> to find the most recent stream. If the stream is still active, the tab joins it. If it's already done, the tab shows the full question and answer from the database.</p>\n<p><strong>Discovering streams in real-time.</strong> What about a tab that's already open when someone starts a new stream? Every tab subscribes to an <code>ai:notifications</code> channel. The backend publishes a notification there when a stream starts:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">await</span><span class=\"token plain\"> publish_to_centrifugo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"ai:notifications\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"type\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"stream_started\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"id\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> stream_id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"channel\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"question\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> question</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>When a tab receives this notification, it joins the new stream immediately — no polling, no page reload.</p>\n<p><strong>Subscribing with catch-up.</strong> Every tab — including the one that started the stream — subscribes to the stream's channel with <code>since: {offset: 0, epoch: ''}</code>. This tells Centrifugo to deliver all existing channel history through the normal recovery flow, then continue with live publications — all through the same <code>publication</code> handler, in order:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> opts </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">since</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">offset</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">epoch</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">''</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">subscription </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> opts</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">subscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// History and live publications arrive through the same handler, in order.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">processPublication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>No client-side buffering, no deduplication logic. Centrifugo's recovery mechanism handles the sequencing. This also eliminates the race condition between starting the stream and subscribing — even if the backend publishes tokens before the client subscribes, they arrive through history recovery.</p>\n<p>Building this from scratch means implementing ordered pub/sub with offset tracking, fan-out to multiple subscribers, and a protocol to distinguish history replay from live delivery. Centrifugo provides all of these as built-in primitives.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"the-pattern\">The pattern<a href=\"https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo#the-pattern\" class=\"hash-link\" aria-label=\"Direct link to The pattern\" title=\"Direct link to The pattern\" translate=\"no\">​</a></h3>\n<p>This is a realistic pattern for combining a database with real-time delivery:</p>\n<ul>\n<li class=\"\"><strong>PostgreSQL</strong> is the source of truth for completed conversations — query it to show past streams, build dashboards, or feed analytics.</li>\n<li class=\"\"><strong>Centrifugo</strong> handles live token delivery and short-term history for mid-stream catch-up.</li>\n<li class=\"\"><strong>The backend stays stateless</strong> — it writes to PG and publishes to Centrifugo, it doesn't hold client connections or track who is listening.</li>\n</ul>\n<p>A new tab can appear at any point during a stream, catch up seamlessly, and continue with live tokens. This works because Centrifugo's core operation is fan-out: publish once, deliver to all subscribers on the channel. No extra work on the backend, no tracking of which tabs or devices are connected.</p>\n<p>Practical cases: a user starts a conversation on desktop, picks up the phone, the response keeps streaming. Or a customer support scenario where an AI suggests responses and multiple agents see the same stream. These are all just N subscribers on one channel. And since Centrifugo supports channel multiplexing, the AI stream, a notifications feed, and presence indicators can all share a single WebSocket connection — each additional subscription is virtually free.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>Each of these features solves a real problem in AI streaming:</p>\n<ul>\n<li class=\"\"><strong>Recovery</strong> handles the inevitable network disruptions without application-level complexity</li>\n<li class=\"\"><strong>Redis</strong> decouples the real-time layer, enables horizontal scaling, and persists history</li>\n<li class=\"\"><strong>Transport fallbacks</strong> ensure the service works in restricted network environments</li>\n<li class=\"\"><strong>Multi-tab sync with PostgreSQL</strong> shows how Centrifugo pairs with a database — PG for durable state, Centrifugo for real-time delivery and catch-up</li>\n</ul>\n<p>None of these are AI-specific features — they're general real-time infrastructure capabilities that happen to be exactly what AI token streaming needs. Implementing them from scratch is significant engineering work: retry logic, offset tracking, message buffering, transport negotiation, fan-out routing. Centrifugo provides them as a single infrastructure layer.</p>",
            "url": "https://centrifugal.dev/blog/2026/03/01/scaling-ai-token-streams-with-centrifugo",
            "title": "Scaling AI token streams with Centrifugo",
            "summary": "An interactive demo playground demonstrating what Centrifugo brings to AI token streaming — channel multiplexing, recovery, multi-tab sync, transport fallbacks, and horizontal scaling with Redis.",
            "date_modified": "2026-03-01T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "ai",
                "streaming",
                "tutorial",
                "websocket",
                "sse"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags",
            "content_html": "<p>Real-time applications often face the challenge of delivering relevant content to subscribers while minimizing bandwidth usage and client-side processing overhead. Recently introduced <a class=\"\" href=\"https://centrifugal.dev/docs/server/publication_filtering\">publication filtering by tags</a> in Centrifugo OSS and Centrifugo PRO addresses this challenge from a new side – by allowing clients to subscribe to channels with server-side filters, ensuring that only publications with matching tags are delivered to subscribers.</p>\n<img src=\"https://centrifugal.dev/img/publication_filtering.png\">\n<br>\n<br>\n<p>This feature may significantly help with bandwidth optimization for real-time messaging applications, particularly in scenarios where clients would otherwise receive and discard a significant portion of messages in a channel anyway. Not only network costs may be reduced in this case, but also processing overhead which leads to a faster battery drain on mobile devices.</p>\n<p>During last months we observed the increased interest in this feature from Centrifugo users, so it was eventually implemented in Centrifugo v6.4.0. In this blog post we will discuss the design goals, implementation decisions, and performance benchmarks that led to the final solution.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"design-goals\">Design Goals<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#design-goals\" class=\"hash-link\" aria-label=\"Direct link to Design Goals\" title=\"Direct link to Design Goals\" translate=\"no\">​</a></h2>\n<p>When designing publication filtering for Centrifugo, we established several key principles:</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"zero-allocation-performance-in-hot-broadcast-path\">Zero-Allocation Performance in hot broadcast path<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#zero-allocation-performance-in-hot-broadcast-path\" class=\"hash-link\" aria-label=\"Direct link to Zero-Allocation Performance in hot broadcast path\" title=\"Direct link to Zero-Allocation Performance in hot broadcast path\" translate=\"no\">​</a></h4>\n<p>The filtering mechanism must be zero-allocation during evaluation because it operates in the hot path during broadcasts to many subscribers. Any memory allocations during filtering would significantly impact performance and increase garbage collection pressure.</p>\n<p>While for most applications it may be fine, the predictability of filtering overhead decreases if evaluation allocates. We will see in the benchmarks below how many allocations may be caused by a single publication in channel with 10k subscribers.</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"protocol-compatibility\">Protocol Compatibility<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#protocol-compatibility\" class=\"hash-link\" aria-label=\"Direct link to Protocol Compatibility\" title=\"Direct link to Protocol Compatibility\" translate=\"no\">​</a></h4>\n<p>Filters must be easy to serialize/deserialize to/from Protobuf and be fully JSON compatible, ensuring seamless integration with existing client SDKs and Centrifugo protocol.</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"programmatic-construction\">Programmatic Construction<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#programmatic-construction\" class=\"hash-link\" aria-label=\"Direct link to Programmatic Construction\" title=\"Direct link to Programmatic Construction\" translate=\"no\">​</a></h4>\n<p>The filtering system should be easily constructible programmatically, allowing developers to build dynamic filters based on application conditions without the need in string formatting and templating.</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"simplicity-and-security\">Simplicity and Security<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#simplicity-and-security\" class=\"hash-link\" aria-label=\"Direct link to Simplicity and Security\" title=\"Direct link to Simplicity and Security\" translate=\"no\">​</a></h4>\n<p>The implementation should remain simple enough and avoid complexity that could limit adoption or introduce security vulnerabilities. The filtering system should only filter based on data that subscribers can already see in publications, ensuring no security boundaries are crossed.</p>\n<p>Centrifugo ensures permissions on channel level, filters are not adding a new layer of data protection. Subscriber in channel is able to read all the publications in that stream – filters do not add any permission functionality here.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"implementation-decision-filter-by-publication-tags\">Implementation decision: filter by Publication tags<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#implementation-decision-filter-by-publication-tags\" class=\"hash-link\" aria-label=\"Direct link to Implementation decision: filter by Publication tags\" title=\"Direct link to Implementation decision: filter by Publication tags\" translate=\"no\">​</a></h2>\n<p>To implement publication filtering, we decided to use tags associated with each publication. Tags are key-value pairs that can be attached to publication:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">message Publication </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  bytes data </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">4</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Data contains publication payload.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"> tags </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">7</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Optional tags associated with publication.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>It's important that <code>tags</code> are already part of the client protocol. Each channel subscriber has access to data and tags of Publication already. So we do not introduce any new security boundaries here.</p>\n<p>For examples in this post, let's consider scenario where a channel represents event stream with football match updates. Stream may look like this:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Publication 1</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"data\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"23.27\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"event_type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"possession_change\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"event_data\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"team\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Real Madrid\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"tackler\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Arda Guler\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"tags\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"event_type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"possession_change\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Publication 2</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"data\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"23.30\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"event_type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"goal\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"event_data\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"team\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Real Madrid\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"scorer\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Kilian Mbappe\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"assistant\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Arda Guler\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"tags\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"event_type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"goal\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Publication 3</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"data\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"24.10\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"event_type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"shot\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"event_data\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"team\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Bayern Munich\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"shooter\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Harry Kane\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"xG\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"0.85\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"outcome\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"saved\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"tags\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"event_type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"shot\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"xG\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"0.85\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>We can suppose that user does not need all the match events. Some basic filtering example may be: user is only interested in <code>\"goal\"</code> type events.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"implementation-decision-cel-vs-custom-filters\">Implementation Decision: CEL vs custom filters<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#implementation-decision-cel-vs-custom-filters\" class=\"hash-link\" aria-label=\"Direct link to Implementation Decision: CEL vs custom filters\" title=\"Direct link to Implementation Decision: CEL vs custom filters\" translate=\"no\">​</a></h2>\n<p>Initially, we considered using Google's <a href=\"https://github.com/google/cel-go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Common Expression Language</a> (CEL) for filtering, which would have provided a familiar and powerful expression syntax. We already use CEL in other parts of Centrifugo, so it seemed a very natural decision initially. For the scenario above the filter could look like:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">tags[\"event_type\"] == \"goal\"</span><br></div></code></pre></div></div>\n<p>Seems simple and straightforward, right? True, and we were quite enthusiastic about using CEL, but the devil is in the details. Turned out for this specific part of Centrifugo it was not the best choice.</p>\n<p>First thing to mention, on every subscription we need to compile the CEL expression to a program. Compilation is not a very cheap operation. Here is how compilation may look like with the <code>cel-go</code> library:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">buildCELProgram</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">expr </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">cel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Program</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  env</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> cel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewEnv</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    cel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Variable</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"tags\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">MapType</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">cel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">StringType</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">StringType</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  ast</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> issues </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> env</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Compile</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">expr</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> issues </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;&amp;</span><span class=\"token plain\"> issues</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> issues</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> ast</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">OutputType</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> cel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">BoolType </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> errors</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"expected bool output type\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> env</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Program</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ast</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Build and use the program against tags.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">prg</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">buildCELProgram</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`tags[\"event_type\"] == \"goal\"`</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Handle err.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">activation </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">any</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"tags\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> tags</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">out</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Eval</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">activation</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Handle err.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> out </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">==</span><span class=\"token plain\"> types</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">True </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Match!</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>An environment made by <code>cel.NewEnv</code> call may be shared between subscriptions, but parsing expression and checking AST must be done per each subscription. We will see the overhead for this below in benchmarks.</p>\n<p>For a simple expression like <code>tags[\"event_type\"] == \"goal\"</code>:</p>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>CEL</td><td>41,606</td><td>28,581</td><td>23,178</td><td>326</td></tr></tbody></table>\n<p>Or with a more complex expression like <code>int(tags[\"count\"]) &gt; 42 &amp;&amp; double(tags[\"price\"]) &gt;= 99.5 &amp;&amp; tags[\"ticker\"].contains(\"GOO\")</code>:</p>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>CEL</td><td>13,784</td><td>86,623</td><td>67,560</td><td>1,060</td></tr></tbody></table>\n<p>Which is 3x more.</p>\n<p>Subscription requests are often frequent operation in Centrifugo, so seeing these numbers is of course unfortunate. Possible optimization here could be caching the same expressions and re-using CEL programs, adds some complexity – but feasible.</p>\n<p>More importantly though. While compiled CEL expressions are rather fast to evaluate – evaluations of even simple CEL expressions still come with memory allocations. Memory allocations directly add CPU overhead and also create more load on Garbage Collector (GC). These allocations may not be a huge problem in other parts, but during publication broadcast process it's a very unpredictable performance overhead.</p>\n<p>In Centrifugo broadcast process prepares a Publication once and then just adds it to each subscriber queue with minimal allocations during this process. Any allocations per each subscriber in that place in channels with many subscribers can be a performance killer. Another place where we would like to not sacrifice the performance is automatic Publication recovery Centrifugo feature. Recovery is already not a very cheap process to do, so any additional overhead caused by filtering is not a good thing.</p>\n<p>Let's look at evaluating CEL program with expression like <code>tags[\"event_type\"] == \"goal\"</code>:</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"single-evaluation\">Single Evaluation<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#single-evaluation\" class=\"hash-link\" aria-label=\"Direct link to Single Evaluation\" title=\"Direct link to Single Evaluation\" translate=\"no\">​</a></h4>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>CEL</td><td>13,632,322</td><td>79.70</td><td>32</td><td>2</td></tr></tbody></table>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"10k-subscribers-massive-broadcast-simulation\">10k Subscribers (massive broadcast simulation)<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#10k-subscribers-massive-broadcast-simulation\" class=\"hash-link\" aria-label=\"Direct link to 10k Subscribers (massive broadcast simulation)\" title=\"Direct link to 10k Subscribers (massive broadcast simulation)\" translate=\"no\">​</a></h4>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>CEL</td><td>1,490</td><td>791,854</td><td>320,147</td><td>20,000</td></tr></tbody></table>\n<p>Or with more complex expression like <code>int(tags[\"count\"]) &gt; 42 &amp;&amp; double(tags[\"price\"]) &gt;= 99.5 &amp;&amp; tags[\"ticker\"].contains(\"GOO\")</code>:</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"single-evaluation-1\">Single Evaluation<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#single-evaluation-1\" class=\"hash-link\" aria-label=\"Direct link to Single Evaluation\" title=\"Direct link to Single Evaluation\" translate=\"no\">​</a></h4>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>CEL</td><td>3,579,276</td><td>333.5</td><td>104</td><td>7</td></tr></tbody></table>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"10k-subscribers-massive-broadcast-simulation-1\">10k Subscribers (massive broadcast simulation)<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#10k-subscribers-massive-broadcast-simulation-1\" class=\"hash-link\" aria-label=\"Direct link to 10k Subscribers (massive broadcast simulation)\" title=\"Direct link to 10k Subscribers (massive broadcast simulation)\" translate=\"no\">​</a></h4>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>CEL</td><td>354</td><td>3,366,629</td><td>1,040,445</td><td>70,000</td></tr></tbody></table>\n<p>One more concern against the design goals above is that CEL expressions do not offer programmatic construction. It's just a string. This, while OK for other cases like server-side configurations, does not fit well client applications where filter often must be built programmatically from the application state.</p>\n<p>Mostly due to this couple of reasons we decided to implement a custom filtering system based on a tree of nodes with logical and comparison operators. This approach is not as flexible as CEL, but it covers most common filtering use cases while providing zero-allocation performance during evaluation and programmatic construction capabilities. Let's look at the chosen design more closely.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"filternode-design\">FilterNode design<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#filternode-design\" class=\"hash-link\" aria-label=\"Direct link to FilterNode design\" title=\"Direct link to FilterNode design\" translate=\"no\">​</a></h2>\n<p>The filtering system in Centrifugo is based on a tree structure using <code>FilterNode</code> object defined in the client protocol Protobuf schema:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">message FilterNode </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Operation type:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// - If not set or empty → leaf node (comparison)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// - \"and\" → logical AND of child nodes</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// - \"or\" → logical OR of child nodes</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// - \"not\" → logical NOT of single child</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"> op </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Key for comparison (leaf nodes only)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"> key </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">2</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Comparison operator for leaf nodes</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// \"eq\", \"neq\", \"in\", \"nin\", \"ex\", \"nex\",</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// \"sw\", \"ew\", \"ct\", \"gt\", \"gte\", \"lt\", \"lte\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"> cmp </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">3</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Single value for most comparisons</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"> val </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">4</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Multiple values for set operations</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  repeated </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"> vals </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">5</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Child nodes for logical operations</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  repeated FilterNode nodes </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">6</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>This structure may be passed in subscribe request to Centrifugo, it's already part of Protobuf schema, so no additional parsing is required on server side to build the filter tree since it comes ready – only a very fast zero-allocation validation step is performed.</p>\n<p>It's also fully compatible with Centrifugo optimized JSON serialization (as a general rule, we avoid using <code>enums</code> and <code>oneof</code> in the Protobuf schema due to that).</p>\n<p>The filter is attached to a Subscription and then applied to <code>tags</code> in every channel Publication in a channel during broadcast.</p>\n<p>Filter supports a comprehensive set of comparison and logical operators.</p>\n<ul>\n<li class=\"\"><strong>eq/neq</strong>: for exact equality and inequality checks</li>\n<li class=\"\"><strong>sw/ew/ct</strong>: to perform string starts with, ends with, contains comparisons</li>\n<li class=\"\"><strong>in/nin</strong>: to make set membership operations</li>\n<li class=\"\"><strong>gt/gte/lt/lte</strong>: for numeric comparisons with automatic type coercion using zero-allocation decimal library (<a href=\"https://github.com/quagmt/udecimal\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">quagmt/udecimal</a>)</li>\n<li class=\"\"><strong>ex/nex</strong>: for key existence and non-existence checks</li>\n</ul>\n<p>More complex filtering with nested conditions may be built using logical operations: <strong>and</strong>, <strong>or</strong>, <strong>not</strong>.</p>\n<p>And the implementation of evaluation is a simple recursive function with zero allocations during evaluation. Here is a simplified version of it (with only one logical operator and a couple of comparisons, full code with all the above's features may be found <a href=\"https://github.com/centrifugal/centrifuge/blob/master/internal/filter/filter.go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">on Github</a>)</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Match</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">f </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">FilterNode</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> tags </span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">bool</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">switch</span><span class=\"token plain\"> f</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Op </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> OpLeaf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    val</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ok </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> tags</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">f</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">switch</span><span class=\"token plain\"> f</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Cmp </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> CompareEQ</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> ok </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;&amp;</span><span class=\"token plain\"> val </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">==</span><span class=\"token plain\"> f</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Val</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> CompareExists</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> ok</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> OpAnd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> child </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">range</span><span class=\"token plain\"> f</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Nodes </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Match</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">child</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> tags</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">false</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">false</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Overall, this structure allows rather powerful filtering capabilities while maintaining zero-allocation evaluation performance in hot path.</p>\n<p>This is how filtering of football match events by <code>event_type</code> tag may look like with FilterNode structure:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> tagsFilter </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">key</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"event_type\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"eq\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">val</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"goal\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"match_events:match_id\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">tagsFilter</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> basicFilter</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>More complex filters may be built using logical operators:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> tagsFilter </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">op</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"or\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">nodes</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">key</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"event_type\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"eq\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">val</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"goal\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token literal-property property\">op</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"and\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token literal-property property\">nodes</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">key</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"event_type\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"eq\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">val</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"shot\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">key</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"xG\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"gte\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">val</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"0.8\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"match_events:match_id\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">tagsFilter</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> tagsFilter</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>It's possible to build filters programmatically using helper functions to ensure type safety and reduce boilerplate. For example, for JavaScript we may have:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> </span><span class=\"token maybe-class-name\">Filter</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Comparison operators.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">eq</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> val</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"eq\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> val </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">neq</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> val</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"neq\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> val </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">in</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> vals</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"in\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> vals </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">nin</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> vals</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"nin\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> vals </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">exists</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"ex\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">notExists</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"nex\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">startsWith</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> val</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"sw\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> val </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">endsWith</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> val</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"ew\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> val </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">contains</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> val</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"ct\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> val </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">gt</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> val</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"gt\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> val </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">gte</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> val</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"gte\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> val </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">lt</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> val</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"lt\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> val </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">lte</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">key</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> val</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">cmp</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"lte\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> val </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Logical operators.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">and</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token parameter\">nodes</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">op</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"and\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> nodes </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">or</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token parameter\">nodes</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">op</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"or\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> nodes </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">not</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">node</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">op</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"not\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">nodes</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">node</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Same filter using helper.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> tagsFilter </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token maybe-class-name\">Filter</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">or</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token maybe-class-name\">Filter</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">eq</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"event_type\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"goal\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token maybe-class-name\">Filter</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">and</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token maybe-class-name\">Filter</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">eq</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"event_type\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"shot\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token maybe-class-name\">Filter</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">gte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"xG\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"0.8\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"match_events:match_id\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">tagsFilter</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> tagsFilter</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Someone can go further and build a string expression parser to build FilterNode tree from string representation similar to CEL, but we did not see a strong need for that so far – leaving as an exercise for the developers.</p>\n<p>With the described approach it seems that we found a good balance between flexibility, performance, and simplicity – meeting the design goals.</p>\n<p>Now let's see at some benchmarks we did to compare this custom approach with CEL.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"performance-comparison-against-cel\">Performance Comparison against CEL<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#performance-comparison-against-cel\" class=\"hash-link\" aria-label=\"Direct link to Performance Comparison against CEL\" title=\"Direct link to Performance Comparison against CEL\" translate=\"no\">​</a></h2>\n<p>We ran a series of benchmarks comparing <strong>FilterNode</strong> against CEL. The tests covered both <strong>simple expression</strong> and <strong>more complex expression</strong> used earlier, measuring three scenarios:</p>\n<ul>\n<li class=\"\"><strong>BenchmarkCompareCompile</strong> – overhead of compiling the expression (relevant at subscription time).</li>\n<li class=\"\"><strong>BenchmarkCompare</strong> – a single evaluation of the filter.</li>\n<li class=\"\"><strong>BenchmarkCompare10k</strong> – 10,000 evaluations (simulating overhead during broadcast to 10k subscribers).</li>\n</ul>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"simple-expression\">Simple Expression<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#simple-expression\" class=\"hash-link\" aria-label=\"Direct link to Simple Expression\" title=\"Direct link to Simple Expression\" translate=\"no\">​</a></h3>\n<p><code>tags[\"event_type\"] == \"goal\"</code></p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"compilation-overhead-at-subscription-time\">Compilation Overhead (at subscription time)<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#compilation-overhead-at-subscription-time\" class=\"hash-link\" aria-label=\"Direct link to Compilation Overhead (at subscription time)\" title=\"Direct link to Compilation Overhead (at subscription time)\" translate=\"no\">​</a></h4>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>FilterNode</td><td>11,606,757</td><td>103.5</td><td>328</td><td>3</td></tr><tr><td>CEL</td><td>41,605</td><td>28,683</td><td>23,164</td><td>326</td></tr></tbody></table>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"single-evaluation-2\">Single Evaluation<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#single-evaluation-2\" class=\"hash-link\" aria-label=\"Direct link to Single Evaluation\" title=\"Direct link to Single Evaluation\" translate=\"no\">​</a></h4>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>FilterNode</td><td>102,141,284</td><td>11.69</td><td>0</td><td>0</td></tr><tr><td>CEL</td><td>14,766,996</td><td>78.96</td><td>32</td><td>2</td></tr></tbody></table>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"10k-evaluations-massive-broadcast-simulation\">10k Evaluations (massive broadcast simulation)<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#10k-evaluations-massive-broadcast-simulation\" class=\"hash-link\" aria-label=\"Direct link to 10k Evaluations (massive broadcast simulation)\" title=\"Direct link to 10k Evaluations (massive broadcast simulation)\" translate=\"no\">​</a></h4>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>FilterNode</td><td>10,000</td><td>112,080</td><td>0</td><td>0</td></tr><tr><td>CEL</td><td>1,480</td><td>794,553</td><td>320,135</td><td>20,000</td></tr></tbody></table>\n<p><strong>Analysis:</strong><br>\n<!-- -->FilterNode compiles nearly <strong>280x faster</strong> than CEL – which is obvious because it's just a matter of several struct creations. Once compiled, executes about <strong>7x faster per evaluation</strong> with zero allocations. At scale (10k evals), it is roughly <strong>7x faster</strong> and completely allocation-free, whereas CEL incurs significant heap usage.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"complex-expression\">Complex Expression<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#complex-expression\" class=\"hash-link\" aria-label=\"Direct link to Complex Expression\" title=\"Direct link to Complex Expression\" translate=\"no\">​</a></h3>\n<p><code>int(tags[\"count\"]) &gt; 42 &amp;&amp; double(tags[\"price\"]) &gt;= 99.5 &amp;&amp; tags[\"ticker\"].contains(\"GOO\")</code></p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"compilation-overhead-at-subscription-time-1\">Compilation Overhead (at subscription time)<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#compilation-overhead-at-subscription-time-1\" class=\"hash-link\" aria-label=\"Direct link to Compilation Overhead (at subscription time)\" title=\"Direct link to Compilation Overhead (at subscription time)\" translate=\"no\">​</a></h4>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>FilterNode</td><td>5,815,352</td><td>206.8</td><td>664</td><td>5</td></tr><tr><td>CEL</td><td>13,548</td><td>88,699</td><td>67,502</td><td>1,060</td></tr></tbody></table>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"single-evaluation-3\">Single Evaluation<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#single-evaluation-3\" class=\"hash-link\" aria-label=\"Direct link to Single Evaluation\" title=\"Direct link to Single Evaluation\" translate=\"no\">​</a></h4>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>FilterNode</td><td>11,372,026</td><td>93.78</td><td>0</td><td>0</td></tr><tr><td>CEL</td><td>3,609,756</td><td>333.8</td><td>104</td><td>7</td></tr></tbody></table>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"10k-evaluations-massive-broadcast-simulation-1\">10k Evaluations (massive broadcast simulation)<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#10k-evaluations-massive-broadcast-simulation-1\" class=\"hash-link\" aria-label=\"Direct link to 10k Evaluations (massive broadcast simulation)\" title=\"Direct link to 10k Evaluations (massive broadcast simulation)\" translate=\"no\">​</a></h4>\n<table><thead><tr><th>Benchmark Name</th><th>Iterations</th><th>ns/op</th><th>B/op</th><th>allocs/op</th></tr></thead><tbody><tr><td>FilterNode</td><td>1,290</td><td>923,829</td><td>0</td><td>0</td></tr><tr><td>CEL</td><td>356</td><td>3,350,432</td><td>1,040,445</td><td>70,000</td></tr></tbody></table>\n<p><strong>Analysis:</strong><br>\n<!-- -->For more complex expressions, FilterNode is even stronger in comparison. Compilation is <strong>~430x faster</strong> than CEL, and evaluation is <strong>3–4x faster</strong>. Under load (10k evals), FilterNode is nearly <strong>4x faster</strong> and entirely allocation-free, while CEL creates GC pressure.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"takeaways\">Takeaways<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#takeaways\" class=\"hash-link\" aria-label=\"Direct link to Takeaways\" title=\"Direct link to Takeaways\" translate=\"no\">​</a></h3>\n<ul>\n<li class=\"\"><strong>Compile-time overhead:</strong> FilterNode compiles <strong>hundreds of times faster</strong> than CEL with small number of allocations. This matters in scenarios like channel subscription where expressions are frequently registered.</li>\n<li class=\"\"><strong>Evaluation speed:</strong> FilterNode executes <strong>3–7x faster</strong> than CEL, depending on complexity the difference may be even more.</li>\n<li class=\"\"><strong>Scalability:</strong> FilterNode provides <strong>zero allocations per evaluation</strong>, making it extremely GC-friendly at scale. CEL, in contrast, allocates memory on every evaluation. In broadcast scenarios with thousands of subscribers, FilterNode avoids the GC overhead and scales predictably, while CEL quickly incurs both time and memory costs.</li>\n</ul>\n<p>FilterNode approach delivers <strong>order-of-magnitude improvements</strong> in both latency and memory efficiency compared to CEL, especially under high-throughput workloads like subscription broadcasts.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"demo\">Demo<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#demo\" class=\"hash-link\" aria-label=\"Direct link to Demo\" title=\"Direct link to Demo\" translate=\"no\">​</a></h2>\n<p>Here is how it may look like in real life with Centrifugo:</p>\n<video width=\"100%\" loop=\"\" muted=\"\" controls=\"\" src=\"/img/blog_filter_by_tags_demo.mp4\"></video>\n<p>You can see a simple browser app which subscribes to a channel with stock price ticks with a filter on <code>ticker</code> tag. After that, client only receives messages matching the filter. You can find the client side source code <a href=\"https://github.com/centrifugal/centrifuge/blob/c2caf7f4ef0dbc64689ddab438169b419693a11c/_examples/tags_filter/index.html#L226\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">in Centrifuge Go lib examples</a>.</p>\n<p>In this example we're also demonstrating the change of filter on the fly by unsubscribing and subscribing again with a different filter. Also note that offsets of messages are not incremental here because of filtering – client only receives messages matching the filter.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>For applications dealing with high-volume channels where clients need only a subset of messages, publication filtering offers a compelling solution that optimizes both network usage and client-side processing overhead. This is particularly beneficial for mobile clients where battery life and data usage are critical.</p>\n<p>The custom implementation, while less powerful than expression languages like CEL, provides the exact functionality needed for real-time filtering scenarios while maintaining the performance characteristics essential for high-throughput applications.</p>\n<p>Note, there was no goal here to say that CEL is bad – no, it's an awesome tool for many use cases including existing usages in Centrifugo. Just for this specific case of publication filtering in Centrifugo we needed something more performance predictable and programmable.</p>\n<p>The filtering by tags is available starting from Centrifugo v6.4.0, with <a class=\"\" href=\"https://centrifugal.dev/docs/server/publication_filtering\">the documentation available</a>. For now, it's supported only in Javascript SDK, but we plan to add support in other SDKs too depending on demand.</p>",
            "url": "https://centrifugal.dev/blog/2025/10/14/server-side-publication-filtering-by-tags",
            "title": "Publication filtering by tags - reducing bandwidth with server-side stream filtering",
            "summary": "Learn how Centrifugo's new publication filtering feature allows clients to subscribe with server-side tag filters, significantly reducing bandwidth usage and client-side processing overhead through efficient zero-allocation filtering.",
            "date_modified": "2025-10-14T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "performance",
                "go"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo",
            "content_html": "<p>Centrifugo may be used as an efficient and scalable transport for streaming AI responses. In this article, we will stream GPT-3.5 Turbo responses in real-time using Centrifugo temporary channels and Python. We will use OpenAI API to get the answers to user's prompts and stream them to the user using Centrifugo. The user will be able to see the response as it is being generated, similar to how ChatGPT works.</p>\n<p>Here is a video of the final result:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/gpt.mp4\"></video>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"source-code\">Source code<a href=\"https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo#source-code\" class=\"hash-link\" aria-label=\"Direct link to Source code\" title=\"Direct link to Source code\" translate=\"no\">​</a></h2>\n<p>The source code of this example is available on <a href=\"https://github.com/centrifugal/examples/tree/master/v6/gpt-stream\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">GitHub</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"-tech-stack\">🧰 Tech Stack<a href=\"https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo#-tech-stack\" class=\"hash-link\" aria-label=\"Direct link to 🧰 Tech Stack\" title=\"Direct link to 🧰 Tech Stack\" translate=\"no\">​</a></h2>\n<p>In this example, we will use the following technologies:</p>\n<ul>\n<li class=\"\"><a href=\"https://fastapi.tiangolo.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastAPI</a> – async backend in Python which is good for streaming.</li>\n<li class=\"\"><strong>Centrifugo</strong> – will be used as transport for streaming responses to web clients.</li>\n<li class=\"\"><a href=\"https://openai.com/api/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">OpenAI API</a> – LLM responses (via GPT-3.5 Turbo is used in the example).</li>\n<li class=\"\">Some <a href=\"https://tailwindcss.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Tailwind CSS</a> for styling.</li>\n<li class=\"\"><a href=\"https://nginx.org/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Nginx</a> as a reverse proxy to serve the frontend and route API requests to the backend.</li>\n<li class=\"\"><a href=\"https://docs.docker.com/compose/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docker Compose</a> to run everything with a single command.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"backend\">Backend<a href=\"https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo#backend\" class=\"hash-link\" aria-label=\"Direct link to Backend\" title=\"Direct link to Backend\" translate=\"no\">​</a></h2>\n<p>We will build the backend using <a href=\"https://fastapi.tiangolo.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FastAPI</a> - which is a modern web framework for building APIs with Python. It is easy to use, and has great support for asynchronous programming, which is perfect for streaming responses.</p>\n<p>The entire backend app is about 70 lines of code only:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> fastapi </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> FastAPI</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> pydantic </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> BaseModel</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> openai </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> OpenAI</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> httpx</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> os</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> FastAPI</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">client </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> OpenAI</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">api_key</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\">os</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">getenv</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"OPENAI_API_KEY\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_HTTP_API_URL </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://centrifugo:8000/api\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_HTTP_API_KEY </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"secret\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">BaseModel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    text</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:rgb(199, 146, 234)\">@app</span><span class=\"token decorator annotation punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token decorator annotation punctuation\" style=\"color:rgb(199, 146, 234)\">post</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"/api/execute\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">api_execute</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">cmd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">await</span><span class=\"token plain\"> handle_command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">cmd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">class</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">StreamMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">BaseModel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    text</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">str</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    done</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">bool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">handle_command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">cmd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> Command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    text </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> cmd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">text</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    channel </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> cmd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">channel</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">try</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        response </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">chat</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">completions</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">create</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            model</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"gpt-3.5-turbo\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            messages</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"role\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"user\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"content\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            stream</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">True</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> chunk </span><span class=\"token keyword\" style=\"font-style:italic\">in</span><span class=\"token plain\"> response</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            token </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> chunk</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">choices</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">delta</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">content </span><span class=\"token keyword\" style=\"font-style:italic\">or</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token keyword\" style=\"font-style:italic\">await</span><span class=\"token plain\"> publish_message</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                    channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                    StreamMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">text</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> done</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">False</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">model_dump</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">await</span><span class=\"token plain\"> publish_message</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            StreamMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">text</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> done</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">True</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">model_dump</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">except</span><span class=\"token plain\"> Exception </span><span class=\"token keyword\" style=\"font-style:italic\">as</span><span class=\"token plain\"> e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">await</span><span class=\"token plain\"> publish_message</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            StreamMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">text</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string-interpolation string\" style=\"color:rgb(195, 232, 141)\">f\"⚠️ Error: </span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string-interpolation interpolation\">e</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token string-interpolation string\" style=\"color:rgb(195, 232, 141)\">\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> done</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">True</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">model_dump</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">publish_message</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> stream_message</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    payload </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"channel\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"data\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> stream_message</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    headers </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"X-API-Key\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string-interpolation string\" style=\"color:rgb(195, 232, 141)\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string-interpolation interpolation\">CENTRIFUGO_HTTP_API_KEY</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token string-interpolation string\" style=\"color:rgb(195, 232, 141)\">\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Content-Type\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"application/json\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">with</span><span class=\"token plain\"> httpx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">AsyncClient</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">as</span><span class=\"token plain\"> http_client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">await</span><span class=\"token plain\"> http_client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">post</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token string-interpolation string\" style=\"color:rgb(195, 232, 141)\">f\"</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string-interpolation interpolation\">CENTRIFUGO_HTTP_API_URL</span><span class=\"token string-interpolation interpolation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token string-interpolation string\" style=\"color:rgb(195, 232, 141)\">/publish\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> json</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\">payload</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> headers</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\">headers</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>Let's go through the code step by step:</p>\n<ol>\n<li class=\"\"><strong>FastAPI Setup</strong>: We create a FastAPI application instance.</li>\n<li class=\"\"><strong>OpenAI Client</strong>: We initialize the OpenAI client with the API key from environment variables.</li>\n<li class=\"\"><strong>Command Model</strong>: We define a Pydantic model <code>Command</code> to validate incoming requests with <code>text</code> and <code>channel</code> fields.</li>\n<li class=\"\"><strong>API Endpoint</strong>: We create an endpoint <code>/api/execute</code> that accepts POST requests with a <code>Command</code> payload.</li>\n<li class=\"\"><strong>Command Handler</strong>: The <code>handle_command</code> function processes the command, sending the user's text to OpenAI's chat completion API and streaming the response.</li>\n<li class=\"\"><strong>Stream Message Model</strong>: We define a <code>StreamMessage</code> model to structure the messages sent to Centrifugo.</li>\n<li class=\"\"><strong>Publish Message</strong>: The <code>publish_message</code> function sends the streamed messages to the specified Centrifugo channel using its HTTP API.</li>\n<li class=\"\"><strong>Error Handling</strong>: If an error occurs during the OpenAI API call, we send an error message to the Centrifugo channel.</li>\n<li class=\"\"><strong>Asynchronous Execution</strong>: The use of <code>async</code> and <code>await</code> allows the application to handle multiple requests concurrently, making it efficient for streaming responses.</li>\n</ol>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"frontend\">Frontend<a href=\"https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo#frontend\" class=\"hash-link\" aria-label=\"Direct link to Frontend\" title=\"Direct link to Frontend\" translate=\"no\">​</a></h2>\n<p>The frontend in this example is a single <code>index.html</code> file which draws a chat interface, handles user prompts and connects to Centrifugo to receive answer tokens in real-time.</p>\n<p>Here is the code for the frontend (<code>frontend/index.html</code>):</p>\n<div class=\"language-html codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-html codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token doctype punctuation\" style=\"color:rgb(199, 146, 234);font-style:italic\">&lt;!</span><span class=\"token doctype doctype-tag\" style=\"color:rgb(199, 146, 234);font-style:italic\">DOCTYPE</span><span class=\"token doctype\" style=\"color:rgb(199, 146, 234);font-style:italic\"> </span><span class=\"token doctype name\" style=\"color:rgb(199, 146, 234);font-style:italic\">html</span><span class=\"token doctype punctuation\" style=\"color:rgb(199, 146, 234);font-style:italic\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">lang</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">en</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">head</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">meta</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">charset</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">UTF-8</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">/&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">title</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\">Chat with GPT Streaming</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">title</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">src</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">https://unpkg.com/centrifuge@5.3.5/dist/centrifuge.js</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token script\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">src</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">https://cdn.tailwindcss.com</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token script\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">meta</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">name</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">viewport</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">content</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">width=device-width, initial-scale=1.0</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">/&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">head</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">bg-gradient-to-br from-gray-900 via-black to-gray-900 text-gray-200 min-h-screen flex flex-col items-center justify-start py-6 px-4 text-base</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">w-full max-w-2xl bg-gray-800/40 backdrop-blur-lg border border-gray-700 shadow-2xl rounded-2xl overflow-hidden</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-6 py-4 text-2xl font-bold tracking-wide</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        🔮 Chat with GPT with streaming over Centrifugo</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">            </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">chat</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">            </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">h-96 overflow-y-auto p-4 space-y-3 bg-gray-900 scrollbar-thin scrollbar-thumb-purple-500 scrollbar-track-gray-800 transition-colors duration-300</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">border-t border-gray-700 px-4 py-3 bg-gray-900 flex gap-3</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">input</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">input</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">type</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">text</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">placeholder</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">Type your question...</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">flex-1 border border-gray-600 bg-gray-800 placeholder-gray-400 text-white rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-1 transition-shadow duration-200</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                </span><span class=\"token tag special-attr attr-name\" style=\"color:rgb(255, 203, 107)\">onkeydown</span><span class=\"token tag special-attr attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag special-attr attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag special-attr attr-value value javascript language-javascript keyword control-flow\" style=\"color:rgb(255, 85, 114);font-style:italic\">if</span><span class=\"token tag special-attr attr-value value javascript language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token tag special-attr attr-value value javascript language-javascript\" style=\"color:rgb(255, 85, 114)\">event</span><span class=\"token tag special-attr attr-value value javascript language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token tag special-attr attr-value value javascript language-javascript property-access\" style=\"color:rgb(255, 85, 114)\">key</span><span class=\"token tag special-attr attr-value value javascript language-javascript\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag special-attr attr-value value javascript language-javascript operator\" style=\"color:rgb(137, 221, 255)\">===</span><span class=\"token tag special-attr attr-value value javascript language-javascript\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag special-attr attr-value value javascript language-javascript string\" style=\"color:rgb(195, 232, 141)\">'Enter'</span><span class=\"token tag special-attr attr-value value javascript language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token tag special-attr attr-value value javascript language-javascript\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag special-attr attr-value value javascript language-javascript function\" style=\"color:rgb(130, 170, 255)\">handleSend</span><span class=\"token tag special-attr attr-value value javascript language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token tag special-attr attr-value value javascript language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token tag special-attr attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">/&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">button</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                </span><span class=\"token tag special-attr attr-name\" style=\"color:rgb(255, 203, 107)\">onclick</span><span class=\"token tag special-attr attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag special-attr attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag special-attr attr-value value javascript language-javascript function\" style=\"color:rgb(130, 170, 255)\">handleSend</span><span class=\"token tag special-attr attr-value value javascript language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token tag special-attr attr-value value javascript language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token tag special-attr attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">bg-gradient-to-r from-indigo-500 to-purple-500 hover:from-purple-500 hover:to-indigo-600 text-white font-medium px-4 py-2 rounded-lg transition-shadow duration-200 shadow-lg hover:shadow-2xl</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            Send</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">button</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript constant\" style=\"color:rgb(130, 170, 255)\">USER</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"User_\"</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript known-class-name class-name\" style=\"color:rgb(255, 203, 107)\">Math</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">floor</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript known-class-name class-name\" style=\"color:rgb(255, 203, 107)\">Math</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">random</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript number\" style=\"color:rgb(247, 140, 108)\">1000</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript constant\" style=\"color:rgb(130, 170, 255)\">BACKEND_URL</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"/api/execute\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript constant\" style=\"color:rgb(130, 170, 255)\">CENTRIFUGO_WS</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"ws://\"</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">location</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">host</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"/connection/websocket\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> centrifuge </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">new</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript constant\" style=\"color:rgb(130, 170, 255)\">CENTRIFUGO_WS</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    centrifuge</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> chat </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"chat\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> input </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"input\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">function</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript function\" style=\"color:rgb(130, 170, 255)\">appendMessage</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">text</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> id </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword null nil\" style=\"font-style:italic\">null</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> type </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"user\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">let</span><span class=\"token script language-javascript\"> el </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> id </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">?</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">id</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword null nil\" style=\"font-style:italic\">null</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token script language-javascript\">el</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            el </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">createElement</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"div\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            el</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">className</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token script language-javascript template-string string\" style=\"color:rgb(195, 232, 141)\">msg px-3 py-2 rounded-lg max-w-full break-words </span><span class=\"token script language-javascript template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token script language-javascript template-string interpolation\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript template-string interpolation\">                    type </span><span class=\"token script language-javascript template-string interpolation operator\" style=\"color:rgb(137, 221, 255)\">===</span><span class=\"token script language-javascript template-string interpolation\"> </span><span class=\"token script language-javascript template-string interpolation string\" style=\"color:rgb(195, 232, 141)\">\"user\"</span><span class=\"token script language-javascript template-string interpolation\"> </span><span class=\"token script language-javascript template-string interpolation operator\" style=\"color:rgb(137, 221, 255)\">?</span><span class=\"token script language-javascript template-string interpolation\"> </span><span class=\"token script language-javascript template-string interpolation string\" style=\"color:rgb(195, 232, 141)\">\"bg-blue-500 text-white self-end ml-auto\"</span><span class=\"token script language-javascript template-string interpolation\"> </span><span class=\"token script language-javascript template-string interpolation operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token script language-javascript template-string interpolation\"> </span><span class=\"token script language-javascript template-string interpolation string\" style=\"color:rgb(195, 232, 141)\">\"bg-gray-700 text-gray-100\"</span><span class=\"token script language-javascript template-string interpolation\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript template-string interpolation\">            </span><span class=\"token script language-javascript template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            el</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">id</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> id </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">||</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            chat</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">appendChild</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">el</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        el</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">innerHTML</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> text</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">replace</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript regex regex-delimiter\">/</span><span class=\"token script language-javascript regex regex-source language-regex escape\">\\n</span><span class=\"token script language-javascript regex regex-delimiter\">/</span><span class=\"token script language-javascript regex regex-flags\">g</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'&lt;br&gt;'</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        chat</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">scrollTop</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> chat</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">scrollHeight</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">async</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">function</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript function\" style=\"color:rgb(130, 170, 255)\">handleStreamSubscription</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript parameter\">channel</span><span class=\"token script language-javascript parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript parameter\"> replyId</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> sub </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> centrifuge</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">channel</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">let</span><span class=\"token script language-javascript\"> reply </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        sub</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"publication\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript parameter\">ctx</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> msg </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> ctx</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">data</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">msg</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">text</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> token </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> msg</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">text</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">||</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                reply </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">+=</span><span class=\"token script language-javascript\"> token</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                </span><span class=\"token script language-javascript function\" style=\"color:rgb(130, 170, 255)\">appendMessage</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token script language-javascript template-string string\" style=\"color:rgb(195, 232, 141)\">GPTBot: </span><span class=\"token script language-javascript template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token script language-javascript template-string interpolation\">reply</span><span class=\"token script language-javascript template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> replyId</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"bot\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">msg</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">done</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                sub</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">unsubscribe</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        sub</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token script language-javascript\"> sub</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">ready</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">async</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">function</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript function\" style=\"color:rgb(130, 170, 255)\">handleSend</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> text </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> input</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">value</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">trim</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token script language-javascript\">text</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        input</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">value</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> msgId </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> crypto</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">randomUUID</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> channel </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token script language-javascript template-string string\" style=\"color:rgb(195, 232, 141)\">stream_</span><span class=\"token script language-javascript template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token script language-javascript template-string interpolation\">msgId</span><span class=\"token script language-javascript template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript function\" style=\"color:rgb(130, 170, 255)\">appendMessage</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token script language-javascript template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token script language-javascript template-string interpolation constant\" style=\"color:rgb(130, 170, 255)\">USER</span><span class=\"token script language-javascript template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript template-string string\" style=\"color:rgb(195, 232, 141)\">: </span><span class=\"token script language-javascript template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token script language-javascript template-string interpolation\">text</span><span class=\"token script language-javascript template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword null nil\" style=\"font-style:italic\">null</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"user\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> cmd </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript literal-property property\">text</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token script language-javascript\"> text</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript literal-property property\">channel</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token script language-javascript\"> channel</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript function\" style=\"color:rgb(130, 170, 255)\">handleStreamSubscription</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">channel</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> msgId</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript function\" style=\"color:rgb(130, 170, 255)\">fetch</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript constant\" style=\"color:rgb(130, 170, 255)\">BACKEND_URL</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript literal-property property\">method</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"POST\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript literal-property property\">headers</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string-property property\">\"Content-Type\"</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"application/json\"</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript literal-property property\">body</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript known-class-name class-name\" style=\"color:rgb(255, 203, 107)\">JSON</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">stringify</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">cmd</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><br></div></code></pre></div></div>\n<p>The key parts of the code are:</p>\n<ol>\n<li class=\"\"><strong>Centrifugo Connection</strong>: The frontend connects to Centrifugo WebSocket endpoint using the <a href=\"https://github.com/centrifugal/centrifuge-js\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifuge-js</a> library.</li>\n<li class=\"\"><strong>Chat Interface</strong>: The chat interface is built using Tailwind CSS for styling. It consists of a chat area and an input field for user prompts.</li>\n<li class=\"\"><strong>Message Handling</strong>: The <code>appendMessage</code> function appends messages to the chat area, distinguishing between user and bot messages.</li>\n<li class=\"\"><strong>Stream Subscription</strong>: The <code>handleStreamSubscription</code> function subscribes to a temporary channel for the user's prompt. It listens for incoming messages from Centrifugo and appends them to the chat interface in real-time.</li>\n<li class=\"\"><strong>Sending User Prompts</strong>: The <code>handleSend</code> function sends the user's prompt to the backend API and initiates the stream subscription for the response.</li>\n<li class=\"\"><strong>UUID Generation</strong>: Each user prompt is assigned a unique ID using <code>crypto.randomUUID()</code>, which is used to create a temporary channel for streaming the response.</li>\n<li class=\"\"><strong>Real-time Updates</strong>: The frontend updates the chat interface in real-time as tokens are received from the backend via Centrifugo. Once done signal is received, the subscription is unsubscribed.</li>\n</ol>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"centrifugo\">Centrifugo<a href=\"https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo#centrifugo\" class=\"hash-link\" aria-label=\"Direct link to Centrifugo\" title=\"Direct link to Centrifugo\" translate=\"no\">​</a></h2>\n<p>As we can see frontend connects to Centrifugo WebSocket endpoint and subscribes to a temporary channel for each user prompt. The backend publishes the response tokens to this channel, and the frontend appends them to the chat interface in real-time.</p>\n<p>Here we run Centrifugo with a simple configuration. The <code>config.json</code> file for Centrifugo will look like this:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"http_api\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"key\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"secret\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"client\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"allowed_origins\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"*\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"insecure\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"log\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"level\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"debug\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Note, we enabled insecure mode for the client, which allows us to not think about authentication in this example. In a real application, you should use secure connections and proper authentication mechanisms. We are also using a simple HTTP API key \"secret\" for the backend to publish messages to Centrifugo – you of course should use a more secure key in your app.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"nginx\">Nginx<a href=\"https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo#nginx\" class=\"hash-link\" aria-label=\"Direct link to Nginx\" title=\"Direct link to Nginx\" translate=\"no\">​</a></h2>\n<p>We will use Nginx as a reverse proxy to serve the frontend and route API requests to the backend. Nginx will also handle static files and provide a simple configuration for serving the application. Here is a Nginx server configuration we used (<code>nginx/default.conf</code>):</p>\n<div class=\"language-nginx codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-nginx codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">server {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  listen 80;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  location / {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    root /usr/share/nginx/html;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    index index.html;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    try_files $uri $uri/ =404;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  location /api {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_pass http://backend:5000;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header Host $host;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header X-Real-IP $remote_addr;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  location /connection {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_pass http://centrifugo:8000;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_http_version 1.1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header Upgrade $http_upgrade;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header Connection \"upgrade\";</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header Host $host;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div></code></pre></div></div>\n<p>Basically it consists of three locations:</p>\n<ul>\n<li class=\"\"><code>/</code> – serves the static files from the frontend directory</li>\n<li class=\"\"><code>/api</code> – proxies requests to the backend FastAPI application</li>\n<li class=\"\"><code>/connection</code> – proxies requests to Centrifugo for establishing a connection properly proxying WebSocket Upgrade headers</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"combining-everything-with-docker-compose\">Combining everything with Docker Compose<a href=\"https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo#combining-everything-with-docker-compose\" class=\"hash-link\" aria-label=\"Direct link to Combining everything with Docker Compose\" title=\"Direct link to Combining everything with Docker Compose\" translate=\"no\">​</a></h2>\n<p>Finally, we will combine everything with Docker Compose. The <code>docker-compose.yml</code> file will look like this:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token key atrule\">services</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">centrifugo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">image</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> centrifugo/centrifugo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">v6</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">container_name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> centrifugo</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">ports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"8000:8000\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">volumes</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> ./centrifugo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">/centrifugo</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> centrifugo </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\">c /centrifugo/config.json</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">env_file</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">backend</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">build</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> ./backend</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">container_name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> backend</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">ports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"5000:5000\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">volumes</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> ./backend</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">/app</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">env_file</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> .env</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">depends_on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> centrifugo</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> uvicorn app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">app </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\">host 0.0.0.0 </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\">port 5000 </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\">reload</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">nginx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">image</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> nginx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">latest</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">container_name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> nginx</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">ports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"9000:80\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">volumes</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> ./frontend</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">/usr/share/nginx/html</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> ./nginx/default.conf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">/etc/nginx/conf.d/default.conf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">ro</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">depends_on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> backend</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> centrifugo</span><br></div></code></pre></div></div>\n<p>Note, that to test the app with real OpenAI API you need to set your OpenAI API key in the <code>.env</code> file:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">OPENAI_API_KEY=\"&lt;YOUR_OPEN_AI_TOKEN&gt;\"</span><br></div></code></pre></div></div>\n<p>We made Nginx available on port <code>9000</code>, so once you start the application with:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker compose up</span><br></div></code></pre></div></div>\n<p>you can access the frontend at <a href=\"http://localhost:9000/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:9000</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>In this article, we have shown how to stream ChatGPT responses in real-time using Centrifugo as a real-time transport. We used FastAPI for the backend and OpenAI API for generating responses, but it may be easily adapted to other LLMs or backend frameworks. The example is simple and effective, and it can be used as a starting point for building more complex applications that require real-time streaming of AI responses.</p>\n<p>In real app don't forget to handle user authentication, including proper authentication of user in Centrifugo. For Centrifugo part see for example <a class=\"\" href=\"https://centrifugal.dev/docs/tutorial/centrifugo#adding-jwt-connection-authentication\">JWT auth example</a> in our Grand Chat tutorial.</p>",
            "url": "https://centrifugal.dev/blog/2025/06/17/streaming-ai-gpt-responses-with-centrifugo",
            "title": "Streaming AI responses with Centrifugo",
            "summary": "Centrifugo is an efficient and scalable transport for streaming AI responses. In this article, we will stream GPT-3.5 Turbo responses in real-time using Centrifugo temporary channels and Python. Simple and effective!",
            "date_modified": "2025-06-17T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "ai",
                "python",
                "tutorial"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard",
            "content_html": "<p>Centrifugo v6.2.0 added a bunch of new asynchronous consumers and new publish API fields such as <code>version</code> and <code>version_epoch</code>. Here, we are covering these changes by a new tutorial.</p>\n<p>In the tutorial, we'll build a real-time leaderboard application that updates dynamically as scores change. We'll use Centrifugo for real-time updates, Redis for storing leaderboard data, React for the frontend, and some Python to trigger ranking changes. This is a nice example of how Centrifugo can be used to create interactive, real-time applications with minimal effort.</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/leaderboard_demo.mp4\"></video>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"how-application-works\">How application works<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#how-application-works\" class=\"hash-link\" aria-label=\"Direct link to How application works\" title=\"Direct link to How application works\" translate=\"no\">​</a></h2>\n<p>Our application will:</p>\n<ol>\n<li class=\"\">Simulate score updates for a set of players</li>\n<li class=\"\">Keep the leaderboard data in Redis ZSET (Sorted Set) data structure</li>\n<li class=\"\">Atomically push leaderboard state to Redis STREAM data structure</li>\n<li class=\"\">Use Centrifugo to consume Redis Stream and push real-time updates to subscribed clients</li>\n<li class=\"\">Display the leaderboard with real-time updates and smooth animations when rankings change</li>\n</ol>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"tutorial-source-code\">Tutorial source code<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#tutorial-source-code\" class=\"hash-link\" aria-label=\"Direct link to Tutorial source code\" title=\"Direct link to Tutorial source code\" translate=\"no\">​</a></h2>\n<p>Jump directly to the tutorial source code which <a href=\"https://github.com/centrifugal/examples/tree/master/v6/leaderboard\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">may be found on Github</a>. Clone the repo, <code>cd</code> into <code>leaderboard</code> directory and run:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker compose up</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"project-structure\">Project Structure<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#project-structure\" class=\"hash-link\" aria-label=\"Direct link to Project Structure\" title=\"Direct link to Project Structure\" translate=\"no\">​</a></h2>\n<p>Here's the structure of our project:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">leaderboard/</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── backend/</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   ├── app.py                 # Python backend service</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   ├── Dockerfile             # Backend Docker configuration</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   ├── requirements.txt       # Python dependencies</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   └── lua/</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│       └── update_leaderboard.lua  # Redis Lua script</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── centrifugo/</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   └── config.json            # Centrifugo configuration</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── nginx/</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   └── nginx.conf             # Nginx configuration</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── web/</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   ├── public/                # React public assets</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   ├── src/                   # React source code</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   ├── Dockerfile             # Frontend Docker configuration</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   └── package.json           # Frontend dependencies</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">└── docker-compose.yml         # Docker Compose configuration</span><br></div></code></pre></div></div>\n<p>A <code>docker-compose.yml</code> file is used to combine all the parts together:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token key atrule\">version</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"3.8\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">services</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">redis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">image</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> redis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">7</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">ports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"6379:6379\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">centrifugo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">image</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> centrifugo/centrifugo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">v6.2.0</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">volumes</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> ./centrifugo/config.json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">/centrifugo/config.json</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> centrifugo </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\">config=/centrifugo/config.json</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">ports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"8000:8000\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">depends_on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token key atrule\">redis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token key atrule\">condition</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> service_started</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">backend</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">build</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> ./backend</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">depends_on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token key atrule\">redis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token key atrule\">condition</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> service_started</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token key atrule\">centrifugo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token key atrule\">condition</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> service_started</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">nginx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">image</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> nginx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">alpine</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">restart</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> always</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">ports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> 8080</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">80</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">volumes</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> ./nginx/nginx.conf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">/etc/nginx/conf.d/default.conf</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">depends_on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token key atrule\">backend</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token key atrule\">condition</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> service_started</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token key atrule\">web</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token key atrule\">condition</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> service_started</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">web</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">build</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> ./web</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">volumes</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> ./web</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">/app</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> /app/node_modules</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">ports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"3000:3000\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> sh </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\">c \"npm install </span><span class=\"token important\">&amp;&amp;</span><span class=\"token plain\"> npm start\"</span><br></div></code></pre></div></div>\n<p>Here we use Redis to keep leaderboard data because it has a nice ZSET data structure which is famous for its fit for the leaderboard's use case – it's very efficient since data is being managed in-memory, allowing for fast read and write operations.</p>\n<p>Redis also provides a Lua scripting engine that allows us to perform atomic operations on the data. We use lua scripting actively in Centrifugo Redis integrations, and here we will also utilize the power of it to atomically fill Redis STREAM which will be consumed by Centrifugo.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"building-the-backend-service\">Building the Backend Service<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#building-the-backend-service\" class=\"hash-link\" aria-label=\"Direct link to Building the Backend Service\" title=\"Direct link to Building the Backend Service\" translate=\"no\">​</a></h2>\n<p>The backend in this tutorial is very thin and is responsible only for simulating score updates. Here is the whole Python code which triggers ranking updates in Redis:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> time</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> random</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> redis</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">main</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    r </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> redis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Redis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">host</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'redis'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> port</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">6379</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">with</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">open</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'lua/update_leaderboard.lua'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'r'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">as</span><span class=\"token plain\"> f</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        lua_script </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> f</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">read</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    update_leaderboard </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">register_script</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">lua_script</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    leader_names </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Alice\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Bob\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Charlie\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"David\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Eve\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">while</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">True</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        leader </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> random</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">choice</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">leader_names</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        increment </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> random</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">randint</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">10</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        channel </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"leaderboard\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        update_leaderboard</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            keys</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"leaderboard\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"leaderboard-state\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"leaderboard-stream\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            args</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">leader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> increment</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">sleep</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0.2</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> __name__ </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"__main__\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    main</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>This Python script:</p>\n<ol>\n<li class=\"\">Connects to Redis</li>\n<li class=\"\">Loads the Lua script for updating the leaderboard</li>\n<li class=\"\">Randomly selects a player and increments their score</li>\n<li class=\"\">Calls the Lua script to update the leaderboard in Redis</li>\n<li class=\"\">Repeats this process every 0.2 seconds</li>\n</ol>\n<p>Lua script in <code>backend/lua/update_leaderboard.lua</code> is more interesting:</p>\n<div class=\"language-lua codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-lua codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">-- Get or create state hash containing both epoch and version</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local leaderboard_key = KEYS[1]</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local state_key = KEYS[2]</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local stream_key = KEYS[3]</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local name = ARGV[1]</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local score_inc = tonumber(ARGV[2])</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local channel = ARGV[3]</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">-- Increment leaderboard score</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">redis.call('ZINCRBY', leaderboard_key, score_inc, name)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local epoch = redis.call(\"HGET\", state_key, \"epoch\")</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">if not epoch then</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    local t = redis.call(\"TIME\")</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    epoch = tostring(t[1])</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    redis.call(\"HSET\", state_key, \"epoch\", epoch, \"version\", 0)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">end</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">-- Increment version atomically using HINCRBY</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local version = redis.call(\"HINCRBY\", state_key, \"version\", 1)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">-- Always update TTL regardless of whether state is new or existing</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">redis.call(\"EXPIRE\", state_key, 86400) -- Set TTL (24 hours, adjust as needed)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">-- Get leaderboard data</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local members = redis.call('ZREVRANGE', leaderboard_key, 0, -1, 'WITHSCORES')</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local leaders = {}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">for i = 1, #members, 2 do</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    table.insert(leaders, { name = members[i], score = tonumber(members[i+1]) })</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">end</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">-- Prepare payload for Centrifugo publish API command.</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local publish_payload = {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  channel = channel,</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  data = { leaders = leaders },</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  version = version, -- a tip for Centrifugo about state version</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  version_epoch = epoch, -- a tip for Centrifugo about state epoch</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">-- Add to stream which is consumed by Centrifugo.</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">local payload = cjson.encode(publish_payload)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">redis.call('XADD', stream_key, 'MAXLEN', '~', 10000, '*', </span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  'method', 'publish', 'payload', payload)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">return members</span><br></div></code></pre></div></div>\n<p>This Lua script:</p>\n<ol>\n<li class=\"\">Increments a player's score in a Redis sorted set</li>\n<li class=\"\">Maintains a HASH to give Centrifugo a tip about leaderboard state version and version epoch</li>\n<li class=\"\">Retrieves the current leaderboard data and adds it to Redis STREAM together with version and version epoch fields. It adds it in a format which Centrifugo Redis Stream consumer understands.</li>\n</ol>\n<p>1 and 3 are rather self-explaining. But why do we need to maintain leaderboard state incremental version and its epoch?</p>\n<p>The reason is that Redis Stream concurrent consumers working with a consumer group cannot maintain message ordering. So, if you have several Centrifugo instances consuming from the same Redis Stream, they are not able to guarantee that messages are processed in the order they were added to the stream.</p>\n<p>Starting from Centrifugo v6.2.0, we can use <code>version</code> and <code>version_epoch</code> fields to ensure that clients always receive the most up-to-date state of the leaderboard, even if messages are processed out of order. When Centrifugo receives publications with versions less or equal to those already seen – it skips them, so the client does not receive non-actual data. Of course, this makes sense in cases like the one in this tutorial, where we are publishing the entire state to a channel. An important note is that for version logic to work, we will need to enable Centrifugo history for channels since version information is kept by Centrifugo in the history stream meta information object.</p>\n<p>The <code>version</code> is an incremental number, and the logic with it should be straightforward to understand. But why do we need <code>version_epoch</code>? The <code>version_epoch</code> is a string that is used to identify the generation of the data. Since Redis is an in-memory data store, it is possible that the data may be lost if the Redis server is restarted or if the data is evicted from memory. By using <code>version_epoch</code> and tracking its change, Centrifugo avoids a situation where the <code>version</code> counter is restarted and Centrifugo ignores all updates until <code>version</code> reaches the number seen before. Using <code>version_epoch</code> is optional – if it's not passed to Centrifugo, then Centrifugo only looks at the <code>version</code> field to make the decision.</p>\n<p>Note that while we are using a single leaderboard in this tutorial, you can extend the Lua script to support multiple leaderboards by adding a leaderboard id/name to leaderboard ZSET and state HASH keys. The Redis STREAM key will stay the same – all updates will go through it, just use different channels in the publication object.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"configuring-centrifugo\">Configuring Centrifugo<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#configuring-centrifugo\" class=\"hash-link\" aria-label=\"Direct link to Configuring Centrifugo\" title=\"Direct link to Configuring Centrifugo\" translate=\"no\">​</a></h2>\n<p>Now let's configure Centrifugo to consume the Redis Stream and push updates to connected clients.</p>\n<p>To do this, we need Centrifugo to consume the Redis Stream, so <code>centrifugo/config.json</code> may look like this:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token property\">\"client\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"insecure\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"allowed_origins\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"*\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token property\">\"channel\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"without_namespace\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token property\">\"history_size\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token property\">\"history_ttl\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"24h\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token property\">\"consumers\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token property\">\"enabled\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"leaderboard_redis\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redis_stream\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token property\">\"redis_stream\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"address\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redis:6379\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"streams\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"leaderboard-stream\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"consumer_group\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"centrifugo\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"num_workers\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>This configuration consumes the Redis Stream and also enables insecure WebSocket connections, which is handy for tutorial purposes – so we don't need to think about client authentication and channel permissions here.</p>\n<p>Another important thing here is that enabling channel history is required for version logic to work because Centrifugo keeps version data in the history meta information.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"creating-the-frontend-react-application\">Creating the Frontend React Application<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#creating-the-frontend-react-application\" class=\"hash-link\" aria-label=\"Direct link to Creating the Frontend React Application\" title=\"Direct link to Creating the Frontend React Application\" translate=\"no\">​</a></h2>\n<p>Initialize a new React application:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">npx create-react-app web</span><br></div></code></pre></div></div>\n<p>Navigate to the web directory and install the dependencies:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">cd web</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">npm install centrifuge motion bootstrap</span><br></div></code></pre></div></div>\n<p>Update <code>web/src/App.js</code>:</p>\n<div class=\"language-jsx codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-jsx codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports maybe-class-name\">React</span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token imports\"> </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token imports\"> useState</span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token imports\"> useEffect </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'react'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token imports\"> motion </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'motion/react'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token imports\"> </span><span class=\"token imports maybe-class-name\">Centrifuge</span><span class=\"token imports\"> </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'centrifuge'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'bootstrap/dist/css/bootstrap.min.css'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'./App.css'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function maybe-class-name\" style=\"color:rgb(130, 170, 255)\">App</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> setState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">useState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">leaders</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">prevOrder</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">highlights</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">useEffect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"ws://localhost:8000/connection/websocket\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"leaderboard\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">message</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> data </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> message</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">setState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">prevState</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> newHighlights </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> newLeaders </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">leaders</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">leader</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token keyword\" style=\"font-style:italic\">let</span><span class=\"token plain\"> highlightClass </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> prevRank </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> prevState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">prevOrder</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">leader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">prevRank </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!==</span><span class=\"token plain\"> </span><span class=\"token keyword nil\" style=\"font-style:italic\">undefined</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">prevRank </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"> index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">              highlightClass </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"highlight-up\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">else</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">prevRank </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\"> index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">              highlightClass </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"highlight-down\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">highlightClass</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            newHighlights</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">leader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> highlightClass</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> leader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> newOrder </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        newLeaders</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">forEach</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">leader</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          newOrder</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">leader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\">prevState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token literal-property property\">leaders</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> newLeaders</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token literal-property property\">prevOrder</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> newOrder</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token literal-property property\">highlights</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\">prevState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">highlights</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\">newHighlights </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">unsubscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">disconnect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">className</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">container mt-5</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">      </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">className</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">card</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">className</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">card-header</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\">Real-time Leaderboard with Centrifugo</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">className</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">card-body</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">          </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">table</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">className</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">table table-striped</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">            </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">thead</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">              </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">tr</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">                </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">th</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">scope</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">col</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">className</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">rank-col</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\">Rank</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">th</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">                </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">th</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">scope</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">col</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\">Name</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">th</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">                </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">th</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">scope</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">col</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\">Score</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">th</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">              </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">tr</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">            </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">thead</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">            </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">tbody</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">              </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">state</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">leaders</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">leader</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">motion.tr</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                  </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">key</span><span class=\"token tag script language-javascript script-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token tag script language-javascript\" style=\"color:rgb(255, 85, 114)\">leader</span><span class=\"token tag script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token tag script language-javascript property-access\" style=\"color:rgb(255, 85, 114)\">name</span><span class=\"token tag script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                  </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">layout</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                  </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">className</span><span class=\"token tag script language-javascript script-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token tag script language-javascript\" style=\"color:rgb(255, 85, 114)\">state</span><span class=\"token tag script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token tag script language-javascript property-access\" style=\"color:rgb(255, 85, 114)\">highlights</span><span class=\"token tag script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token tag script language-javascript\" style=\"color:rgb(255, 85, 114)\">leader</span><span class=\"token tag script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token tag script language-javascript property-access\" style=\"color:rgb(255, 85, 114)\">name</span><span class=\"token tag script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token tag script language-javascript\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">||</span><span class=\"token tag script language-javascript\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag script language-javascript string\" style=\"color:rgb(195, 232, 141)\">''</span><span class=\"token tag script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">                </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">                  </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">td</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">className</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">rank-col</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">index </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">td</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">                  </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">td</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">leader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">td</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">                  </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">td</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">leader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">score</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">td</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">                </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">motion.tr</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">              </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">            </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">tbody</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">          </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">table</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">      </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain-text\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain-text\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">export</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">default</span><span class=\"token plain\"> </span><span class=\"token maybe-class-name\">App</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>This React component:</p>\n<ol>\n<li class=\"\">Connects to Centrifugo via WebSocket</li>\n<li class=\"\">Subscribes to the \"leaderboard\" channel</li>\n<li class=\"\">Updates the UI when new leaderboard data is received</li>\n<li class=\"\">Uses <code>motion</code> animations to highlight changes in rankings</li>\n</ol>\n<p>File <code>web/src/App.css</code> has some CSS styles to make it look better. Here we skip it for brevity.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"configuring-nginx\">Configuring Nginx<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#configuring-nginx\" class=\"hash-link\" aria-label=\"Direct link to Configuring Nginx\" title=\"Direct link to Configuring Nginx\" translate=\"no\">​</a></h2>\n<p>Finally, let's add Nginx, which is useful to have a single app endpoint that proxies requests to the React frontend and Centrifugo WebSocket connection:</p>\n<div class=\"language-nginx codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-nginx codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">server {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    listen 80;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    server_name localhost;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    location / {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        proxy_pass http://web:3000;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        proxy_http_version 1.1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        proxy_set_header Upgrade $http_upgrade;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        proxy_set_header Connection \"upgrade\";</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        proxy_set_header Host $host;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    location /connection/websocket {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        proxy_pass http://centrifugo:8000;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        proxy_http_version 1.1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        proxy_set_header Upgrade $http_upgrade;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        proxy_set_header Connection \"upgrade\";</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        proxy_set_header Host $host;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"running-the-application\">Running the Application<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#running-the-application\" class=\"hash-link\" aria-label=\"Direct link to Running the Application\" title=\"Direct link to Running the Application\" translate=\"no\">​</a></h2>\n<p>Now that we have all the components set up, let's run the application:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker compose up</span><br></div></code></pre></div></div>\n<p>Once everything is running, you can access the application at <a href=\"http://localhost:8080/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:8080</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"adding-fossil-delta-compression\">Adding Fossil delta compression<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#adding-fossil-delta-compression\" class=\"hash-link\" aria-label=\"Direct link to Adding Fossil delta compression\" title=\"Direct link to Adding Fossil delta compression\" translate=\"no\">​</a></h2>\n<p>We are sending the full state in every publication now, but only part of the data changes. Centrifugo provides a <a class=\"\" href=\"https://centrifugal.dev/docs/server/delta_compression\">Fossil delta compression algorithm</a> which can be used to reduce the amount of data sent over the network.</p>\n<p>First, let's look at how our WebSocket session looks now by opening the WebSocket tab in Chrome Dev Tools (right-click and open the image in a new tab to see better):</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/leaderboard_no_fossil-ee21b6ed584231de38d4e90080d2e2e3.jpg\" width=\"3016\" height=\"768\" class=\"img_ev3q\"></p>\n<p>We see the full content is being sent in every message.</p>\n<p>Let's add Fossil delta compression. To do this, we need to modify the client-side subscription:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"leaderboard\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">delta</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'fossil'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>And extend Centrifugo config:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   ...</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token property\">\"channel\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"without_namespace\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        ...</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"allowed_delta_types\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"fossil\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"delta_publish\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   ...</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Reload the app, and see how WebSocket frames look now:</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/leaderboard_fossil-513777bd2e4dd25c05e8f14f0af0ca91.jpg\" width=\"3016\" height=\"758\" class=\"img_ev3q\"></p>\n<p>Instead of full payloads, we see deltas with much smaller data size (x2 reduction), and the Centrifugo SDK automatically applies deltas correctly under the hood, so no other changes to the application code are required.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"adding-cache-recovery-mode\">Adding cache recovery mode<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#adding-cache-recovery-mode\" class=\"hash-link\" aria-label=\"Direct link to Adding cache recovery mode\" title=\"Direct link to Adding cache recovery mode\" translate=\"no\">​</a></h2>\n<p>Leaderboard data is also a good candidate for Centrifugo cache recovery mode feature. We can instantly and automatically load the latest publication known data upon subscription from Centrifugo stream history.</p>\n<p>To do this, we need history to be enabled (we already have) and add a couple of extra options to the configuration:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token string-property property\">\"channel\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token string-property property\">\"without_namespace\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token string-property property\">\"force_recovery\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token string-property property\">\"force_recovery_mode\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"cache\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>And one more option (<code>since</code> object) to the client-side subscription to trigger recovery upon the initial subscription:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"leaderboard\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">delta</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'fossil'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">since</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>After making this, the latest leaderboard data will be immediately displayed to the user upon subscription without the need for extra synchronization of initial state loading and real-time updates.</p>\n<p>This is how it looks from the WebSocket frame perspective:</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/leaderboard_cache-e1b4526260ca0f8d720d2be958e67589.jpg\" width=\"3022\" height=\"1006\" class=\"img_ev3q\"></p>\n<p>I.e., we see that the initial data was sent within the subscribe response. And it's in JSON string format here because we are using delta compression. If we disable delta compression, we will see the initial data just as a regular JSON object.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"other-possible-improvements\">Other possible improvements<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#other-possible-improvements\" class=\"hash-link\" aria-label=\"Direct link to Other possible improvements\" title=\"Direct link to Other possible improvements\" translate=\"no\">​</a></h2>\n<p>With Centrifugo, only with a little extra effort you can:</p>\n<ul>\n<li class=\"\">Add authentication to the WebSocket connection</li>\n<li class=\"\">Use Centrifugo built-in channel permissions to restrict access to channels</li>\n<li class=\"\">Use Centrifugo built-in online presence feature to show who is online</li>\n<li class=\"\">Use binary protocol to reduce the amount of data sent over the network and improve serialization performance</li>\n<li class=\"\">Provide WebSocket fallbacks based on Server-Sent Events (SSE) or HTTP-streaming</li>\n<li class=\"\">Scale Centrifugo nodes to handle <a class=\"\" href=\"https://centrifugal.dev/blog/2020/02/10/million-connections-with-centrifugo\">millions of connections</a>.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>In this tutorial, we've built a real-time leaderboard application using Centrifugo, Redis, and React. This demonstrates how Centrifugo can be used to create interactive, real-time applications with minimal effort.</p>\n<p>By leveraging the real-time capabilities of Centrifugo, you can create engaging, interactive applications that provide users with immediate feedback and updates.</p>\n<p>Moreover, Centrifugo comes with many optimizations to make it the most effective solution for real-time applications. For example, it may use Fossil delta compression to reduce the amount of data sent over the network, and it has a cache recovery mode to ensure that clients can recover from network interruptions without losing updates.</p>",
            "url": "https://centrifugal.dev/blog/2025/04/28/websocket-real-time-leaderboard",
            "title": "Building a real-time WebSocket leaderboard with Centrifugo and Redis",
            "summary": "In this article, we create a real-time leaderboard using Centrifugo, Redis, React and Python. Here we show the usage of Centrifugo built-in asynchronous consumer from Redis Stream. The post additionally showcases Fossil delta compression and cache recovery mode.",
            "date_modified": "2025-04-28T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "redis",
                "react",
                "python",
                "tutorial"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released",
            "content_html": "<p>We are excited to announce Centrifugo v6 – a new major release that is now live. This release includes fundamental improvements in the configuration to simplify working with Centrifugo from both user and core development perspectives. It also adds several useful features and enhances observability for both Centrifugo OSS and Centrifugo PRO.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Prefer podcast version of this post? (8.5 MB).</div><div class=\"admonitionContent_BuS1\"><audio controls=\"\"><source src=\"/img/v6.mp3\" type=\"audio/mp3\"></audio></div></div>\n<p>For those who never heard about Centrifugo before, it is a scalable real-time messaging server in a self-hosted environment. Centrifugo allows you to build real-time features in your application, such as chat, notifications, live updates, and more. It's modern, fast, reliable and lightweight. Centrifugo is used by many companies worldwide to power real-time features in their applications. Find out more information in <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/introduction\">introduction</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"why-centrifugo-v6-was-required\">Why Centrifugo v6 was required?<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#why-centrifugo-v6-was-required\" class=\"hash-link\" aria-label=\"Direct link to Why Centrifugo v6 was required?\" title=\"Direct link to Why Centrifugo v6 was required?\" translate=\"no\">​</a></h2>\n<img src=\"https://centrifugal.dev/img/v6_most_wanted.jpg\" align=\"left\" style=\"margin-right:15px;margin-bottom:5px;float:left;max-width:300px\">\n<p>In a recent blog post, we talked about <a class=\"\" href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones\">notable Centrifugo v5 milestones</a>. The v5 release was a significant milestone in the Centrifugo project's history, introducing numerous new features and improvements. However, the time for a new major release had come.</p>\n<p>Over the years, Centrifugo has evolved into a robust platform packed with numerous features. However, as Centrifugo's capabilities expanded, so did the complexity of its configuration. Settings became increasingly dispersed across various parts of the codebase, making them harder to manage and understand. Adding new features required modifying more areas than it should have. With v6, we are addressing this challenge head-on by restructuring the configuration system and rethinking its organization.</p>\n<p>Additionally, there were a few areas that required improvement but could not be addressed without breaking changes. A couple of deprecated features were removed in v6.</p>\n<p>Let's begin our dive into the Centrifugo v6 release with a description of the removed parts.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"removing-sockjs\">Removing SockJS<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#removing-sockjs\" class=\"hash-link\" aria-label=\"Direct link to Removing SockJS\" title=\"Direct link to Removing SockJS\" translate=\"no\">​</a></h2>\n<p>SockJS is a JavaScript library that provides a WebSocket-like object, but under the hood, it uses various transports to establish a connection (falling back to HTTP instead of WebSocket in case of connection issues).</p>\n<p>The SockJS transport was deprecated in the Centrifugal ecosystem <a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#:~:text=SockJS%20is%20still%20supported%20by%20Centrifugo%20and%20centrifuge%2Djs%2C%20but%20it%27s%20now%20DEPRECATED.\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">since the v4 release</a>. We encouraged users to reach out if SockJS was still necessary <a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#the-future-of-sockjs\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">in blog posts</a> and marked it as deprecated in the documentation. However, nobody reached out during this time.</p>\n<p>The SockJS client is poorly maintained these days, with issues not being addressed and some transports becoming outdated.</p>\n<p>Since Centrifugo v4, we have <a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#modern-websocket-emulation-in-javascript\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">our own WebSocket emulation layer</a>. Unlike SockJS's HTTP-based fallbacks, our layer:</p>\n<ul>\n<li class=\"\">does not require sticky sessions in distributed cases (!),</li>\n<li class=\"\">supports binary in the HTTP-streaming case,</li>\n<li class=\"\">allows batching of messages in a more efficient wire format,</li>\n<li class=\"\">is more performant in terms of CPU and memory on the server side, and</li>\n<li class=\"\">requires fewer round-trips for connection establishment.</li>\n</ul>\n<p>In Centrifugo v6, SockJS has been removed. Our JavaScript SDK, <code>centrifuge-js</code>, will continue to support the SockJS transport for some time to work with Centrifugo v5, but we plan to remove it from the client SDK eventually.</p>\n<p>To enable Centrifugo’s built-in bidirectional emulation, you need to enable <a class=\"\" href=\"https://centrifugal.dev/docs/transports/http_stream\">HTTP streaming</a> or <a class=\"\" href=\"https://centrifugal.dev/docs/transports/sse\">SSE</a> transports in the server configuration, and then configure <code>centrifuge-js</code> to use those <a href=\"https://github.com/centrifugal/centrifuge-js?tab=readme-ov-file#http-based-websocket-fallbacks\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">as described in its README</a>:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> transports </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'websocket'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'ws://localhost:8000/connection/websocket'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'http_stream'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'http://localhost:8000/connection/http_stream'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'sse'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'http://localhost:8000/connection/sse'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">transports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"removing-tarantool\">Removing Tarantool<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#removing-tarantool\" class=\"hash-link\" aria-label=\"Direct link to Removing Tarantool\" title=\"Direct link to Removing Tarantool\" translate=\"no\">​</a></h2>\n<p>Centrifugo v3 introduced the experimental Tarantool engine, and we were excited about it potentially being a solid alternative to Redis. However, since then, the Tarantool engine hasn’t seen many updates and is now missing some important features, like idempotent publishing and delta compression. These features were added to the memory and Redis engines during the v5 release, but unfortunately, Tarantool was left behind.</p>\n<p>We were aware of only two setups where the Tarantool engine was used – and both clients eventually moved away from it with our help. Additionally, our usage statistics do not show any notable adoption of the Tarantool engine.</p>\n<p>The reality is that while Tarantool provides some interesting technical advantages over Redis, maintaining proper integration with it and keeping it current is impossible given the resources of Centrifugal Labs. Furthermore, there was no significant support from the Centrifugo community to push its development forward.</p>\n<p>For these reasons, we decided to remove Tarantool integration from Centrifugo. All Tarantool-related repositories will be moved to read-only mode. For now Centrifugal Labs will focus on Redis, Redis-compatible brokers, and NATS as the main scalability options for Centrifugo.</p>\n<p>Sometimes, it's necessary to drop some ballast to continue a beautiful journey...</p>\n<img src=\"https://centrifugal.dev/img/ballast.jpg\">\n<p>Meanwhile, our Redis integration has been improved. In Centrifugo v4, we migrated to the <a href=\"https://github.com/redis/rueidis\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">redis/rueidis</a> Go library, which enabled the Centrifugo node to achieve better Redis communication throughput. You can find more details in the blog post <a class=\"\" href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance\">Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library</a>.</p>\n<p>We have also put more effort into integrating with Redis-compatible storages, such as <a href=\"https://www.dragonflydb.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">DragonflyDB</a>, which may unlock new and interesting capabilities without the need to maintain a separate engine.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"configuration-refactoring\">Configuration refactoring<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#configuration-refactoring\" class=\"hash-link\" aria-label=\"Direct link to Configuration refactoring\" title=\"Direct link to Configuration refactoring\" translate=\"no\">​</a></h2>\n<p>Over the years, Centrifugo's configuration has been built on the approach initially established in its early versions. At the beginning, the number of configuration options was relatively small and manageable. However, with every new version and feature, the configuration became increasingly difficult to maintain and extend.</p>\n<p>Refactoring this part is a challenging and not particularly enjoyable process, and it inevitably results in breaking compatibility. Nevertheless, for v6, we decided it was time to make this change.</p>\n<p>Centrifugo v6 configuration has been completely restructured and now consists of distinct blocks – with all the options grouped together to clearly indicate the layer they correspond to.</p>\n<p>For example, there is a <code>client</code> top-level configuration block that contains options related to real-time client connections. To illustrate, let's take the <code>allowed_origins</code> option from Centrifugo v5:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">Centrifugo v5 config example</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"allowed_origins\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"https://example.com\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>It was moved under <code>client</code> section in v6:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"client\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"allowed_origins\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"https://example.com\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>It is now clear which layer of Centrifugo a given option corresponds to. For instance, the <code>allowed_origins</code> option is related to client connections – not to the server API or the admin web interface.</p>\n<p>Let's look at another example. Remember that Centrifugo supports not only JSON configuration files but also YAML and TOML? Let’s look at another example, this time in YAML format:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">Centrifugo v5 YAML config example</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token key atrule\">port</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8000</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">token_hmac_secret_key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> XXX</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">admin_password</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> XXX</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">admin_secret</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> XXX</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">api_key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> XXX</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">allowed_origins</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">//localhost</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">3000</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">presence</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">namespaces</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\">name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> ns</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">presence</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:rgb(255, 88, 116)\">true</span><br></div></code></pre></div></div>\n<p>In v6 becomes:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token key atrule\">http_server</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">port</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8000</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">hmac_secret_key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> XXX</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">allowed_origins</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">//localhost</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">3000</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">admin</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">password</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> XXX</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">secret</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> XXX</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">http_api</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> XXX</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token key atrule\">channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">without_namespace</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">presence</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">namespaces</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> </span><span class=\"token key atrule\">name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> ns</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">presence</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean important\" style=\"color:rgb(255, 88, 116)\">true</span><br></div></code></pre></div></div>\n<p>Again – we believe it's much more clear now what part of server each option relates to.</p>\n<p>We eliminated the situation where options were scattered across the Centrifugo codebase, often with unclear defaults and a non-obvious process for adding a new option. Now, the configuration is represented by a single Go struct. Config sections are organized into nested structs. Defaults are explicitly defined using struct field definition tags, making them easy to identify. This approach is simple to follow and extend – in most cases, adding a new option is as straightforward as introducing a new field in the struct or a nested struct for more complex functionality. Having all options centralized in a single struct also unlocks new possibilities for working with the configuration, as we will explore below.</p>\n<p>One aspect we'd like to highlight is that channel options for channels without any namespace prefix are now defined under the <code>channel -&gt; without_namespace</code> block (as shown in the example above). This change ensures that options for channels without a namespace are not mixed with other Centrifugo options at the same level of configuration. Previously, the way namespace options were organized in the codebase led to several bugs – options for channels without a namespace required separate extraction, which was often overlooked. This issue has now been resolved. Additionally, with new config structure we more explicitly encourage users to adopt channel namespaces as a best practice when working with Centrifugo.</p>\n<p>A great feature of Centrifugo is that it warns about unknown options in the configuration file and unknown environment variables at startup. This functionality, which helps users identify configuration mistakes, was already present in previous versions and remains in v6. Now, it also supports keys in deeply nested objects and arrays of objects without requiring excessive copy-paste in the codebase.</p>\n<p>Re-structuring the configuration also affects how environment variables are constructed to configure Centrifugo. This will require users to update their environment variable configurations. To assist with this, we have added configuration converters to the v6 migration guide and introduced new CLI commands, which should greatly simplify the process (see more details below).</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"tls-config-unification\">TLS config unification<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#tls-config-unification\" class=\"hash-link\" aria-label=\"Direct link to TLS config unification\" title=\"Direct link to TLS config unification\" translate=\"no\">​</a></h2>\n<p>An important aspect of the new Centrifugo v6 configuration is that it uses the same TLS configuration object consistently across the entire system. Whenever you are configuring TLS now, you can expect the same field names, just at different configuration levels. TLS for the HTTP server, Redis client, NATS client, Kafka client, PostgreSQL client, and even mTLS support – all can be configured in a unified way.</p>\n<p>The new <a class=\"\" href=\"https://centrifugal.dev/docs/server/configuration#tls-config-object\">TLS config object</a>, which was already used in some places in v5, allows certs and keys to be passed in three different ways:</p>\n<ul>\n<li class=\"\">as a string in the config with PEM-encoded cert/key content,</li>\n<li class=\"\">as a base64 encoded string of PEM-encoded cert/key, or</li>\n<li class=\"\">as a path to a file with PEM-encoded cert/key.</li>\n</ul>\n<p>So you can choose the method that is most convenient for you.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"proxy-config-improvements\">Proxy config improvements<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#proxy-config-improvements\" class=\"hash-link\" aria-label=\"Direct link to Proxy config improvements\" title=\"Direct link to Proxy config improvements\" translate=\"no\">​</a></h2>\n<p>Due to its self-hosted nature, Centrifugo can provide its users an efficient way to proxy various connection events to the application backend, enabling the backend to respond in a customized manner to control the flow. This feature, called <a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy\">proxy</a> is widely used by Centrifugo users. It allows authenticating connections in a customized manner (when the built-in JWT Centrifugo authentication is not suitable), managing channel subscription permissions and publication validations, refreshing client sessions, and handling RPC calls sent by a client over a bidirectional real-time connection.</p>\n<p>In v6, there are a couple of notable improvements in the proxy feature configuration to make it simpler to use.</p>\n<p>First, the separation between granular and non-granular proxy modes has been removed. In other words, there is no need to switch to a granular mode and reconfigure everything – proxies may be added gradually in the way which is more suitable for a real-time feature.</p>\n<p>The <code>connect</code> and <code>refresh</code> proxies can now be enabled and configured at the <code>client</code> level, while other types of proxies, which relate to channels, are configured within the <code>channel</code> block and enabled at the channel namespace level. RPC proxy configuration is now defined under a separate <code>rpc</code> section in the configuration.</p>\n<p>It is now possible to define default proxies for all event types separately - <code>connect</code>, <code>refresh</code>, <code>subscribe</code>, <code>publish</code>, <code>sub_refresh</code>, etc, – each with its own set of options. Previously, all proxies inherited the same set of options, and only endpoints and timeouts could be customized for specific proxy types. This new flexibility allows you to configure the desired proxy behavior without needing to use named proxy objects in many cases. For example, you can now define <code>connect</code> and <code>refresh</code> proxies with different sets of headers for each. While this behavior seems intuitive, it was previously only achievable by using the granular proxy mode and referencing custom proxies by name. Now, named proxy objects may be avoided in many cases where they were required before. So you can postpone using them until a more granular configuration is really required.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"defaultconfig-cli-helper\">defaultconfig cli helper<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#defaultconfig-cli-helper\" class=\"hash-link\" aria-label=\"Direct link to defaultconfig cli helper\" title=\"Direct link to defaultconfig cli helper\" translate=\"no\">​</a></h2>\n<p>To simplify the process of creating a new configuration file or discovering available options, we added a new CLI command <code>defaultconfig</code>.</p>\n<p>The <code>defaultconfig</code> command provides a way to get the configuration file with all defaults for all available configuration options. It will be possible using the command like:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo defaultconfig -c config.json</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo defaultconfig -c config.yaml</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo defaultconfig -c config.toml</span><br></div></code></pre></div></div>\n<p>Also, in dry-run mode it will be posted to STDOUT instead of file:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo defaultconfig -c config.json --dry-run</span><br></div></code></pre></div></div>\n<p>Finally, it's possible to provide this command a base configuration file - so the result will inherit option values from base file and will extend it with defaults for everything else:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo defaultconfig -c config.json --dry-run --base existing_config.json</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"defaultenv-cli-helper\">defaultenv cli helper<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#defaultenv-cli-helper\" class=\"hash-link\" aria-label=\"Direct link to defaultenv cli helper\" title=\"Direct link to defaultenv cli helper\" translate=\"no\">​</a></h2>\n<p>In addition to <code>defaultconfig</code> added <code>defaultenv</code> command which prints all config options as environment vars with default values to STDOUT:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ centrifugo defaultenv</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_HTTP_SERVER_ADDRESS=\"\"</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_HTTP_SERVER_PORT=\"8000\"</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_ADMIN_ENABLED=false</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_ADMIN_EXTERNAL=false</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_ADMIN_HANDLER_PREFIX=\"\"</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_ADMIN_INSECURE=false</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_ADMIN_PASSWORD=\"\"</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_ADMIN_SECRET=\"\"</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">...</span><br></div></code></pre></div></div>\n<p>Like <code>defaultconfig</code>, it also supports the base config file to inherit values from:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo defaultenv --base config.json</span><br></div></code></pre></div></div>\n<p>When using <code>--base</code>, if you additionally provide <code>--base-non-zero-only</code> flag – the output will contain only environment variables for keys which were set to non zero values in the base config file. For example, let's say you have Centrifugo v6 JSON configuration file:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"client\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"allowed_origins\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:8000\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"engine\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redis\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"redis\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token property\">\"address\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redis://localhost:6379\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"admin\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"enabled\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">false</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Running:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo defaultenv --base config.json --base-non-zero-only</span><br></div></code></pre></div></div>\n<p>– will output only variables set in config file to non zero value (thus <code>admin.enabled</code> skipped also):</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_CLIENT_ALLOWED_ORIGINS=\"http://localhost:8000\"</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_ENGINE_REDIS_ADDRESS=\"redis://localhost:6379\"</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CENTRIFUGO_ENGINE_TYPE=\"redis\"</span><br></div></code></pre></div></div>\n<p>We already like <code>defaultenv</code> a lot ourselves when combined with <code>grep</code>, to quickly try configuring sth in development and finding out the necessary option:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo defaultenv | grep \"HMAC\"</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"headers-emulation\">Headers emulation<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#headers-emulation\" class=\"hash-link\" aria-label=\"Direct link to Headers emulation\" title=\"Direct link to Headers emulation\" translate=\"no\">​</a></h2>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>TLDR</div><div class=\"admonitionContent_BuS1\"><p>Use headers emulation when using <code>centrifuge-js</code> SDK in browser and Centrifugo <a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy\">proxy</a> feature to add a custom header to the request from Centrifugo to the backend.</p></div></div>\n<p>The WebSocket API in web browsers does not allow setting custom HTTP headers, which makes implementing authentication for WebSocket connections from browsers more challenging.</p>\n<p>Centrifugo’s JWT authentication provides a robust solution by allowing authentication via a JWT token sent internally in the first client protocol message. However, not everyone prefers to use JWT, so many Centrifugo users have configured a connect proxy to authenticate incoming connections.</p>\n<p>Unfortunately, in such cases, only cookie-based authentication was available, as web browsers automatically include the <code>Cookie</code> header for WebSocket Upgrade requests to the same domain. Other types of authentication, such as appending a <code>Bearer</code> token in a header, were only possible by passing the token in URL parameters or with initial custom <code>data</code> sent in the connection. While these methods work, they are often inconvenient because the backend cannot easily reuse existing middlewares for authentication.</p>\n<p>A useful feature introduced in Centrifugo v6 is called <strong>Headers Emulation</strong>. This feature is available exclusively in our browser SDK, <code>centrifuge-js</code> (and only makes sense there, as other platforms allow setting headers natively). With this feature, it is now possible to provide a custom headers map in the <code>Centrifuge</code> constructor options. These values are automatically translated into HTTP headers when Centrifugo makes connection proxy requests to the backend. Internally, these custom headers are still passed to Centrifugo via the first client protocol message.</p>\n<p>We are not the first offering such a workaround BTW - those familiar with Cloudflare WebSocket API may know that Cloudflare Workers allow setting custom headers for WebSocket connections <a href=\"https://blog.cloudflare.com/do-it-again/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">in a Sec-WebSocket-Protocol header</a>. However, that approach violates WebSocket RFC, and Centrifugo provides a better implementation here moving headers passing to its own client protocol level.</p>\n<p>Here is an example of how to use <code>centrifuge-js</code> with the headers emulation feature:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"wss://example.com/connection/websocket\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string-property property\">\"headers\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token string-property property\">\"Authorization\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Bearer XXX\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>There is also a setter method in SDK to update headers later on.</p>\n<p>Note, that Centrifugo proxy configuration requires a white list of headers to proxy to the backend, the white list will still be used when working with headers sent in such a way.</p>\n<p>This feature simplifies browser-based WebSocket authentication, enabling developers to define custom headers in a familiar and efficient way.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"publication-data-mode-for-kafka-consumers\">Publication data mode for Kafka consumers<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#publication-data-mode-for-kafka-consumers\" class=\"hash-link\" aria-label=\"Direct link to Publication data mode for Kafka consumers\" title=\"Direct link to Publication data mode for Kafka consumers\" translate=\"no\">​</a></h2>\n<p>Another feature added in Centrifugo v6 simplifies integrating Centrifugo with Kafka via its asynchronous Kafka consumer. Previously, Centrifugo could integrate with Kafka topics but required a special payload format, where each message in the topic represented a Centrifugo API command. This approach worked well for Kafka topics specifically set up for Centrifugo.</p>\n<p>With Centrifugo v6, a new <a class=\"\" href=\"https://centrifugal.dev/docs/server/consumers#kafka-consumer-options\"><strong>Publication Data Mode</strong></a> has been introduced for Kafka consumers. When this mode is enabled, Centrifugo expects messages in Kafka topics to contain data ready for direct publication, rather than server API commands. It is also possible to use special Kafka headers to specify the channels to which the data should be published.</p>\n<p>The primary goal of this mode is to simplify Centrifugo's integration with existing Kafka topics, making it easier to deliver real-time messages to clients without needing to restructure the topic's payload format.</p>\n<p>Additionally, since Centrifugo allows configuring an array of asynchronous consumers, it is possible to use Kafka consumers in different modes simultaneously.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"separate-broker-and-presence-manager\">Separate broker and presence manager<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#separate-broker-and-presence-manager\" class=\"hash-link\" aria-label=\"Direct link to Separate broker and presence manager\" title=\"Direct link to Separate broker and presence manager\" translate=\"no\">​</a></h2>\n<p>Centrifugo's engine internally consists of two main components: the Broker and the PresenceManager. During the v5 lifecycle, we introduced the ability to set custom brokers and presence managers for different namespaces in Centrifugo PRO. With the v6 release, this separation is now explicit in the OSS edition as well, allowing the Broker and Presence Manager to be configured independently – see more details in <a class=\"\" href=\"https://centrifugal.dev/docs/server/engines#separate-broker-and-presence-manager\">Engines and Scalability</a> documentation chapter.</p>\n<p>Configuring the NATS broker as an alternative to Redis is now more straightforward, and adding custom brokers is simpler than before.</p>\n<p>One potentially useful application of this flexibility is using separate Redis installations for the Broker and PresenceManager, allowing them to scale independently. Alternatively, you could use NATS for a \"at most once\" broker implementation while keeping Redis for presence management. This provides more flexibility for Centrifugo OSS users to tailor their setups to their specific needs.</p>\n<p>We continue to support configuring the Redis engine in Centrifugo v6, as this approach works well for many users and remains the default recommendation. With the new configuration layout, it looks like this:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.yaml</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token key atrule\">engine</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">type</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> redis</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">redis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">address</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> localhost</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">6379</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"observability-enhancements\">Observability enhancements<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#observability-enhancements\" class=\"hash-link\" aria-label=\"Direct link to Observability enhancements\" title=\"Direct link to Observability enhancements\" translate=\"no\">​</a></h2>\n<p>Several useful metrics have been added to Centrifugo in v6 to improve monitoring and observability.</p>\n<p>The first one, <code>centrifugo_client_connections_inflight</code>, shows the number of inflight connections over a specific transport. This is particularly useful when using WebSocket with fallbacks, as it allows you to easily see the percentage of users who are unable to establish a WebSocket connection.</p>\n<p>The next metric, <code>centrifugo_command_errors_total</code>, is a counter that tracks API command errors with response code resolution.</p>\n<p>Another counter, <code>centrifugo_api_command_errors_total</code>, helps identify which API commands return errors, including Centrifugo-specific error codes.</p>\n<p>Asynchronous consumers now have dedicated metrics as well, allowing you to monitor the number of messages consumed by each consumer and the number of processing errors.</p>\n<p>Centrifugo also provides metrics for the Redis broker PUB/SUB layer, such as the number of errors and the inflight buffered messages in PUB/SUB processor workers. These metrics are invaluable for monitoring the system and gaining a deeper understanding of its state.</p>\n<p>For a full description of all available metrics in Centrifugo v6, see the <a class=\"\" href=\"https://centrifugal.dev/docs/server/observability#exposed-metrics\">exposed metrics documentation</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"actualized-grafana-dashboard\">Actualized Grafana dashboard<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#actualized-grafana-dashboard\" class=\"hash-link\" aria-label=\"Direct link to Actualized Grafana dashboard\" title=\"Direct link to Actualized Grafana dashboard\" translate=\"no\">​</a></h2>\n<p>We understand how important it is to have a solid starting point for monitoring Centrifugo. That's why we updated our <a href=\"https://grafana.com/grafana/dashboards/13039-centrifugo/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Grafana dashboard</a> to include all the new metrics added during the Centrifugo v5 lifecycle as well as those introduced in v6. The dashboard now features 42 panels (up from 22) for the OSS edition, providing a much better understanding of what's happening with your installation.</p>\n<p>We also reconsidered how rates are displayed on the dashboard. Instead of summing up rates per minute, we now display rates per second, which aligns more closely with common practices in the Prometheus ecosystem.</p>\n<img src=\"https://centrifugal.dev/img/grafana.jpg\" alt=\"Updated Grafana dashboard for Centrifugo\">\n<p>Additionally, we've expanded the Grand Chat tutorial to include a <a class=\"\" href=\"https://centrifugal.dev/docs/tutorial/monitoring\">new chapter on integrating a Prometheus and Grafana stack</a> with the tutorial messenger application.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"other-improvements\">Other improvements<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#other-improvements\" class=\"hash-link\" aria-label=\"Direct link to Other improvements\" title=\"Direct link to Other improvements\" translate=\"no\">​</a></h2>\n<p>Other improvements introduced in the v6 release include:</p>\n<ul>\n<li class=\"\">The ability to set custom <a class=\"\" href=\"https://centrifugal.dev/docs/server/configuration#http_serverinternal_tls\">TLS configurations for internal</a> HTTP endpoints. Previously, it was only possible to disable TLS for internal endpoints while keeping TLS for external ones.</li>\n<li class=\"\">Added TLS support for PostgreSQL clients, including support for asynchronous consumers from PostgreSQL outbox tables, database connections, and PostgreSQL-based push notification queue clients.</li>\n<li class=\"\">The ability to configure custom TLS settings for the proxy HTTP client.</li>\n<li class=\"\">A new <code>message_size_limit</code> option for WebTransport, which effectively limits the maximum size of individual messages sent through a WebTransport connection.</li>\n<li class=\"\">Improved logging for TLS configuration at the debug level during startup to help diagnose issues with TLS setups.</li>\n<li class=\"\">Centrifugo now supports <code>.env</code> file with environment variables – can be handy for local development.</li>\n<li class=\"\">Redis Cluster and Sentinel setups can now be fully configured using the Redis <code>address</code> option, with support for <code>redis+cluster://</code> and <code>redis+sentinel://</code> schemes. Previously, the <code>address</code> option only supported standalone single Redis setups. A key addition is the ability to set different Redis master names for setups reusing the same Sentinel nodes to manage different Redis shards. This approach also simplifies Redis configuration, especially for sharded setups, as each shard can be represented by a separate address string with its own Redis options in the <code>address</code> array. See the updated <a class=\"\" href=\"https://centrifugal.dev/docs/server/engines\">Engines and Scalability</a> documentation chapter.</li>\n<li class=\"\">Refactored logging throughout the Centrifugo source code, making it more straightforward and concise. This eliminated the dependency on the <a href=\"https://github.com/centrifugal/centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifuge</a> <code>Node</code> object in many places where it was used only for logging purposes – an awkward legacy now resolved. This refactor also allowed us to utilize the <code>zerolog</code> library for the fastest way to write logs, with strictly typed log values replacing the previously used untyped field maps.</li>\n<li class=\"\">Centrifugo users from Mayflower needed a way to determine the best Redis setup for their load profile, so we created a <a href=\"https://github.com/centrifugal/centrifuge/tree/master/_examples/redis_benchmark\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">simple benchmarking tool</a> to simulate Centrifugo-specific loads on Redis.</li>\n</ul>\n<p>Additionally, a significant amount of work has been done on the documentation:</p>\n<ul>\n<li class=\"\">All chapters have been reviewed, and configuration samples have been updated.</li>\n</ul>\n<p>Finally, we updated our official <a href=\"https://github.com/centrifugal/helm-charts\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Helm chart</a> and the <a href=\"https://github.com/centrifugal/grand-chat-tutorial\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">source code of the Grand Chat Tutorial</a> to reflect the changes in v6.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"centrifugo-pro-improvements\">Centrifugo PRO improvements<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#centrifugo-pro-improvements\" class=\"hash-link\" aria-label=\"Direct link to Centrifugo PRO improvements\" title=\"Direct link to Centrifugo PRO improvements\" translate=\"no\">​</a></h2>\n<p>Centrifugo PRO v6, as usual, inherits all the changes from the OSS edition. The configuration layout refactoring has also affected some parts of the Centrifugo PRO configuration. Notably, the new <code>defaultconfig</code> and <code>defaultenv</code> CLI commands work seamlessly with Centrifugo PRO as well.</p>\n<p>Beyond the configuration layout adjustments, several improvements have been made to Centrifugo PRO features.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"improved-channel-state-events\">Improved channel state events<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#improved-channel-state-events\" class=\"hash-link\" aria-label=\"Direct link to Improved channel state events\" title=\"Direct link to Improved channel state events\" translate=\"no\">​</a></h3>\n<p>In v6, we have taken a step forward with the Centrifugo PRO <a href=\"https://centrifugal.dev/docs/pro/channel_state_events\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">channel state events</a> feature.</p>\n<p>We addressed an edge case where the first <code>occupied</code> event in a channel might not be delivered due to a race condition. Additionally, the processing of the <code>vacated</code> events queue has been made more efficient.</p>\n<p>Furthermore, if a channel state proxy is defined in a namespace, it is no longer necessary to explicitly provide an array of events to send. Once the proxy is enabled, Centrifugo PRO will send both <code>occupied</code> and <code>vacated</code> events by default.</p>\n<p>That said, we still consider this feature to be in an alpha state.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"dedicated-postgresql-for-push-notifications-queue\">Dedicated PostgreSQL for push notifications queue<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#dedicated-postgresql-for-push-notifications-queue\" class=\"hash-link\" aria-label=\"Direct link to Dedicated PostgreSQL for push notifications queue\" title=\"Direct link to Dedicated PostgreSQL for push notifications queue\" translate=\"no\">​</a></h3>\n<p>A dedicated PostgreSQL <a href=\"https://centrifugal.dev/docs/pro/push_notifications\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">push notifications</a> queue configuration has been added in Centrifugo PRO. This means it is no longer necessary to use the same PostgreSQL instance for the push notifications queue and device management – they can now be separate.</p>\n<p>If you are using Centrifugo push notifications solely for broadcasting pushes to known FCM/APNs/HMS tokens, without utilizing the device management API, you can avoid the creation of extra tables in PostgreSQL that Centrifugo would otherwise create for device data storage and related functionality.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"tutorial-for-push-notifications\">Tutorial for push notifications<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#tutorial-for-push-notifications\" class=\"hash-link\" aria-label=\"Direct link to Tutorial for push notifications\" title=\"Direct link to Tutorial for push notifications\" translate=\"no\">​</a></h3>\n<p>We have <a class=\"\" href=\"https://centrifugal.dev/docs/tutorial/push_notifications\">extended the Grand Chat tutorial</a> to include a new appendix chapter on integrating push notifications with Centrifugo PRO. This tutorial will help you understand how to send Web push notifications to mobile devices using Centrifugo PRO and FCM.</p>\n<p>While the chapter is still under construction, it already includes all the necessary parts combined into a working implementation.</p>\n<video width=\"100%\" loop=\"\" muted=\"\" controls=\"\" src=\"/img/grand-chat-tutorial-demo-push.mp4\"></video>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"generalized-channel-patterns\">Generalized channel patterns<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#generalized-channel-patterns\" class=\"hash-link\" aria-label=\"Direct link to Generalized channel patterns\" title=\"Direct link to Generalized channel patterns\" translate=\"no\">​</a></h3>\n<p>Centrifugo PRO enhances channel configuration with the <a href=\"https://centrifugal.dev/docs/pro/channel_patterns\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Channel Patterns</a> feature. This introduces a flexible way to model channels, similar to how developers configure routes for HTTP request processing in HTTP servers.</p>\n<p>Previously, PRO users who wanted to use channel patterns had to completely replace the general namespaces mechanism with patterns. In v6, channel patterns can now be used alongside Centrifugo OSS namespaces.</p>\n<p>Each channel namespace can include a <code>pattern</code> string option to designate it as a <strong>pattern namespace</strong>. Namespaces with patterns will only be resolved if the channel matches the defined pattern. This enables the use of patterns for some channels and simple prefix-based namespacing for others, making it much easier to transition from Centrifugo OSS to Centrifugo PRO. Channel namespace configurations can now be updated gradually.</p>\n<p>Additionally, features that were previously unavailable for channel patterns, such as automatic personal channel subscriptions, are now accessible to PRO users. However, the channel patterns feature must still be explicitly enabled, as it is not enabled by default. This avoids introducing unintended side effects for setups transitioning to the PRO version, especially those with channels starting with <code>/</code> (used for channels without a namespace).</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"namespace-resolution-for-metrics\">Namespace resolution for metrics<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#namespace-resolution-for-metrics\" class=\"hash-link\" aria-label=\"Direct link to Namespace resolution for metrics\" title=\"Direct link to Namespace resolution for metrics\" translate=\"no\">​</a></h3>\n<p>Centrifugo PRO already supported channel namespace resolution for transport message sent/received metrics. This feature is very useful for setups with many namespaces, as it helps identify which namespaces consume more bandwidth.</p>\n<p>In v6, we’ve expanded this capability by providing channel namespace resolution for all Centrifugo metrics related to channels. This enhancement offers deeper insights into how Centrifugo is being used in your setup and enables data-driven business decisions.</p>\n<p>For example, you can now segment metrics by channel namespace for metrics such as:</p>\n<ul>\n<li class=\"\"><code>centrifugo_node_messages_sent_count</code> / <code>centrifugo_node_messages_received_count</code></li>\n<li class=\"\"><code>centrifugo_client_command_duration_seconds</code></li>\n<li class=\"\"><code>centrifugo_client_num_reply_errors</code></li>\n<li class=\"\">and many more</li>\n</ul>\n<p>This extended visibility makes it easier to monitor, analyze, and optimize your Centrifugo setup.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"clients-and-subscriptions-inflight\">Clients and subscriptions inflight<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#clients-and-subscriptions-inflight\" class=\"hash-link\" aria-label=\"Direct link to Clients and subscriptions inflight\" title=\"Direct link to Clients and subscriptions inflight\" translate=\"no\">​</a></h3>\n<p>Centrifugo PRO's ClickHouse analytics previously enabled building client connection distributions using client name and app version segmentation. This made it possible to understand how many clients from different environments – such as browsers, Android, or iOS devices – were currently connected to your Centrifugo setup. This functionality leveraged the fact that our SDKs pass the SDK name to the server and provide a way to redefine it.</p>\n<p>In Centrifugo v6 PRO, this information is now exposed directly as part of Prometheus metrics through the <code>centrifugo_client_connections_inflight</code> gauge. To avoid cardinality issues, Centrifugo requires explicit configuration of registered client names and versions.</p>\n<p>Additionally, Centrifugo v6 PRO introduces the <code>centrifugo_client_subscriptions_inflight</code> metric, which includes both client name and channel namespace resolution. These metrics offer valuable insights into current and historical Centrifugo usage.</p>\n<p>While these metrics are extremely useful, the ClickHouse analytics feature still provides a deeper level of resolution for individual connections and channel subscriptions. However, the use of ClickHouse can now be deferred if these new metrics provide sufficient data to address your needs.</p>\n<p>Moreover, the metrics mentioned above, including channel namespace resolution and inflight connections/subscriptions, are now integrated into Centrifugo's <a href=\"https://grafana.com/grafana/dashboards/13039-centrifugo/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">official Grafana dashboard</a>.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"sentry-integration\">Sentry integration<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#sentry-integration\" class=\"hash-link\" aria-label=\"Direct link to Sentry integration\" title=\"Direct link to Sentry integration\" translate=\"no\">​</a></h3>\n<p>The next improvement for Centrifugo PRO observability is integration with <a href=\"https://sentry.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Sentry</a>.</p>\n<p>With just <a class=\"\" href=\"https://centrifugal.dev/docs/pro/observability_enhancements#sentry-integration\">a couple of lines in the configuration</a>, you can enable this feature:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  ...</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"sentry\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"enabled\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"dsn\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"your-project-public-dsn\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>– and you will see Centrifugo PRO errors collected by your self-hosted or cloud Sentry installation.</p>\n<img src=\"https://centrifugal.dev/img/sentry.jpg\">\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"leverage-redis-replicas\">Leverage Redis replicas<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#leverage-redis-replicas\" class=\"hash-link\" aria-label=\"Direct link to Leverage Redis replicas\" title=\"Direct link to Leverage Redis replicas\" translate=\"no\">​</a></h3>\n<p>Some Centrifugo users have Redis setups configured with replication. This might be a Redis Sentinel-based primary-replica setup or a Redis Cluster where each shard consists of a primary and several replicas.</p>\n<p>Centrifugo PRO v6 <a class=\"\" href=\"https://centrifugal.dev/docs/pro/scalability#leverage-redis-replicas\">enables leveraging existing replicas</a> for specific functionalities:</p>\n<ul>\n<li class=\"\"><strong>Channel subscriptions</strong>: Move all channel subscriptions to a replica, reducing the load on the primary instance.</li>\n<li class=\"\"><strong>Presence information</strong>: Read presence data from a replica, again offloading potentially slower requests from the primary.</li>\n</ul>\n<p>These enhancements extend Centrifugo's scalability options and can help users optimize resource usage, potentially allowing them to maintain high performance on lower infrastructure resources.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"redis-cluster-sharded-pubsub\">Redis Cluster sharded PUB/SUB<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#redis-cluster-sharded-pubsub\" class=\"hash-link\" aria-label=\"Direct link to Redis Cluster sharded PUB/SUB\" title=\"Direct link to Redis Cluster sharded PUB/SUB\" translate=\"no\">​</a></h3>\n<p>Another feature now available in Centrifugo PRO is support for sharded PUB/SUB in Redis Cluster. <a href=\"https://redis.io/docs/latest/develop/interact/pubsub/#sharded-pubsub\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Sharded PUB/SUB</a> was introduced in Redis 7.0 to address the scalability challenges of PUB/SUB in Redis Cluster. In a normal PUB/SUB setup, all publications are propagated to all nodes in the Redis Cluster, reducing throughput as more Redis nodes are added. The utilization of Redis shards is usually unequal when using PUB/SUB in Redis Cluster as subscriptions land to one of the shards. With sharded PUB/SUB, the channel keyspace is divided into slots (similar to normal keys), and PUB/SUB operations are distributed across Redis Cluster nodes based on channel names.</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"custom-keyspace-partitioning\">Custom keyspace partitioning<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#custom-keyspace-partitioning\" class=\"hash-link\" aria-label=\"Direct link to Custom keyspace partitioning\" title=\"Direct link to Custom keyspace partitioning\" translate=\"no\">​</a></h4>\n<p>When using Centrifugo PRO <a class=\"\" href=\"https://centrifugal.dev/docs/pro/scalability#redis-cluster-sharded-pubsub\">with the sharded PUB/SUB feature</a>, several important considerations must be kept in mind. This feature changes how Centrifugo constructs keys and channel names in Redis compared to the standard non-sharded setup. Specifically, Centrifugo divides the channel space into a configurable number of \"partitions,\" typically 64 to 128 (though this can be adjusted based on your needs).</p>\n<p>Partitioning ensures compatibility with Redis Cluster's slot system while maintaining a manageable number of connections between Centrifugo and Redis. Each partition uses a dedicated connection for PUB/SUB communication with the Redis Cluster.</p>\n<p>Without partitioning, each Centrifugo node could potentially create up to 16,384 connections to the Redis Cluster—one for each cluster slot—which is impractical. The partitioning strategy avoids this issue and ensures efficient and scalable communication.</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"when-to-use-sharded-pubsub\">When to Use Sharded PUB/SUB<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#when-to-use-sharded-pubsub\" class=\"hash-link\" aria-label=\"Direct link to When to Use Sharded PUB/SUB\" title=\"Direct link to When to Use Sharded PUB/SUB\" translate=\"no\">​</a></h4>\n<p>Given implementation details mentioned, we recommend using Redis sharded PUB/SUB only if:</p>\n<ol>\n<li class=\"\">You are already using a Redis Cluster.</li>\n<li class=\"\">You are approaching the scalability limits of your current setup.</li>\n</ol>\n<p>Switching to sharded PUB/SUB mode, despite the changes in keys and channel names in Redis, can significantly improve your system's ability to handle larger workloads.</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"scaling-beyond-a-single-redis-cluster\">Scaling Beyond a Single Redis Cluster<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#scaling-beyond-a-single-redis-cluster\" class=\"hash-link\" aria-label=\"Direct link to Scaling Beyond a Single Redis Cluster\" title=\"Direct link to Scaling Beyond a Single Redis Cluster\" translate=\"no\">​</a></h4>\n<p>If you reach the scalability limits of a single Redis Cluster with sharded PUB/SUB, you can scale further by adding additional, isolated Redis Clusters. Centrifugo can be configured to use multiple clusters, enabling scaling similar to its consistent sharding mechanism over isolated single Redis instances. In this setup, sharding occurs across multiple Redis Clusters.</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"redis\" src=\"https://centrifugal.dev/assets/images/redis_arch-2eed8366b3d81263ea9ec2201512f964.png\" width=\"2535\" height=\"1306\" class=\"img_ev3q\"></p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"migrate-to-v6\">Migrate to v6<a href=\"https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released#migrate-to-v6\" class=\"hash-link\" aria-label=\"Direct link to Migrate to v6\" title=\"Direct link to Migrate to v6\" translate=\"no\">​</a></h2>\n<p>For those who are not using Tarantool or SockJS, migrating to Centrifugo v6 is primarily a matter of updating the Centrifugo server configuration.</p>\n<p>We understand that, given the nature and number of configuration changes, this may not be straightforward. To simplify the migration process, we've prepared an automatic configuration migration tool (which supports both file and environment configuration migration). You can find more details in the <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/migration_v6\">Centrifugo v6 migration guide</a>.</p>\n<p>The client protocol, server API protocol, and proxy event protocol have remained unchanged. Therefore, after running Centrifugo v6 with a properly updated configuration, you can expect zero issues (well, zero new issues) with your existing integrations. If you have – let us know. If you need assistance with the migration process or have any other Centrifugo-related questions, check out our <a href=\"https://centrifugal.dev/docs/getting-started/community\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">community channels</a> for support.</p>\n<p>We hope you enjoy the new Centrifugo! It's cleaner, simpler to extend, more developer-friendly, and comes with useful features. As always, let the Centrifugal force be with you 🖲</p>",
            "url": "https://centrifugal.dev/blog/2025/01/16/centrifugo-v6-released",
            "title": "Centrifugo v6 released",
            "summary": "We are excited to tell the world about Centrifugo v6 – a new major release, which is now live. This release contains fundamental changes in the configuration and adds several useful features and more observability to Centrifugo OSS and Centrifugo PRO.",
            "date_modified": "2025-01-16T00:00:00.000Z",
            "author": {
                "name": "Centrifugal team"
            },
            "tags": [
                "centrifugo",
                "release"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones",
            "content_html": "<p>While we concentrate our main efforts on Centrifugo v6 development, let's recap the most notable features and improvements introduced during Centrifugo v5 lifecycle.</p>\n<p>We are excited about how many things done in 2024 we can be proud of. Also, we are grateful to our community for the feedback, support and love towards the project we see.</p>\n<p>Let's start remembering what had happened with the project over the year.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"grand-tutorial--the-most-advanced-websocket-chat-app-tutorial\">Grand Tutorial – the most advanced WebSocket chat app tutorial<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#grand-tutorial--the-most-advanced-websocket-chat-app-tutorial\" class=\"hash-link\" aria-label=\"Direct link to Grand Tutorial – the most advanced WebSocket chat app tutorial\" title=\"Direct link to Grand Tutorial – the most advanced WebSocket chat app tutorial\" translate=\"no\">​</a></h2>\n<p>We launched the <a class=\"\" href=\"https://centrifugal.dev/docs/tutorial/intro\"><strong>Grand Tutorial</strong></a>, an official step-by-step guide how to build a complex real-time application with Centrifugo. This comprehensive tutorial walks developers through the entire process, implementing client side, server side and Centrifugo integration. Moreover, it gives answers to robust message delivery and provides performance estimation you can expect to achieve with Centrifugo.</p>\n<video width=\"100%\" loop=\"\" muted=\"\" controls=\"\" src=\"/img/grand-chat-tutorial-demo.mp4\"></video>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"built-in-asynchronous-consumers\">Built-in asynchronous consumers<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#built-in-asynchronous-consumers\" class=\"hash-link\" aria-label=\"Direct link to Built-in asynchronous consumers\" title=\"Direct link to Built-in asynchronous consumers\" translate=\"no\">​</a></h2>\n<p>While working on the Grand Chat Tutorial, we realized that Centrifugo could natively consume external queues to simplify integration with the existing backends. Thus, at some point of v5 life cycle built-in asynchronous consumers were introduced. At this point we have two types of them:</p>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/server/consumers#postgresql-outbox-consumer\">PostgreSQL Outbox Table</a>. Enables seamless integration with transactional outbox pattern for reliable  messaging. Save your model and emit event to Centrifugo in one transaction.</li>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/server/consumers#kafka-consumer\">Kafka Topics</a>. Native Kafka topic consumption makes Centrifugo even more versatile for real-time event streaming.</li>\n</ul>\n<p>You can find examples how both can be used in <a class=\"\" href=\"https://centrifugal.dev/docs/tutorial/outbox_cdc\">Broadcast using transactional outbox and CDC</a> chapter of the tutorial.</p>\n<p>In v6 we continue improving asynchronous consumers and adding a new mode for Kafka consumer – more details very soon! During v6 life cycle we can add more built-in integrations with popular queue systems.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"idempotency-key-for-publish-api\">Idempotency key for publish API<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#idempotency-key-for-publish-api\" class=\"hash-link\" aria-label=\"Direct link to Idempotency key for publish API\" title=\"Direct link to Idempotency key for publish API\" translate=\"no\">​</a></h2>\n<p>The addition of <code>idempotency_key</code> in publish and broadcast <a class=\"\" href=\"https://centrifugal.dev/docs/server/server_api#api-methods\">server API methods</a> provides an effective way to prevent duplicate messages and ensure consistency in a distributed system as it gives a way to safely make publication retries upon network failures.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"grpc-proxy-subscription-streams\">gRPC Proxy Subscription Streams<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#grpc-proxy-subscription-streams\" class=\"hash-link\" aria-label=\"Direct link to gRPC Proxy Subscription Streams\" title=\"Direct link to gRPC Proxy Subscription Streams\" translate=\"no\">​</a></h2>\n<p>Centrifugo v5.1.0 introduced an experimental support for <a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy_streams\">gRPC proxy subscription streams</a>. This feature allows developers to send individual stream of events to the client using gRPC-streaming based proxies (bidirectional or unidirectional), providing a way to integrate with third-party existing streams using GRPC streaming between Centrifugo and backend.</p>\n<p>Technically, this is quite an interesting concept – Centrifugo can multiplex different streams within a single WebSocket connection (or any other supported transport like SSE or HTTP-streaming), and the communication between Centrifugo and the backend is multiplexed within a single HTTP/2 connection (used by GRPC internally).</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/on_demand_stream_connections-7cc823b5765cd558dba320f7f7bfa439.png\" width=\"3242\" height=\"1065\" class=\"img_ev3q\"></p>\n<p>This feature makes it possible to integrate with external streams in munutes. If you've ever heard about <a href=\"http://websocketd.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WebSocketd</a> – then the idea is very similar, but over the network! With multiplexing, authentication, authorization, scalabilty Centrifugo features coming with the concept out-of-the-box.</p>\n<p>See also <a class=\"\" href=\"https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions\">Stream logs from Loki to browser with Centrifugo Websocket-to-GRPC subscriptions</a> relevant blog post.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"delta-compression-in-channels\">Delta Compression in Channels<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#delta-compression-in-channels\" class=\"hash-link\" aria-label=\"Direct link to Delta Compression in Channels\" title=\"Direct link to Delta Compression in Channels\" translate=\"no\">​</a></h2>\n<p><a class=\"\" href=\"https://centrifugal.dev/docs/server/delta_compression\">Delta compression</a> based on Fossil algorithm was introduced to minimize data transfer overhead in channels, optimizing performance and bandwidth usage during real-time message delivery. The feature is quite unique for self-hosted real-time messaging systems. And we'd like to note that now we not only support delta compression in JavaScript SDK, but recently the community contributed support for it to our Java and Python SDKs. And there is an ongoing PR to Swift SDK.</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"delta frames\" src=\"https://centrifugal.dev/assets/images/delta_abstract-9104c3b2e3b81831daecf3b400e0d798.png\" width=\"4002\" height=\"902\" class=\"img_ev3q\"></p>\n<p>See example where delta compression can be very beneficial in <a class=\"\" href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments\">Experimenting with real-time data compression by simulating a football match events\n</a> blog post.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"cache-recovery-mode-for-channels\">Cache recovery mode for channels<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#cache-recovery-mode-for-channels\" class=\"hash-link\" aria-label=\"Direct link to Cache recovery mode for channels\" title=\"Direct link to Cache recovery mode for channels\" translate=\"no\">​</a></h2>\n<p>The new <a class=\"\" href=\"https://centrifugal.dev/docs/server/cache_recovery\">cache recovery mode</a> in Centrifugo transforms channels into real-time caches by delivering only the most recent message upon resubscription. This feature has proven highly effective for <a href=\"https://www.azuracast.com/docs/developers/now-playing-data/#high-performance-updates\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">AzuraCast's \"Now Playing\"</a> functionality, ensuring effective updates for users. With Centrifugo’s cache recovery mode, the system now operates more efficiently, significantly reducing the load on the backend.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"expanded-observability\">Expanded observability<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#expanded-observability\" class=\"hash-link\" aria-label=\"Direct link to Expanded observability\" title=\"Direct link to Expanded observability\" translate=\"no\">​</a></h2>\n<p>Several new metrics were added to provide deeper insights into Centrifugo operations. A couple of examples:</p>\n<ul>\n<li class=\"\"><code>centrifugo_node_pub_sub_lag_seconds</code>: tracks lag in pub/sub processing – may be very important for server monitoring.</li>\n<li class=\"\"><code>centrifugo_client_ping_pong_duration_seconds</code>: measures client latency distribution on top of existing ping-pong mechanism with transport type resolution.</li>\n</ul>\n<p>Metrics would be one of the main focuses of Centrifugo v6, so we will continue working hard on better observability of Centrifugo.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"nats-integration-improvements\">Nats integration improvements<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#nats-integration-improvements\" class=\"hash-link\" aria-label=\"Direct link to Nats integration improvements\" title=\"Direct link to Nats integration improvements\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/server/engines#nats-raw-mode\">Raw mode</a>. Offers a way to consume existing Nats topics with 1-to-1 matching to Centrifugo channels</li>\n<li class=\"\">It's now possible to have <a class=\"\" href=\"https://centrifugal.dev/docs/server/engines#nats_allow_wildcards\">wildcard subscriptions</a> with Nats – i.e. a way to consume from many channels having only one subscription.</li>\n</ul>\n<p>The useful addition in Centrifugo PRO was a possibility to use <a class=\"\" href=\"https://centrifugal.dev/docs/pro/namespace_engines\">per-namespace brokers and presence managers</a>. So it's possible to use wildcard subscriptions with Nats in one channel namespace, and benefit from Redis broker features (presence, history cache and automatic recovery) in other channel namespaces.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"redis-integration-improvements\">Redis integration improvements<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#redis-integration-improvements\" class=\"hash-link\" aria-label=\"Direct link to Redis integration improvements\" title=\"Direct link to Redis integration improvements\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\"><a href=\"https://redis.io/blog/announcing-redis-community-edition-and-redis-stack-74/#:~:text=You%20can%20now%20set%20an%20expiration%20for%20hash%20fields.\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Redis 7.4 added possibility</a> to set per field TTL in HASH data structure. We <a class=\"\" href=\"https://centrifugal.dev/docs/server/engines#redis_presence_hash_field_ttl\">utilized this</a> for Centrifugo presence to handle online presence more efficiently and reducing the overhead of presence requests.</li>\n<li class=\"\">Global Redis Presence User Mapping. We've <a class=\"\" href=\"https://centrifugal.dev/docs/server/engines#optimize-getting-presence-stats\">provided an option</a> to drastically improve <code>presence_stats</code> performance for channels with a large number of active subscribers, making Redis-backed deployments more efficient.</li>\n<li class=\"\">Centrifugo works gracefully with Redis eviction algorithms now. It degrades in a way that clients lose their positions in channel streams, but they can <a class=\"\" href=\"https://centrifugal.dev/docs/server/history_and_recovery#how-recovery-works\">recover in idiomatic way</a>.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"better-sdks\">Better SDKs<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#better-sdks\" class=\"hash-link\" aria-label=\"Direct link to Better SDKs\" title=\"Direct link to Better SDKs\" translate=\"no\">​</a></h2>\n<p>Several improvements were made in client SDKs. Among others, we'd like to highlight the following:</p>\n<ul>\n<li class=\"\">some important connection stability and correctness improvements in Javascript SDK, for example <a href=\"https://github.com/centrifugal/centrifuge-js/pull/281\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">this one</a> where we fixed <code>WebSocket is closed before the connection is established</code> error preventing reconnect after long offline period, or <a href=\"https://github.com/centrifugal/centrifuge-js/pull/278\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">this one</a> where we worked on the correctness of quick subscribe/unsubscribe scenarios.</li>\n<li class=\"\">at some point we brought back to life our <a href=\"https://github.com/centrifugal/centrifuge-python\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Python realtime SDK</a>. For many years we did not have enough resources to do this, but now we are happy to see it alive again.</li>\n<li class=\"\">delta compression support in Javascript SDK. And later the support for it was added in Java and Python SDKs, and that <a href=\"https://github.com/centrifugal/centrifuge-java/pull/74\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">was contributed</a> by Centrifugal community members which is simply great.</li>\n<li class=\"\">a method to <a href=\"https://github.com/centrifugal/centrifuge-swift/issues/106\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">reset reconnecting state</a> in Swift SDK, which can be expanded to other SDKs in the future. Previously, we only had similar functionality in Javascript SDK where browser online/offline notifications are available.</li>\n<li class=\"\">we now have a high-quality alternative to our official Dart/Flutter SDK – see <a href=\"https://github.com/PlugFox/spinify\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://github.com/PlugFox/spinify</a> made by <a href=\"https://github.com/PlugFox\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">PlugFox</a>. An alternative may provide more idiomatic API for Dart developers, more performance and customization.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"centrifugo-pro-milestones\">Centrifugo PRO milestones<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#centrifugo-pro-milestones\" class=\"hash-link\" aria-label=\"Direct link to Centrifugo PRO milestones\" title=\"Direct link to Centrifugo PRO milestones\" translate=\"no\">​</a></h2>\n<p>In v5 release post we <a class=\"\" href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#introducing-centrifugal-labs-ltd\">introduced Centrifugal Labs</a> – a company around Centrifugo PRO product, the enhanced version of Centrifugo. It was operational since then. During the year we found new PRO customers, and generally passed very important milestones as a company.</p>\n<p>The most important for this post is that we continued to improve Centrifugo PRO with new features. Let's recap!</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"compression-improvements\">Compression improvements<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#compression-improvements\" class=\"hash-link\" aria-label=\"Direct link to Compression improvements\" title=\"Direct link to Compression improvements\" translate=\"no\">​</a></h3>\n<p>The first thing to mention regarding Centrifugo PRO is compression performance optimizations. We introduced the cache for prepared WebSocket messages to reduce CPU usage and improve compression ratio. This feature <a class=\"\" href=\"https://centrifugal.dev/blog/2024/08/19/optimizing-websocket-compression\">was very helpful for our customers</a> who have a lot of data to send over WebSocket connections. Also, <a class=\"\" href=\"https://centrifugal.dev/docs/pro/delta_at_most_once\">delta compression for channels with at most once delivery guarantees</a> is available for Centrifugo PRO users.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"better-observability\">Better observability<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#better-observability\" class=\"hash-link\" aria-label=\"Direct link to Better observability\" title=\"Direct link to Better observability\" translate=\"no\">​</a></h3>\n<p>We believe that PRO version should provide an <a class=\"\" href=\"https://centrifugal.dev/docs/pro/observability_enhancements\">enhanced observability</a> as when the business grows it's crucial to have a deep insight into the system. One notable feature is namespace segmentation for in/out message size metrics. This allows identifying namespaces which consume a lot of bandwidth. In Centrifugo v6 we will make a step forward here, stay tuned!</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"new-features-for-push-notifications\">New features for push notifications<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#new-features-for-push-notifications\" class=\"hash-link\" aria-label=\"Direct link to New features for push notifications\" title=\"Direct link to New features for push notifications\" translate=\"no\">​</a></h3>\n<p>One of the most attractive features of Centrifugo PRO is <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications\">push notifications</a> support. During the year several notable improvements were made regarding it. Let's recap.</p>\n<p><a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications#timezone-aware-push\">Timezone-aware push notifications</a> – Centrifugo PRO can now send push notifications based on device timezone. Or <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications#templating\">notification templating</a> - allows using variables and substitute them to values based on particular device metadata. Also, <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications#localizations\">notification localizations</a> - for providing translations of push content based on particular device locale. Next, <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications#push-rate-limits\">per user device rate limiting</a> - lets app developers be more careful about the number and rate of push notifications on per user device basis.</p>\n<p>And finally, better scalability of push notification device/topic storage by using reads from PostgreSQL replicas.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"channel-state-events-preview\">Channel state events preview<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#channel-state-events-preview\" class=\"hash-link\" aria-label=\"Direct link to Channel state events preview\" title=\"Direct link to Channel state events preview\" translate=\"no\">​</a></h3>\n<p>We published a <a class=\"\" href=\"https://centrifugal.dev/docs/pro/channel_state_events\">Channel state events</a> feature preview – to be notified about channel occupied and channel vacated events on the app backend. Note that we do not recommend this feature for production usage yet. While it may seem simple - the implementation is quite complex under the hood, because we try to solve important issues like event ordering, avoiding event race conditions, making sure we survive Centrifugo node restarts, scalability with Redis Cluster. This all requires a careful approach, so we want to step-by-step improve the feature based on the customer feedback. In Centrifugo v6, we make a little step forward with channel state events, will share more details very soon.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"sso-for-admin-ui\">SSO for admin UI<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#sso-for-admin-ui\" class=\"hash-link\" aria-label=\"Direct link to SSO for admin UI\" title=\"Direct link to SSO for admin UI\" translate=\"no\">​</a></h3>\n<p>We added <a class=\"\" href=\"https://centrifugal.dev/docs/pro/admin_idp_auth\">SSO for admin UI</a> using OpenID connect (OIDC)protocol, this is a very natural feature to have for corporate users who want to use their existing identity provider to authenticate in Centrifugo admin UI instead of custom password management.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/admin_idp_auth-92fc159f4d015d269d1666453dbee5e3.png\" width=\"1972\" height=\"1012\" class=\"img_ev3q\"></p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"centrifugo-v6-is-coming\">Centrifugo v6 is coming<a href=\"https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones#centrifugo-v6-is-coming\" class=\"hash-link\" aria-label=\"Direct link to Centrifugo v6 is coming\" title=\"Direct link to Centrifugo v6 is coming\" translate=\"no\">​</a></h2>\n<p>There were more improvements and features introduced during Centrifugo v5 lifecycle, here we tried to highlight the most notable ones. This year was quite productive for the project, and we hope your work was more productive due to the help of Centrifugo.</p>\n<p>We are looking forward to the upcoming year and the release of Centrifugo v6 very soon. Stay tuned for the upcoming announcement where we will introduce the new features and improvements of the new major release.</p>\n<p>Merry Christmas and Happy New Year everyone! 🎄</p>",
            "url": "https://centrifugal.dev/blog/2024/12/23/centrifugo-v5-milestones",
            "title": "Notable Centrifugo v5 milestones in 2024",
            "summary": "While we concentrate our main efforts on Centrifugo v6 development, let's recap the most notable features and improvements introduced during Centrifugo v5 lifecycle.",
            "date_modified": "2024-12-23T00:00:00.000Z",
            "author": {
                "name": "Centrifugal team"
            },
            "tags": [
                "centrifugo"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2024/08/19/optimizing-websocket-compression",
            "content_html": "<p>In a recent blog post, we <a href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">talked about the Delta Compression</a> feature of the Centrifuge/Centrifugo stack. We continue the topic of real-time data compression here, and in this post, we will show the optimizations for WebSocket compression made recently in collaboration with one of our Centrifugo PRO customers.</p>\n<p>The optimizations described here allowed our customer to reduce the transmit bandwidth used for real-time communication by 3x by enabling WebSocket compression, while keeping server CPU and memory utilization at comparable levels. This eventually resulted in notable savings of up to $12,000 per month on their bandwidth bill.</p>\n<p>The optimization is now part of our open-source <a href=\"https://github.com/centrifugal/centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifugal/centrifuge</a> library for the Go language and <a href=\"https://centrifugal.dev/docs/pro/overview\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo PRO</a> offering. However, the concepts described here can be applied not only to other Go projects but also beyond the Go ecosystem.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>Thanks</div><div class=\"admonitionContent_BuS1\"><p>Huge kudos to the Skin.Club engineering team (and Sergey in particular) for bringing us the case, evaluating the changes, providing graphs from production, and reviewing this post.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"enabling-websocket-compression\">Enabling WebSocket compression<a href=\"https://centrifugal.dev/blog/2024/08/19/optimizing-websocket-compression#enabling-websocket-compression\" class=\"hash-link\" aria-label=\"Direct link to Enabling WebSocket compression\" title=\"Direct link to Enabling WebSocket compression\" translate=\"no\">​</a></h2>\n<p>WebSocket compression allows reducing the size of messages exchanged between a client and server over a WebSocket connection. Nowadays, it's achieved using the <code>permessage-deflate</code> algorithm (see <a href=\"https://datatracker.ietf.org/doc/html/rfc7692\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RFC 7692</a>), which compresses the data before transmission. By reducing the data size, WebSocket compression helps optimize bandwidth usage and may actually lower latency due to the reduced data size over the wire.</p>\n<p><a href=\"https://github.com/centrifugal/centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifuge library</a> (the core of Centrifugo server) uses <a href=\"https://github.com/gorilla/websocket\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Gorilla WebSocket</a> library for its WebSocket transport implementation (actually, we have our own fork with minor modifications, but the core is the same). Gorilla WebSocket supports WebSocket compression by using a Go standard's library <a href=\"https://pkg.go.dev/compress/flate\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">compress/flate</a> package. Enabling compression on server side can be done by setting an <code>EnableCompression</code> option of the <a href=\"https://pkg.go.dev/github.com/gorilla/websocket#Upgrader\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">websocket.Upgrader</a>:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> upgrader </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> websocket</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Upgrader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    EnableCompression</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Then it's possible to control whether to use compression for a message by using <code>EnableWriteCompression</code> method before you are writing data to the specific connection:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">conn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">EnableWriteCompression</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">false</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Write data.</span><br></div></code></pre></div></div>\n<p>When paying for bandwidth, minimizing its usage can result in substantial cost savings, even if it leads to higher CPU and memory consumption on the server side. This is why our customer chose to enable WebSocket compression to evaluate its impact on their metrics:</p>\n<img src=\"https://centrifugal.dev/img/ws_compression_enabling.jpg\">\n<p>Sorry for the image quality; at that point, we did not know that the case would eventually lead to a blog post. However, what is important to emphasize from these graphs is that when WebSocket compression was enabled:</p>\n<ul>\n<li class=\"\">The graphs show a significant reduction in transmit bandwidth on nodes, from 7.5 to 2.5 MiB/s at peak times (or in sum across all nodes from 45 to 15 MiB/s).</li>\n<li class=\"\">We observe a notable (approximately 2x) increase in CPU usage.</li>\n<li class=\"\">We see how memory usage was affected in a bad way – instead of being super-stable we now observe sporadic spikes, up to 2.5x larger values.</li>\n</ul>\n<p>Despite the negative change of CPU and memory utilization, migrating to WebSocket compression was still economically beneficial for our customer. As mentioned earlier, this step alone allowed the company to save up to $12,000 per month on bandwidth costs, while the additional resource usage costs were comparably much lower.</p>\n<p>Fortunately, at Centrifugal Labs, we had <a href=\"https://github.com/gorilla/websocket/issues/203\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">prior experience</a> with WebSocket compression performance overhead. This allowed us to suggest some optimizations to mitigate the resource usage degradation. To understand these optimizations, we first need to explore how WebSocket compression works under the hood.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"permessage-deflate-in-gorillawebsocket\">Permessage-deflate in gorilla/websocket<a href=\"https://centrifugal.dev/blog/2024/08/19/optimizing-websocket-compression#permessage-deflate-in-gorillawebsocket\" class=\"hash-link\" aria-label=\"Direct link to Permessage-deflate in gorilla/websocket\" title=\"Direct link to Permessage-deflate in gorilla/websocket\" translate=\"no\">​</a></h2>\n<p>In Go, when you need to compress the WebSocket frame your starting point is <a href=\"https://pkg.go.dev/compress/flate\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">compress/flate</a> package from Go standard library. That's what Gorilla WebSocket uses to implement <code>permessage-deflate</code> compression. You can find the implementation in <a href=\"https://github.com/gorilla/websocket/blob/3810b2346f49a47aa0b99c23a7aa619d5f5dcf80/compression.go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">compression.go</a> file.</p>\n<p>When compressing frames the user may choose one of the <a href=\"https://pkg.go.dev/compress/flate#pkg-constants\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">predefined levels of compression</a> - from 1 (BestSpeed) to 9 (BestCompression).</p>\n<p>One of the important compression implementation details is that Gorilla WebSocket uses <code>sync.Pool</code> for <code>flate.Writer</code> (separate pool for each level of compression) and <code>flate.Reader</code> types:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    flateWriterPools </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">maxCompressionLevel </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-</span><span class=\"token plain\"> minCompressionLevel </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">sync</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Pool</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    flateReaderPool  </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> sync</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Pool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">interface</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> flate</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewReader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>Having a pool for these objects allows Gorilla WebSocket to reuse existing readers/writers and generally avoid additional allocations. The benefit of having a pool for <code>flate.Writer</code> is enormous, especially when considering how large this type is:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">package</span><span class=\"token plain\"> main</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"unsafe\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"compress/flate\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">main</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> w flate</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Writer</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">unsafe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Sizeof</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">w</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Output: 656640</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>So it's around 650 KB on its own! For <code>flate.Reader</code> it's not that critical as it's only 16B overhead. Having pools makes a lot of sense and helps Gorilla WebSocket to perform well when compressing data.</p>\n<p>Thus when you call <a href=\"https://pkg.go.dev/github.com/gorilla/websocket#Conn.WriteMessage\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Conn.WriteMessage</a> or use <a href=\"https://pkg.go.dev/github.com/gorilla/websocket#Conn.NextWriter\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Conn.NextWriter</a> API for connections with compression negotiated under the hood Gorilla Websocket library acquires <code>flate.Writer</code> from the proper pool for the time of write operation.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>No context takeover</div><div class=\"admonitionContent_BuS1\"><p>Gorilla WebSocket implements <code>permessage-deflate</code> <a href=\"https://datatracker.ietf.org/doc/html/rfc7692#section-7.1.1.1\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">without context takeover</a> only. Implementing a context takeover would mean attaching a <code>flate.Writer</code> to each connection – which is very memory inefficient as we can see. Though it can be still a viable money-effective bandwidth optimization for WebSocket – especially if the app does not have a lot of concurrent connections.</p></div></div>\n<p>But below we will describe a scenario in which having just a <code>sync.Pool</code> is not enough to avoid extra allocations.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"preparedmessage-type\">PreparedMessage type<a href=\"https://centrifugal.dev/blog/2024/08/19/optimizing-websocket-compression#preparedmessage-type\" class=\"hash-link\" aria-label=\"Direct link to PreparedMessage type\" title=\"Direct link to PreparedMessage type\" translate=\"no\">​</a></h2>\n<p>Let’s delve into the specifics of Centrifuge/Centrifugo. Both the Centrifuge library and the Centrifugo server implement the PUB/SUB pattern for real-time messaging. Clients can establish a WebSocket connection and subscribe to multiple channels, all multiplexed over a single connection. A single channel can have many online subscribers — often thousands. Our APIs enable publishing a message to a channel, ensuring it is delivered to all online subscribers of that channel. Here’s a simple illustration of the PUB/SUB pattern:</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"pub_sub\" src=\"https://centrifugal.dev/assets/images/pub_sub-5477abf6fb38219fc0848d9c9c3dc2b1.png\" width=\"2924\" height=\"1231\" class=\"img_ev3q\"></p>\n<p>When you publish a message to a channel, our WebSocket server puts the message into each subscribed client's individual queue. Client queues are processed concurrently in separate goroutines, and messages are eventually sent to the client over the transport implementation. WebSocket is the most frequently used transport implementation in our case.</p>\n<p>With WebSocket compression enabled, broadcasts to a channel with many online subscribers result in many compression operations that compress the same message concurrently. This, in turn, results in a load on <code>sync.Pool</code>, which grows significantly at the point of such a broadcast. Given the size of each <code>flate.Writer</code> object, this is exactly what causes the temporary memory spikes we observed on the graphs above. More allocations also mean more CPU utilization.</p>\n<p>In your app, you may observe something like this in the heap profile:</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"img\" src=\"https://centrifugal.dev/assets/images/ws_compression_profile-bf2e06a8ae8136796695c14f133d8e8f.jpg\" width=\"2186\" height=\"1252\" class=\"img_ev3q\"></p>\n<p>That's why, at some point in the past, Gary Burd (author of Gorilla WebSocket) <a href=\"https://github.com/gorilla/websocket/pull/211\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">added</a> a <a href=\"https://pkg.go.dev/github.com/gorilla/websocket#PreparedMessage\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">PreparedMessage</a> type. This is a helper type that allows caching the constructed WebSocket frame depending on various negotiated connection options (compression used or not, compression level).</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">How PreparedMessage is defined</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// PreparedMessage caches on the wire representations of a message payload.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Use PreparedMessage to efficiently send a message payload to multiple</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// connections. PreparedMessage is especially useful when compression is used</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// because the CPU and memory expensive compression operation can be executed</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// once for a given set of compression options.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> PreparedMessage </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    messageType </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">int</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    data        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    mu          sync</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Mutex</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    frames      </span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">prepareKey</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">preparedFrame</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// prepareKey defines a unique set of options to cache prepared frames in PreparedMessage.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> prepareKey </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    isServer         </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">bool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    compress         </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">bool</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    compressionLevel </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">int</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// preparedFrame contains data in wire representation.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> preparedFrame </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    once sync</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Once</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    data </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Then there is a method <a href=\"https://pkg.go.dev/github.com/gorilla/websocket#Conn.WritePreparedMessage\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">func (*Conn) WritePreparedMessage</a>, which should be used to write data to all connections interested in a message — the proper WebSocket frame will be created once and then automatically re-used. By the way, this is an example of the <a href=\"https://github.com/gorilla/websocket/blob/ce903f6d1d961af3a8602f2842c8b1c3fca58c4d/prepared.go#L72\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">elegant use</a> of <code>sync.Once</code>.</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">Using PreparedMessage</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">preparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> websocket</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewPreparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">websocket</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">TextMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> conn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">WritePreparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">preparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>This means that if we broadcast the same prepared message to many connections, we remove the excessive load on <code>sync.Pool</code>, just taking a one <code>flate.Writer</code> from the pool instead of many. This way, we avoid large memory spikes due to big size of <code>flate.Writer</code> objects and <code>sync.Pool</code> growth.</p>\n<p>For broadcasts, <code>PreparedMessage</code> approach significantly reduces CPU usage by minimizing the need to construct and compress WebSocket frames, especially as the number of concurrent subscribers increases. Additionally, it reduces the allocation of large <code>flate.Writer</code> objects, further optimizing CPU utilization.</p>\n<p>Gorilla WebSocket contains <a href=\"https://github.com/gorilla/websocket/blob/3810b2346f49a47aa0b99c23a7aa619d5f5dcf80/conn_broadcast_test.go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">benchmarks</a> which compare message broadcast to many connections with enabled compression without and with <code>PreparedMessage</code> usage. Let's run them:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">❯ </span><span class=\"token keyword\" style=\"font-style:italic\">go</span><span class=\"token plain\"> test </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-</span><span class=\"token plain\">run xxx </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-</span><span class=\"token plain\">bench BenchmarkBroadcast </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-</span><span class=\"token plain\">benchmem</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Compression_100_conn</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8</span><span class=\"token plain\">             </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">198619</span><span class=\"token plain\"> ns</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t   </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">14113</span><span class=\"token plain\"> B</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t    </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">301</span><span class=\"token plain\"> allocs</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CompressionPrepared_100_conn</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8</span><span class=\"token plain\">      </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">42643</span><span class=\"token plain\"> ns</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t   </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">12320</span><span class=\"token plain\"> B</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t     </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">19</span><span class=\"token plain\"> allocs</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Compression_1000_conn</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8</span><span class=\"token plain\">           </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1797432</span><span class=\"token plain\"> ns</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t  </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">123864</span><span class=\"token plain\"> B</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t   </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">3001</span><span class=\"token plain\"> allocs</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CompressionPrepared_1000_conn</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8</span><span class=\"token plain\">    </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">649506</span><span class=\"token plain\"> ns</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t   </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">11421</span><span class=\"token plain\"> B</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t     </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">19</span><span class=\"token plain\"> allocs</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Compression_10000_conn</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8</span><span class=\"token plain\">         </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">16506132</span><span class=\"token plain\"> ns</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1040709</span><span class=\"token plain\"> B</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t  </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">30007</span><span class=\"token plain\"> allocs</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">CompressionPrepared_10000_conn</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8</span><span class=\"token plain\">  </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">7702265</span><span class=\"token plain\"> ns</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t   </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">11631</span><span class=\"token plain\"> B</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op\t     </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">21</span><span class=\"token plain\"> allocs</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">op</span><br></div></code></pre></div></div>\n<p>Benchmarks demonstrate that <code>PreparedMessage</code> significantly reduces memory allocations during broadcasts, with the impact becoming more significant as the number of connections increases.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"preparedmessage-cache\">PreparedMessage cache<a href=\"https://centrifugal.dev/blog/2024/08/19/optimizing-websocket-compression#preparedmessage-cache\" class=\"hash-link\" aria-label=\"Direct link to PreparedMessage cache\" title=\"Direct link to PreparedMessage cache\" translate=\"no\">​</a></h2>\n<p>For Centrifuge/Centrifugo though, we couldn't directly use <code>PreparedMessage</code> in the part of the code responsible for preparing messages for channel broadcasts. This is because doing so would introduce a dependency on a WebSocket-specific type in a layer of code that should remain agnostic to the underlying real-time transport.</p>\n<p>To avoid this, we chose not to rely on the <code>PreparedMessage</code> type in the broadcasting preparation layer. Instead, we implemented a cache of <code>PreparedMessage</code> types within the WebSocket transport implementation layer.</p>\n<p>We use the data to be sent to the WebSocket connection as a cache key, and the <code>PreparedMessage</code> object as a value. The WebSocket transport implementation checks the cache before constructing <code>PreparedMessage</code> and has a high chance of finding it. The TTL (time-to-live) of each cache entry can be kept very short (something like 1 second should be sufficient). The size of the cache should be comparable to the total size of all different messages being broadcasted concurrently. For most WebSocket applications, a cache size of several megabytes should be more than enough.</p>\n<p>We used <a href=\"https://github.com/maypok86/otter\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">maypok86/otter</a> cache for our implementation, but there are a lot of other options in Go ecosystem.</p>\n<p>Here is how we initialize the cache:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">otter</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">MustBuilder</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">websocket</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">PreparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">int</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">CompressionPreparedMessageCacheSize</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Cost</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">key </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> value </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">websocket</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">PreparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">uint32</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">2</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">uint32</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">len</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">WithTTL</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Second</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Build</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>And use it at the point where we want to write data to the WebSocket connection on a transport layer:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> usePreparedMessage </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    key </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> convert</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">BytesToString</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    preparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ok </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">preparedCache</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Get</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token plain\">ok </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> err </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        preparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> websocket</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewPreparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">messageType</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">preparedCache</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Set</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> preparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">conn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">WritePreparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">preparedMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">else</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>You may wonder why the cache at this level can make a difference compared to simply using <code>conn.WriteMessage</code>. At first glance, the probability of finding a <code>PreparedMessage</code> in the cache during a concurrent broadcast seems similar to getting a <code>flate.Writer</code> from the <code>sync.Pool</code> for reuse. However, that's not entirely true, because the <code>WriteMessage</code> method of Gorilla WebSocket acquires a <code>flate.Writer</code> for the duration of the write operation, which involves a syscall and generally takes much more time than our cache operations. Additionally, two subsequent writes that reuse the <code>flate.Writer</code> will construct the frame from scratch, whereas a cached <code>PreparedMessage</code> allows us to avoid this.</p>\n<p>It's important to note that the fact we use a cache is actually a Centrifuge-specific detail to avoid dependency on a type from the WebSocket library in places where we don't want to be tied to specific transport implementations. In your WebSocket app that uses Gorilla WebSocket, you can likely create a <code>PreparedMessage</code> directly and use it when iterating over connections to which you need to broadcast the message. This approach might even lead to more predictable behavior than what we have in the Centrifuge case with a cache. While the cache approach is somewhat probabilistic, the probability is high, and as we'll see below, it works well.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"mentioning-klauspostcompress\">Mentioning klauspost/compress<a href=\"https://centrifugal.dev/blog/2024/08/19/optimizing-websocket-compression#mentioning-klauspostcompress\" class=\"hash-link\" aria-label=\"Direct link to Mentioning klauspost/compress\" title=\"Direct link to Mentioning klauspost/compress\" translate=\"no\">​</a></h2>\n<p>Since we are discussing compression optimizations in the Go ecosystem, it would be wrong not to mention the <a href=\"https://github.com/klauspost/compress\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">klauspost/compress</a> library created by Klaus Post. The library is backwards compatible with standard Go packages but provides faster implementations of compression algorithms. Additionally, it offers capabilities beyond those of the standard library.</p>\n<p>Specifically, it provides <a href=\"https://github.com/klauspost/compress?tab=readme-ov-file#stateless-compression\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Stateless Compression</a>, which theoretically could help with the use case we described here.</p>\n<p>However, in our initial evaluations of <a href=\"https://github.com/klauspost/compress\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">klauspost/compress</a> (switching to its <code>flate</code> implementation and also trying the Stateless Compression feature), we did not observe notable improvements in our specific set of benchmarks (and actually observed regressions in some cases). So, for now, we are sticking with the standard Go library. Nevertheless, it's still a promising direction for research, and we may re-evaluate our findings in the future. Since we use our own fork of Gorilla WebSocket, we can easily switch to <a href=\"https://github.com/klauspost/compress\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">klauspost/compress</a> if we prove the benefit.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"results\">Results<a href=\"https://centrifugal.dev/blog/2024/08/19/optimizing-websocket-compression#results\" class=\"hash-link\" aria-label=\"Direct link to Results\" title=\"Direct link to Results\" translate=\"no\">​</a></h2>\n<p>Alright, it's time to see how enabling <code>PreparedMessage</code> cache helped the customer with a resource usage.</p>\n<p>First, let's look at CPU after enabling the cache in production:</p>\n<img src=\"https://centrifugal.dev/img/ws_compression_results_cpu.png\">\n<p>We see that the average CPU utilization is significantly reduced at peak times, closer to values before enabling the WebSocket compression.</p>\n<p>Second, let's look at memory usage:</p>\n<img src=\"https://centrifugal.dev/img/ws_compression_results_mem.png\">\n<p>As we can see on the graph – the pattern smoothed a lot making the usage on each Centrifugo node much more stable. It's still not the same as it was with compression disabled, we can see minor spikes – but the positive effect of <code>PreparedMessage</code> cache is clear.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2024/08/19/optimizing-websocket-compression#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>Enabling WebSocket compression helped our customer significantly reduce monthly costs, and with PreparedMessage cache optimization, we were able to keep resource usage at a comparable level. So, it was mostly a no-brainer improvement.</p>\n<p>If you are a Go developer, our open-source <a href=\"https://github.com/centrifugal/centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifugal/centrifuge</a> library for Go supports WebSocket compression and contains the cache described here — so if you build your app on top of Centrifuge, you just need to enable a couple of options to benefit from it:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">websocketConfig </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">WebsocketConfig</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    Compression</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    CompressionPreparedMessageCacheSize</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1048576</span><span class=\"token plain\"> </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// 1 MB. </span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>If we talk about Go, the technique with prepared message is also applicable to other WebSocket libraries, not just Gorilla WebSocket. For example, with <a href=\"https://github.com/gobwas/ws\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">gobwas/ws</a>, it seems possible to construct a prepared compressed WebSocket frame, though it involves slightly more work than with Gorilla WebSocket, which provides a handy <code>PreparedMessage</code> type for this and abstracts the complexity of frame building (based on the set of connection-negotiated options).</p>\n<p>That's it for today. In some cases, when working with WebSocket broadcasts, the bandwidth may be reduced even more — check out our post about <a href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Delta Compression</a>.</p>",
            "url": "https://centrifugal.dev/blog/2024/08/19/optimizing-websocket-compression",
            "title": "Performance optimizations of WebSocket compression in Go application",
            "summary": "In this post, we explore how WebSocket compression can optimize bandwidth costs. We also discuss strategies to minimize the CPU and memory overhead associated with the enabled WebSocket compression from the Go ecosystem perspective.",
            "date_modified": "2024-08-19T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "centrifuge",
                "websocket",
                "compression",
                "performance"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync",
            "content_html": "<p>Centrifugo and its main building block Centrifuge library for Go both provide a way for clients to receive a stream of events in channels using Subscription objects. Also, there is an automatic history recovery feature which allows clients catching up with missed publications after the reconnect to the WebSocket server and restore the state of a real-time component. While the continuity in the stream is not broken clients can avoid re-fetching a state from the main application database – which optimizes a scenario when many real-time connections reconnect all within a short time interval (for example, during a load balancer restart) by reducing the excessive load on the application database.</p>\n<p>Usually, our users who use recovery features load the document state from the backend, then establish a real-time Subscription to apply changes to the component state coming from the real-time channel. After that the component stays synchronized, even after network issues – due to Centrifugo recovery feature the document state becomes actual again since client catches up the state from the channel history stream.</p>\n<p>There are several hidden complexities in the process though and things left for users to implement. We want to address those here.</p>\n<p>In the post we assume that you are using a channnel with history configured and recovery on, i.e. using a namespace with <code>history_size</code>, <code>history_ttl</code> and <code>force_recovery</code> on, see more details in <a class=\"\" href=\"https://centrifugal.dev/docs/server/history_and_recovery\">History and recovery\n</a> doc chapter.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"complexities-in-state-sync\">Complexities in state sync<a href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync#complexities-in-state-sync\" class=\"hash-link\" aria-label=\"Direct link to Complexities in state sync\" title=\"Direct link to Complexities in state sync\" translate=\"no\">​</a></h2>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"gap-in-time\">Gap in time<a href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync#gap-in-time\" class=\"hash-link\" aria-label=\"Direct link to Gap in time\" title=\"Direct link to Gap in time\" translate=\"no\">​</a></h3>\n<p>The first edge case comes from the fact that there is a possible gap in time between initial loading of the state from the main app database and real-time subscription establishment. Some messages could be published in between of state loading and real-time subscription establishment. So there is a chance that due to this gap in time the component will live in the inconsistent state until the next application page reload. For many apps this is not critical at all, or due to message rates happens very rarely. But in this post we will look at the possible approach to avoid such a case.</p>\n<p>Or imagine a situation when state is loaded, but real-time subscription is delayed due to some temporary error. This increases a gap in time and a chance to miss an intermediary update.</p>\n<p>Centrifugo channel stream offsets are not binded to the application business models in any way, so it's not possible to initially subscribe to the real-time channel and receive all updates happened since the snapshot of the document loaded from the database. There is a way to solve this though, we will cover it shortly.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"re-sync-upon-lost-continuity\">Re-sync upon lost continuity<a href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync#re-sync-upon-lost-continuity\" class=\"hash-link\" aria-label=\"Direct link to Re-sync upon lost continuity\" title=\"Direct link to Re-sync upon lost continuity\" translate=\"no\">​</a></h3>\n<p>Another complexity which is left to the user is the need to react on <code>recovered: false</code> flag provided by the SDK when client can not catch up the state upo re-subscription. This may happen due to channel history retention configuration, or simply because history was lost. In this case our SDKs provide users <code>wasRecovering: true</code> and <code>recovered: false</code> flags, and we suggest re-fetching the document state from the backend in such cases. But while you re-fetch the state you are still receiving real-time updates from the subscription – which leads us to something similar to the problem described above, same race conditions may happen leaving the component in the inconsistent state until reload.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"late-delivery-of-real-time-updates\">Late delivery of real-time updates<a href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync#late-delivery-of-real-time-updates\" class=\"hash-link\" aria-label=\"Direct link to Late delivery of real-time updates\" title=\"Direct link to Late delivery of real-time updates\" translate=\"no\">​</a></h3>\n<p>One more possible problem to discuss is a late delivery of real-time messages.</p>\n<p>When you want to reliably stream document changes to Centrifugo (without loosing any update, due to temporary network issues for example) and keep the order of changes (to not occasionally apply property addition and deletion is different order on the client side) your best bet is using transactional outbox or CDC approaches. So that changes in the database are made atomic and there is a guarantee that the update will be soon issued to the real-time channel of Centrifuge-based server or Centrifugo. Usually transactional outbox or CDC can also maintain correct order of event processing, thus correct order of publising to the real-time channel.</p>\n<p>But this means that upon loading a real-time component it may receive non-actual real-time updates from the real-time subscription – due to outbox table or CDC processing lag. We need a way for the client side to understand whether the update must be applied to the document state or not. Sometimes it's possible to understand due to the nature of component. Like receiving an update with some identifier which already exists in the object on client side. But what if update contains deletion of some property of object? This will mean that object may rollback to non-actual state, then will receive next real-time updates which will move it to back to the actual state. We want to avoid such modifications leading to temporary state glitches at all. Not all cases allow having idempotent real-time updates.</p>\n<p>Even when you are not using outbox/CDC you can still hit a situation of late real-time message delivery. Let's suppose you publish messages to Centrifugal channel synchronously over server publish API reducing the chance of having a lag from the outbox/CDC processing. But the lag may still present. Because while message travelling towards subscriber through Centrifugo, subscriber can load a more freshy initial state from the main database and subscribe to the real-time channel.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"core-principles-of-solution\">Core principles of solution<a href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync#core-principles-of-solution\" class=\"hash-link\" aria-label=\"Direct link to Core principles of solution\" title=\"Direct link to Core principles of solution\" translate=\"no\">​</a></h2>\n<p>In this post we will write a <code>RealTimeDocument</code> JavaScript class designed to synchronize a document state. This class handles initial data loading, real-time updates, and state re-synchronization when required. It should solve the problems described above.</p>\n<p>The good thing is that this helper class is compact enough to be implemented in any programming language, so you can apply it (or the required part of it) for other languages where we already have real-time SDKs.</p>\n<p>We will build the helper on top of several core principles:</p>\n<ul>\n<li class=\"\">The document has an incremental version which is managed atomically and transactionally on the backend.</li>\n<li class=\"\">Initial state loading returns document state together with the current version, loading happens with at least <code>read committed</code> transactional isolation level (default in many databases, ex. PostgreSQL)</li>\n<li class=\"\">All real-time updates published to the document channel have the version attached, and updates are published to the channel in the correct version order.</li>\n</ul>\n<p>We already discussed the approach in our <a class=\"\" href=\"https://centrifugal.dev/docs/tutorial/intro\">Grand tutorial</a> – but now want to generalize it as a re-usable pattern.</p>\n<p>After writing a <code>RealTimeDocument</code> wrapper we will apply it to a simple example of synchronizing counter increments across multiple devices reliably to demonstrate it works. Eventually we get best from two worlds – leveraging Centrifugo publication cache to avoid excessive load on the backend upon massive reconnect and proper document state in all scenarios.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"top-level-api-of-realtimedocument\">Top-level API of RealTimeDocument<a href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync#top-level-api-of-realtimedocument\" class=\"hash-link\" aria-label=\"Direct link to Top-level API of RealTimeDocument\" title=\"Direct link to Top-level API of RealTimeDocument\" translate=\"no\">​</a></h2>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> subscription </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'counter'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> realTimeDocument </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">RealTimeDocument</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    subscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Wraps Subscription.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">load</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token known-class-name class-name\" style=\"color:rgb(255, 203, 107)\">Promise</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">document</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> any</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> version</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> number </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Must load the actual document state and version from the database.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Ex. return { document: result.document, version: result.version };</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">applyUpdate</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">currentDocument</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> any</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">update</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> any</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token parameter\">any</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Must apply update to the document.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// currentDocument.value += update.increment;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// return currentDocument;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">compareVersion</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">currentVersion</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> number</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">update</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> any</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> number </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">|</span><span class=\"token plain\"> </span><span class=\"token parameter keyword null nil\" style=\"font-style:italic\">null</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Must compare versions in real-time publication and current doc version.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// const newVersion = publication.data.version;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// return newVersion &gt; currentVersion ? newVersion : null;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">onChange</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter literal-property property\">document</span><span class=\"token parameter operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token parameter\"> any</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Will be called once the document is loaded for the first time and every time</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// the document state is updated. This is where application may render things</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// based on the document data.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">realTimeDocument</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">startSync</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"implementing-solution\">Implementing solution<a href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync#implementing-solution\" class=\"hash-link\" aria-label=\"Direct link to Implementing solution\" title=\"Direct link to Implementing solution\" translate=\"no\">​</a></h2>\n<p>To address the gap between state load and real-time subscription establishment the obvious solution which is possible with Centrifugal stack is to make the real-time subscription first, and only after that load the state from the backend. This eliminates the possibility to miss messages. But until the state is loaded we need to buffer real-time publications and then apply them to the loaded state.</p>\n<p>Here is where the concept of having incremental document version helps – we can collect messages in the buffer, and then apply only those with version greater than current document version. So that the object will have the correct state after the initial load.</p>\n<p>Here is how we can process real-time publications:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#subscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#isLoaded</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Buffer messages until initial state is loaded.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#messageBuffer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">push</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Process new messages immediately if initial state is already loaded.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> newVersion </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#compareVersion</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#version</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">newVersion </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">===</span><span class=\"token plain\"> </span><span class=\"token keyword null nil\" style=\"font-style:italic\">null</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Skip real-time publication, non actual version.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#document</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#applyUpdate</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#version</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> newVersion</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#onChange</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>And we also need to handle <code>subscribed</code> event properly and load the initial document state from the backend:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#subscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'subscribed'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">wasRecovering</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">recovered</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Successfully re-attached to a stream, nothing else to do.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">else</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Re-syncing due to failed recovery.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#reSync</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">else</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Load data for the first time.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#loadDocumentApplyBuffered</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>For the initial load <code>this.#loadDocumentApplyBuffered()</code> will be called. Here is how it may look like:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">async </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">#loadDocumentApplyBuffered</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">try</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> result </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#load</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#document</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#version</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">version</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#isLoaded</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#processBufferedMessages</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">catch</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Retry the loading, in the final snippet it's implemented</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// and uses exponential backoff for the retry process.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>After loading the state we prosess buffered real-time publications inside <code>#processBufferedMessages</code> method:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">#processBufferedMessages</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#messageBuffer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">forEach</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">msg</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> newVersion </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#compareVersion</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">msg</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#version</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">newVersion</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Otherwise, skip buffered publication.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#document</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#applyUpdate</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> msg</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#version</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> newVersion</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Clear the buffer after processing.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#messageBuffer</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Only call onChange with final document state.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#onChange</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>This way the initial state is loaded correctly. Note also, that version comparisons also help with handling late delivered real-time updates – we now simply skip them inside <code>on('publication')</code> callback.</p>\n<p>Let's go back and look how to manage stream continuity loss:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">recovered</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Successfully re-attached to a stream, nothing else to do.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">else</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Re-syncing due to failed recovery.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#reSync</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>In this case we call <code>#reSync</code> method:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">#reSync</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#isLoaded</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">false</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Reset the flag to collect new messages to the buffer.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">#messageBuffer</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">this</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">#loadDocumentApplyBuffered</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>It basically clears up the class state and calls <code>#loadDocumentApplyBuffered</code> again – repeating the initial sync procedure.</p>\n<p>That's it. Here is <a href=\"https://raw.githubusercontent.com/centrifugal/centrifuge/master/_examples/document_sync/rtdocument.js\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">a full code</a> for the <code>RealTimeDocument</code> class. Note, it also contains backoff implementation to handle possible error while loading the document state from the backend endpoint.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"lets-apply-it\">Let's apply it<a href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync#lets-apply-it\" class=\"hash-link\" aria-label=\"Direct link to Let's apply it\" title=\"Direct link to Let's apply it\" translate=\"no\">​</a></h2>\n<p>I've made a <a href=\"https://github.com/centrifugal/centrifuge/tree/master/_examples/document_sync\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">POC</a> with Centrifuge library to make sure this works.</p>\n<p>In that example I tried to apply <code>RealTimeDocument</code> class to synchronize state of the counter. Periodically timer is incremented on a random value in range [0,9] on the backend and these increments are published to the real-time channel. Note, I could simply publish counter value in every publication over WebSocket – but intentionally decided to send counter increments instead. To make sure nothing is lost during state synchronization so counter value is always correct on the client side.</p>\n<p>Here is a demo:</p>\n<div class=\"vimeo-full-width\"><iframe src=\"/img/docsync.mp4\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen=\"\"></iframe></div>\n<p>Let's look at how <code>RealTimeDocument</code> class was used in the example:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> counterContainer </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"counter\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> client </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'ws://localhost:8000/connection/websocket'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> subscription </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'counter'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> realTimeDocument </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">RealTimeDocument</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    subscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">load</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> response </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">fetch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'/api/counter'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> result </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> response</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">document</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">value</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">version</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> result</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">version</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">applyUpdate</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> update</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+=</span><span class=\"token plain\"> update</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">increment</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">compareVersion</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">currentVersion</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> update</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> newVersion </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> update</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">version</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> newVersion </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"> currentVersion </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">?</span><span class=\"token plain\"> newVersion </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword null nil\" style=\"font-style:italic\">null</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">onChange</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        counterContainer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">textContent</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">debug</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Note – we can call sync even before connect.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">realTimeDocument</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">startSync</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Things to observe:</p>\n<ul>\n<li class=\"\">We return <code>{\"version\":4823,\"value\":21656}</code> from <code>/api/counter</code></li>\n<li class=\"\">Send <code>{\"version\":4824,\"increment\":9}</code> over real-time channel</li>\n<li class=\"\">Counter updated every 250 milliseconds, history size is 20, retention 10 seconds</li>\n<li class=\"\">Upon going offline for a short period we see that <code>/api/counter</code> endpoint not called at all - state fully cought up from Centrifugo history stream</li>\n<li class=\"\">Upon going offline for a longer period Centrifugo was not able to recover the state, so we re-fetched data from scratch and attached to the stream again.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>In this post, we walked through a practical implementation of a <code>RealTimeDocument</code> class using Centrifugal stack for the real-time state synchronization to achieve proper eventually consistent state of the document when using real-time updates. We mentioned possible gotchas when trying to sync the state in real-time and described a generic solution to it.</p>\n<p>You don't need to always follow the solution here. As I mentioned it's possible that your app does not require handling all these edge cases, or they could be handled in alternative ways – this heavily depends on your app business logic.</p>\n<p>Note, that with some changes you can make <code>RealTimeDocument</code> class to behave properly and support graceful degradation behaviour. If there are issues with real-time subscription you can still load the document and display it, and then re-sync the state as soon as a real-time system becomes available (successful subscription).</p>",
            "url": "https://centrifugal.dev/blog/2024/06/03/real-time-document-state-sync",
            "title": "Proper real-time document state synchronization within Centrifugal ecosystem",
            "summary": "A simple yet cool example of document state synchronization on top of Centrifugal stack. The end goal is to effectively and reliably synchronize document state to application users – to avoid race condition between state load and updates coming from the real-time subscription.",
            "date_modified": "2024-06-03T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "centrifuge",
                "websocket",
                "docsync"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments",
            "content_html": "<p>Optimizing data transfer over WebSocket connections can significantly reduce bandwidth costs. Compressing data usually leads to memory and CPU resource usage overhead – but in many cases it worth doing anyway since it positively impacts the final bill from the provider (bandwidth cost reduction overweights resource usage increase).</p>\n<p>Centrifugo v5.4.0 introduced <a class=\"\" href=\"https://centrifugal.dev/docs/server/delta_compression\">delta compression</a> feature. But before implementing it we wanted a playground which could demonstrate the potential benefit of using delta compression in Centrifugo channels.</p>\n<p>This post outlines our approach to estimating the potential profit from implementing delta compression. It demonstrates the reduction in data transfer using once concrete use case across various configurations, including different Centrifugo protocol formats and the additional use of WebSocket permessage-deflate compression. Although these numbers can vary significantly depending on the data, we believe the results are valuable for providing a general understanding of Centrifugo compression options. This information can help Centrifugo users apply these insights to their use cases.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"about-delta-compression\">About delta compression<a href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments#about-delta-compression\" class=\"hash-link\" aria-label=\"Direct link to About delta compression\" title=\"Direct link to About delta compression\" translate=\"no\">​</a></h2>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"delta frames\" src=\"https://centrifugal.dev/assets/images/delta_abstract-9104c3b2e3b81831daecf3b400e0d798.png\" width=\"4002\" height=\"902\" class=\"img_ev3q\"></p>\n<p>For a good overview of delta compression topic for the real-time messaging applications I suggest starting with a <a href=\"https://ably.com/blog/message-delta-compression\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">blog post in Ably engineeiring blog</a>.</p>\n<p>Centrifugo is very similar to Ably in many aspects (though self-hosted), so everything said in the linked post equally applies to Centrifugo use cases too. Though we have differences in the final implementation, one notable is that we are using <a href=\"https://fossil-scm.org/home/doc/tip/www/delta_format.wiki\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Fossil</a> delta algorithm in Centrifugo instead of VCDIFF. The reason over VCDIFF was mainly two factors:</p>\n<ul>\n<li class=\"\">availability of several Fossil delta implementations, specifically there are good libraries for Go (see <a href=\"https://github.com/shadowspore/fossil-delta\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">shadowspore/fossil-delta</a>), and for Javascript - <a href=\"https://github.com/dchest/fossil-delta-js\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">fossil-delta-js</a>.</li>\n<li class=\"\">the compactness of the algorithm implementation – under 500 lines of code in JavaScript</li>\n</ul>\n<p>The compactness property is nice because there are no OSS Fossil implementations for Java, Dart and Swift – languages we have SDKs for – so we may have to implement this algorithm in the future ourselves.</p>\n<p>Having said this all, let's proceed to the description of experiment we did to understand possible benefits of various compression techniques, and delta compression in particular.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"experiment-overview\">Experiment Overview<a href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments#experiment-overview\" class=\"hash-link\" aria-label=\"Direct link to Experiment Overview\" title=\"Direct link to Experiment Overview\" translate=\"no\">​</a></h2>\n<p>In the experiment, we simulated a football match, sending the entire game state over a WebSocket connection upon every match event. Our compression playground looks like this:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/el_classico.mp4\"></video>\n<p>It visualizes only the score, but under the hood there are other game changes happen – will be shown below.</p>\n<p>We tested various configurations to evaluate the effectiveness of data compression if different cases. In each setup the same game data was sent over the wire. The data then was captured using <a href=\"https://www.wireshark.org/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WireShark</a> with the filter:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">tcp.srcport == 8000 &amp;&amp; websocket</span><br></div></code></pre></div></div>\n<p>This is how WebSocket packets look in Wireshark when applying a filter mentioned above:</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"wireshark\" src=\"https://centrifugal.dev/assets/images/compression_wireshark-b44531ca65e851165c62331d10d661f4.png\" width=\"2984\" height=\"1362\" class=\"img_ev3q\"></p>\n<p>Bytes captured show the entire overhead from packets in the game channel going from server to client (including TCP/IP overhead).</p>\n<p>The source code of the experiment may be found on Github as a <a href=\"https://github.com/centrifugal/centrifuge/tree/master/_examples/compression_playground\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifuge library example</a>. You can run it to inspect the exact WebSocket frames in each scenario.</p>\n<p>To give reader a general idea about data, we sent 30 publications with the entire football game state, for example here is a first message in a match (2 teams, 11 players):</p>\n<details class=\"details_lb9f alert alert--info details_b_Ee\" data-collapsed=\"true\"><summary>Click to see the data</summary><div><div class=\"collapsibleContent_i85q\"><p></p><div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token property\">\"homeTeam\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Real Madrid\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"players\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"John Doe\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jane Smith\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Alex Johnson\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Chris Lee\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Pat Kim\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Sam Morgan\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jamie Brown\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Casey Davis\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Morgan Garcia\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Taylor White\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jordan Martinez\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token property\">\"awayTeam\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Barcelona\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"players\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Robin Wilson\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Drew Taylor\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"RED_CARD\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jessie Bailey\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Casey Flores\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jordan Walker\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Charlie Green\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Alex Adams\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Morgan Thompson\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Taylor Clark\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jordan Hernandez\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jamie Lewis\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div><p></p></div></div></details>\n<p>Then we send intermediary states – someone scores goal, gets yellow card, being subsctituted. And here is the end message in simulation (final scores, final events attached to corresponding players):</p>\n<details class=\"details_lb9f alert alert--info details_b_Ee\" data-collapsed=\"true\"><summary>Click to see the data</summary><div><div class=\"collapsibleContent_i85q\"><p></p><div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token property\">\"homeTeam\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Real Madrid\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"score\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">3</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"players\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"John Doe\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"YELLOW_CARD\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">6</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">39</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jane Smith\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Alex Johnson\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Chris Lee\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"GOAL\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">84</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Pat Kim\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Sam Morgan\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jamie Brown\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">9</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Casey Davis\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"YELLOW_CARD\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">81</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Morgan Garcia\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">15</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"GOAL\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">30</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"YELLOW_CARD\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">57</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"GOAL\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">62</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"RED_CARD\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">66</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Taylor White\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"YELLOW_CARD\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">18</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">42</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">45</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"YELLOW_CARD\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">69</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"RED_CARD\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">72</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jordan Martinez\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">21</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">24</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token property\">\"awayTeam\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Barcelona\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"score\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">3</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"players\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Robin Wilson\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Drew Taylor\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"RED_CARD\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"GOAL\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">12</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jessie Bailey\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Casey Flores\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"YELLOW_CARD\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">78</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jordan Walker\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">33</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Charlie Green\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"GOAL\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">51</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"GOAL\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">60</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">75</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Alex Adams\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Morgan Thompson\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"YELLOW_CARD\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">27</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">48</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Taylor Clark\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">3</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">87</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jordan Hernandez\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Jamie Lewis\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"events\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"YELLOW_CARD\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">36</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"type\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SUBSTITUTE\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                  </span><span class=\"token property\">\"minute\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">54</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">               </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">         </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div><p></p></div></div></details>\n<p>When we used Protobuf encoding for game state we serialized the data according to this Protobuf schema:</p>\n<details class=\"details_lb9f alert alert--info details_b_Ee\" data-collapsed=\"true\"><summary>Click to see the Protobuf schema for the game state</summary><div><div class=\"collapsibleContent_i85q\"><p></p><div class=\"language-protobuf codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-protobuf codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">syntax = \"proto3\";</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">package centrifugal.centrifuge.examples.compression_playground;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">option go_package = \"./;apppb\";</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">enum EventType {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  UNKNOWN = 0; // Default value, should not be used</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  GOAL = 1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  YELLOW_CARD = 2;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  RED_CARD = 3;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  SUBSTITUTE = 4;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">message Event {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  EventType type = 1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  int32 minute = 2;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">message Player {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  string name = 1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  repeated Event events = 2;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">message Team {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  string name = 1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  int32 score = 2;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  repeated Player players = 3;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">message Match {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  int32 id = 1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  Team home_team = 2;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  Team away_team = 3;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div></code></pre></div></div><p></p></div></div></details>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"results-breakdown\">Results Breakdown<a href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments#results-breakdown\" class=\"hash-link\" aria-label=\"Direct link to Results Breakdown\" title=\"Direct link to Results Breakdown\" translate=\"no\">​</a></h2>\n<p>Below are the results of our experiment, comparing different protocols and compression settings:</p>\n<table><thead><tr><th>Protocol</th><th>Compression</th><th>Delta</th><th>Bytes sent</th><th>Percentage</th></tr></thead><tbody><tr><td>JSON over JSON</td><td>No</td><td>No</td><td>40251</td><td>100.0 (base)</td></tr><tr><td>JSON over JSON</td><td>Yes</td><td>No</td><td>15669</td><td>38.93</td></tr><tr><td>JSON over JSON</td><td>No</td><td>Yes</td><td>6043</td><td>15.01</td></tr><tr><td>JSON over JSON</td><td>Yes</td><td>Yes</td><td>5360</td><td>13.32</td></tr><tr><td>--</td><td>--</td><td>--</td><td>--</td><td>--</td></tr><tr><td>JSON over Protobuf</td><td>No</td><td>No</td><td>39180</td><td>97.34</td></tr><tr><td>JSON over Protobuf</td><td>Yes</td><td>No</td><td>15542</td><td>38.61</td></tr><tr><td>JSON over Protobuf</td><td>No</td><td>Yes</td><td>4287</td><td>10.65</td></tr><tr><td>JSON over Protobuf</td><td>Yes</td><td>Yes</td><td>4126</td><td>10.25</td></tr><tr><td>--</td><td>--</td><td>--</td><td>--</td><td>--</td></tr><tr><td>Protobuf over Protobuf</td><td>No</td><td>No</td><td>16562</td><td>41.15</td></tr><tr><td>Protobuf over Protobuf</td><td>Yes</td><td>No</td><td>13115</td><td>32.58</td></tr><tr><td>Protobuf over Protobuf</td><td>No</td><td>Yes</td><td>4382</td><td>10.89</td></tr><tr><td>Protobuf over Protobuf</td><td>Yes</td><td>Yes</td><td>4473</td><td>11.11</td></tr></tbody></table>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"results-analysis\">Results analysis<a href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments#results-analysis\" class=\"hash-link\" aria-label=\"Direct link to Results analysis\" title=\"Direct link to Results analysis\" translate=\"no\">​</a></h2>\n<p>Let's now discuss the results we observed in detail.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"json-over-json\">JSON over JSON<a href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments#json-over-json\" class=\"hash-link\" aria-label=\"Direct link to JSON over JSON\" title=\"Direct link to JSON over JSON\" translate=\"no\">​</a></h3>\n<p>In this case we are sending JSON data with football match game state over JSON Centrifugal protocol.</p>\n<ol>\n<li class=\"\">\n<p>JSON over JSON (No Compression, No Delta)\nBytes Sent: 40251\nPercentage: 100.0%\nAnalysis: This is a baseline scenario, with no compression and no delta, results in the highest amount of data being sent. But very straightforward to implement.</p>\n</li>\n<li class=\"\">\n<p>JSON over JSON (With Compression, No Delta)\nBytes Sent: 15669\nPercentage: 38.93%\nAnalysis: Enabling compression reduces the data size significantly to 38.93% of the original, showcasing the effectiveness of deflate compression. See <a class=\"\" href=\"https://centrifugal.dev/docs/transports/websocket#websocket_compression\">how to configure compression</a> in Centrifugo, note that it comes with CPU and memory overhead which depends on your load profile.</p>\n</li>\n<li class=\"\">\n<p>JSON over JSON (No Compression, With Delta)\nBytes Sent: 6043\nPercentage: 15.01%\nAnalysis: Using delta compression without deflate compression reduces data size to 15.01% for this use case, only changes are being sent after the initial full payload. See how to enable <a class=\"\" href=\"https://centrifugal.dev/docs/server/delta_compression\">delta compression in channels</a> in Centrifugo. The nice thing about using delta compression instead of deflate compression is that deltas require less and more predictable resource overhead.</p>\n</li>\n<li class=\"\">\n<p>JSON over JSON (With Compression and Delta)\nBytes Sent: 5360\nPercentage: 13.32%\nAnalysis: Combining both compression and delta further reduces the data size to 13.32%, achieving the highest efficiency in this category. The benefit is not huge, because we already send small deltas here.</p>\n</li>\n</ol>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"json-over-protobuf\">JSON over Protobuf<a href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments#json-over-protobuf\" class=\"hash-link\" aria-label=\"Direct link to JSON over Protobuf\" title=\"Direct link to JSON over Protobuf\" translate=\"no\">​</a></h3>\n<p>In this case we are sending JSON data with football match game state over Protobuf Centrifugal protocol.</p>\n<ol start=\"5\">\n<li class=\"\">\n<p>JSON over Protobuf (No Compression, No Delta)\nBytes Sent: 39180\nPercentage: 97.34%\nAnalysis: Switching to Protobuf encoding of Centrifugo protocol but still sending JSON data slightly reduces the data size to 97.34% of the JSON over JSON baseline. The benefit here comes from the fact Centrifugo does not introduce a lot of its own protocol overhead – Protobuf is more compact. But we still send JSON data as Protobuf payloads – that's why it's generally comparable with a baseline.</p>\n</li>\n<li class=\"\">\n<p>JSON over Protobuf (With Compression, No Delta)\nBytes Sent: 15542\nPercentage: 38.61%\nAnalysis: Compression with Protobuf encoding brings similar benefits as with JSON, reducing the data size to 38.61%.</p>\n</li>\n<li class=\"\">\n<p>JSON over Protobuf (No Compression, With Delta)\nBytes Sent: 4287\nPercentage: 10.65%\nAnalysis: Delta compression with Protobuf is effective, reducing data to 10.65%. It's almost x10 reduction in bandwidth compared to the baseline!</p>\n</li>\n</ol>\n<p>I guess at this point you may be curious how delta frames look like in case of JSON protocol. Here is a screenshot:</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"delta frames\" src=\"https://centrifugal.dev/assets/images/delta_frames-7d915a6b62f3cbcbfa4e0a1d738e79df.png\" width=\"2828\" height=\"572\" class=\"img_ev3q\"></p>\n<ol start=\"8\">\n<li class=\"\">JSON over Protobuf (With Compression and Delta)\nBytes Sent: 4126\nPercentage: 10.25%\nAnalysis: This combination provides the best results for JSON over Protobuf, reducing data size to 10.25% from the baseline.</li>\n</ol>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"protobuf-over-protobuf\">Protobuf over Protobuf<a href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments#protobuf-over-protobuf\" class=\"hash-link\" aria-label=\"Direct link to Protobuf over Protobuf\" title=\"Direct link to Protobuf over Protobuf\" translate=\"no\">​</a></h3>\n<p>In this case we are sending Protobuf binary data with football match game state over Protobuf Centrifugal protocol.</p>\n<ol start=\"9\">\n<li class=\"\">\n<p>Protobuf over Protobuf (No Compression, No Delta)\nBytes Sent: 16562\nPercentage: 41.15%\nAnalysis: Using Protobuf for both encoding and transmission <strong>without any compression or delta</strong> reduces data size to 41.15%. So you may get the most efficient setup with nice bandwidth reduction. But the cost is more complex data encoding.</p>\n</li>\n<li class=\"\">\n<p>Protobuf over Protobuf (With Compression, No Delta)\nBytes Sent: 13115\nPercentage: 32.58%\nAnalysis: Compression reduces the data size to 32.58%. Note, that in this case it's not very different from JSON case.</p>\n</li>\n<li class=\"\">\n<p>Protobuf over Protobuf (No Compression, With Delta)\nBytes Sent: 4382\nPercentage: 10.89%\nAnalysis: Delta compression is again very effective here, reducing the data size to 10.89%. Again - comparable to JSON case.</p>\n</li>\n<li class=\"\">\n<p>Protobuf over Protobuf (With Compression and Delta)\nBytes Sent: 4473\nPercentage: 11.11%\nAnalysis: Combining both methods results in a data size of 11.11%. Even more than in JSON case. That's bacause binary data is not compressed very well with deflate algorithm.</p>\n</li>\n</ol>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<ul>\n<li class=\"\">\n<p>WebSocket permessage-deflate compression significantly reduces the amount of data transferred over WebSocket connections. While it incurs CPU and memory overhead, it may be still worth using from a total cost perspective.</p>\n</li>\n<li class=\"\">\n<p>Delta compression makes perfect sense for channels where data changes only slightly between publications. In our experiment, it resulted in a tenfold reduction in bandwidth usage.</p>\n</li>\n<li class=\"\">\n<p>Using binary data in combination with the Centrifugo Protobuf protocol provides substantial bandwidth reduction even without deflate or delta compression. However, this comes at the cost of increased data format complexity. An additional benefit of using the Centrifugo Protobuf protocol is its faster marshalling and unmarshalling on the server side compared to the JSON protocol.</p>\n</li>\n</ul>\n<p>For Centrifugo, these results highlighted the potential of implementing delta compression, so we proceeded with it. The benefit depends on the nature of the data being sent – you can achieve even greater savings if you have larger messages that are very similar to each other.</p>",
            "url": "https://centrifugal.dev/blog/2024/05/30/real-time-data-compression-experiments",
            "title": "Experimenting with real-time data compression by simulating a football match events",
            "summary": "This post shows the potential profit of enabling delta compression in channels and demonstrates the reduction of data transfer in various scenarios, including different Centrifugo protocol formats and using WebSocket permessage-deflate compression.",
            "date_modified": "2024-05-30T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "websocket",
                "compression"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions",
            "content_html": "<p>As of version 5.1.0, Centrifugo introduces an experimental yet powerful extension that promises to simplify the data delivery process to the browser using GRPC streams. We believe it may help you to solve some practical tasks in minutes. Let's dive into how this feature works and how you can leverage it in your applications integrating with <a href=\"https://grafana.com/oss/loki/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Loki</a> real-time log streaming capabilities.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"what-are-proxy-subscription-streams\">What Are Proxy Subscription Streams?<a href=\"https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions#what-are-proxy-subscription-streams\" class=\"hash-link\" aria-label=\"Direct link to What Are Proxy Subscription Streams?\" title=\"Direct link to What Are Proxy Subscription Streams?\" translate=\"no\">​</a></h2>\n<p><a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy_streams\">Proxy Subscription Streams</a> support pushing data directly to Centrifugo client channel subscriptions from your application backend over GRPC streams. This feature is designed to facilitate individual data streams to clients as soon as they subscribe to a channel, acting as a bridge between WebSocket connections from clients and GRPC streams to the backend. It supports both unidirectional (backend to client) and bidirectional (both ways) streams, thereby enhancing flexibility in data streaming.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/on_demand_stream_connections-7cc823b5765cd558dba320f7f7bfa439.png\" width=\"3242\" height=\"1065\" class=\"img_ev3q\"></p>\n<p>The design is inspired by <a href=\"http://websocketd.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Websocketd</a> server – but while Websocketd transforms data from programs running locally, Centrifugo provides a more generic network interface with GRPC. And all other features of Centrifugo like connection authentication, online presence come as a great bonus.</p>\n<p>In the documentation for Proxy Subscription Streams we mentioned streaming logs from Loki as one of the possible use cases. Let's expand on the idea and implement the working solution in just 10 minutes.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"demo-and-source-code\">Demo and source code<a href=\"https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions#demo-and-source-code\" class=\"hash-link\" aria-label=\"Direct link to Demo and source code\" title=\"Direct link to Demo and source code\" translate=\"no\">​</a></h2>\n<p>Here is a demo of what we well get:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/loki.mp4\"></video>\n<p>Take a look at <a href=\"https://github.com/centrifugal/examples/tree/master/v5/subscription_streams_loki\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">full source code on Github</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"setting-up-loki\">Setting Up Loki<a href=\"https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions#setting-up-loki\" class=\"hash-link\" aria-label=\"Direct link to Setting Up Loki\" title=\"Direct link to Setting Up Loki\" translate=\"no\">​</a></h2>\n<p><a href=\"https://grafana.com/oss/loki/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Loki</a> is a horizontally-scalable, highly-available, multi-tenant log aggregation system inspired by Prometheus. It is designed to be very cost-effective and easy to operate, making it a perfect candidate for our real-time log streaming example.</p>\n<p>We will build the example using Docker Compose, all we have to do for the example is to include Loki image to <code>docker-compose.yml</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token key atrule\">services</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">loki</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">image</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> grafana/loki</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">2.9.5</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">ports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"3100:3100\"</span><br></div></code></pre></div></div>\n<p>Loki can ingest logs via various methods, including Promtail, Grafana Agent, Fluentd, and more. For simplicity, we will send logs to Loki ourselves from the Go application.</p>\n<p>To send logs to Loki, we can use the HTTP API that Loki provides. This is a straightforward way to push logs directly from an application. The example below demonstrates how to create a simple Go application that generates logs and sends them to Loki using HTTP POST requests.</p>\n<p>For this post we will be using Go language to implement the backend part. But it could be any other programming language.</p>\n<p>First, let's some code to send a log entries to Loki:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tlokiPushEndpoint </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://loki:3100/loki/api/v1/push\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> lokiPushMessage </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tStreams </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">lokiStream </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`json:\"streams\"`</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> lokiStream </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tStream </span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`json:\"stream\"`</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tValues </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`json:\"values\"`</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">sendLogMessageToLoki</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token plain\"> context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tsources </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"backend1\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"backend2\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"backend3\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tsource </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> sources</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">rand</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Intn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">len</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">sources</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tlogMessage </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> fmt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Sprintf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"log from %s source\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> source</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tpayload </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> lokiPushMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tStreams</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">lokiStream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tStream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t\t</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"source\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> source</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tValues</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">fmt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Sprintf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"%d\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Now</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">UnixNano</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> logMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tjsonData</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Marshal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">payload</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tresp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Post</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlokiPushEndpoint</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"application/json\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> bytes</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewBuffer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">jsonData</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">defer</span><span class=\"token plain\"> resp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Body</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Close</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> resp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">StatusCode </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">StatusNoContent </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> fmt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Errorf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"unexpected status code: %d\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> resp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">StatusCode</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">sendLogsToLoki</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">select</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Done</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\">time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">After</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">200</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Millisecond</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\terr </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">sendLogMessageToLoki</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"error sending log to Loki:\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">continue</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">main</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cancel </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> signal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NotifyContext</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Background</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> syscall</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SIGTERM</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> syscall</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SIGINT</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">defer</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">cancel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">sendLogsToLoki</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>This program defines a <code>sendLogsToLoki</code> function that constructs a log entry and sends it to Loki using its HTTP API. It continuously generates log messages every 200 milliseconds.</p>\n<p>The <code>lokiPushMessage</code> struct is structured to match the JSON payload expected by Loki's <a href=\"https://grafana.com/docs/loki/latest/reference/api/#push-log-entries-to-loki\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>/loki/api/v1/push</code></a> endpoint. Each log entry consists of a set of labels (in the Stream map) and log line values, where each value is a two-element array containing the timestamp and the log line. The timestamp is in nanoseconds to match Loki's expected format.</p>\n<p>Note, in the example we randomly set log entry <code>source</code> label choosing between <code>backend1</code>, <code>backend2</code> and <code>backend3</code> values.</p>\n<p>At this point our program pushes some logs to Loki, now let's add Centrifugo to consume them from browser in real-time.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"configuring-centrifugo\">Configuring Centrifugo<a href=\"https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions#configuring-centrifugo\" class=\"hash-link\" aria-label=\"Direct link to Configuring Centrifugo\" title=\"Direct link to Configuring Centrifugo\" translate=\"no\">​</a></h2>\n<p>Adding Centrifugo is also rather straightforward:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token key atrule\">services</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">centrifugo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">image</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> centrifugo/centrifugo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">v5.3.0</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">restart</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> unless</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\">stopped</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">volumes</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> ./centrifugo/config.json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">/centrifugo/config.json</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> centrifugo </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\">c config.json</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">expose</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">-</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8000</span><br></div></code></pre></div></div>\n<p>Where <code>config.json</code> is:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"client_insecure\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"allowed_origins\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:9000\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"proxy_subscribe_stream_endpoint\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"grpc://backend:12000\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"proxy_subscribe_stream_timeout\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"3s\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"namespaces\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"logs\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token property\">\"proxy_subscribe_stream\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Note, we enabled <code>client_insecure</code> option here – this is to keep example short, but in real live you may benefit from Centrifugo authentication: <a class=\"\" href=\"https://centrifugal.dev/docs/server/authentication\">JWT-based</a> or <a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy#connect-proxy\">proxy-based</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"writing-frontend\">Writing frontend<a href=\"https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions#writing-frontend\" class=\"hash-link\" aria-label=\"Direct link to Writing frontend\" title=\"Direct link to Writing frontend\" translate=\"no\">​</a></h2>\n<div class=\"language-html codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-html codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token doctype punctuation\" style=\"color:rgb(199, 146, 234);font-style:italic\">&lt;!</span><span class=\"token doctype doctype-tag\" style=\"color:rgb(199, 146, 234);font-style:italic\">DOCTYPE</span><span class=\"token doctype\" style=\"color:rgb(199, 146, 234);font-style:italic\"> </span><span class=\"token doctype name\" style=\"color:rgb(199, 146, 234);font-style:italic\">html</span><span class=\"token doctype punctuation\" style=\"color:rgb(199, 146, 234);font-style:italic\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">lang</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">en</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">head</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">meta</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">charset</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">UTF-8</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">title</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\">Streaming logs with Centrifugo and Loki</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">title</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">head</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">app</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">form</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">input</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag special-attr attr-name\" style=\"color:rgb(255, 203, 107)\">onsubmit</span><span class=\"token tag special-attr attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag special-attr attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag special-attr attr-value value javascript language-javascript function\" style=\"color:rgb(130, 170, 255)\">subscribeToLogs</span><span class=\"token tag special-attr attr-value value javascript language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token tag special-attr attr-value value javascript language-javascript\" style=\"color:rgb(255, 85, 114)\">event</span><span class=\"token tag special-attr attr-value value javascript language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token tag special-attr attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">input</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">type</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">text</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">query</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">autocomplete</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">off</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">placeholder</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">Enter log query</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">/&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">button</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">submit</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">type</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">submit</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\">SUBSCRIBE</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">button</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">form</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">logs</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag special-attr attr-name\" style=\"color:rgb(255, 203, 107)\">style</span><span class=\"token tag special-attr attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag special-attr attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag special-attr attr-value value css language-css property\" style=\"color:rgb(255, 85, 114)\">margin-top</span><span class=\"token tag special-attr attr-value value css language-css punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token tag special-attr attr-value value css language-css\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag special-attr attr-value value css language-css number\" style=\"color:rgb(247, 140, 108)\">20</span><span class=\"token tag special-attr attr-value value css language-css unit\" style=\"color:rgb(255, 85, 114)\">px</span><span class=\"token tag special-attr attr-value value css language-css punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token tag special-attr attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">ul</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">lines</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">ul</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">src</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">https://unpkg.com/centrifuge@^5/dist/centrifuge.js</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token script\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">src</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">app.js</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token script\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><br></div></code></pre></div></div>\n<p>In the final version we've also included some CSS to this HTML to make it look a bit nicer.</p>\n<p>And our Javascript code in <code>app.js</code>:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> logs </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'logs'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> lines </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'lines'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> queryInput </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'query'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> button </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'submit'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">subscribeToLogs</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">preventDefault</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> query </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> queryInput</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">value</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token plain\">query</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">alert</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'Please enter a query.'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    queryInput</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">disabled</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    button</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">disabled</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'ws://localhost:9000/connection/websocket'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> subscription </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'logs:stream'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">data</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">query</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> query </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    subscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> logLine </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">line</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> logItem </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">createElement</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'li'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        logItem</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">textContent</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> logLine</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        lines</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">appendChild</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">logItem</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        logs</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">scrollTop</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> logs</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">scrollHeight</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    subscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>In the final example we've also added Nginx container to serve static files and proxy WebSocket connections to Centrifugo. Check it out in the source code.</p>\n<p>When user enters Loki query to input, subscription goes to Centrifugo and Centrifugo then realizes it's a proxy stream subscription (since channel belongs to <code>logs</code> channel namespace). Centrifugo then calls the backend GRPC endpoint (<code>backend:12000</code>) and expect it to implement unidirectional GRPC stream contract. Our last part here - to implement it.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"handle-subscription-stream-on-the-go-side\">Handle subscription stream on the Go side<a href=\"https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions#handle-subscription-stream-on-the-go-side\" class=\"hash-link\" aria-label=\"Direct link to Handle subscription stream on the Go side\" title=\"Direct link to Handle subscription stream on the Go side\" translate=\"no\">​</a></h2>\n<p>On your backend, we'll implement a GRPC service that interacts with Loki to tail logs and then re-send them to Centrifugo subscription stream. Let's implement such service.</p>\n<p>We first need to take Centrifugo <a href=\"https://github.com/centrifugal/centrifugo/blob/master/internal/proxyproto/proxy.proto\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">proxy.proto</a> definitions. And we will implement <code>SubscribeUnidirectional</code> method from it.</p>\n<p>You need to install <a href=\"https://grpc.io/docs/protoc-installation/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>protoc</code></a>, also install plugins for Go and GRPC:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">go install google.golang.org/protobuf/cmd/protoc-gen-go@latest</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest</span><br></div></code></pre></div></div>\n<p>And then:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">protoc -I ./ proxy.proto --go_out=./ --go-grpc_out=./</span><br></div></code></pre></div></div>\n<p>This will generate Protobuf messages and GRPC code required for writing GRPC service. We can use generated definitions now:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"log\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"fmt\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tpb </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"backend/internal/proxyproto\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"github.com/grafana/loki/pkg/logproto\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"google.golang.org/grpc\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"google.golang.org/grpc/credentials/insecure\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tlokiGRPCAddress  </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"loki:9095\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> streamerServer </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tpb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">UnimplementedCentrifugoProxyServer</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tlokiQuerierClient logproto</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">QuerierClient</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> clientData </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tQuery </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`json:\"query\"`</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">streamerServer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">SubscribeUnidirectional</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\treq </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">pb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeRequest</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tstream pb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">CentrifugoProxy_SubscribeUnidirectionalServer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> cd clientData</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\terr </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Unmarshal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">req</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">cd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> fmt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Errorf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"error unmarshaling data: %w\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tquery </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">logproto</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">TailRequest</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tQuery</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> cd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Query</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cancel </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">WithCancel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">defer</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">cancel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tlogStream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">lokiQuerierClient</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Tail</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> query</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> fmt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Errorf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"error querying Loki: %w\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tstarted </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Now</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"unidirectional subscribe called with request\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> req</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">defer</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"unidirectional subscribe finished, elapsed\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Since</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">started</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\terr </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Send</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">pb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">StreamSubscribeResponse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tSubscribeResponse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">pb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeResponse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">select</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\">stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Done</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">default</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tresp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> logStream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Recv</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> fmt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Errorf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"error receiving from Loki stream: %v\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> entry </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">range</span><span class=\"token plain\"> resp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Entries </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tline </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> fmt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Sprintf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"%s: %s\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> entry</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Timestamp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Format</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"2006-01-02T15:04:05.000Z07:00\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> entry</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Line</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\terr </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Send</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">pb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">StreamSubscribeResponse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t\tPublication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">pb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Publication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">Data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`{\"line\":\"`</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> line </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`\"}`</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">main</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tquerierConn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> grpc</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Dial</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">lokiGRPCAddress</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> grpc</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">WithTransportCredentials</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">insecure</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewCredentials</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatalf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"failed to dial Loki: %v\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tquerierClient </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> logproto</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewQuerierClient</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">querierConn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\taddr </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\":12000\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tlis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> net</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Listen</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"tcp\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> addr</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatalf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"failed to listen: %v\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\ts </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> grpc</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewServer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">grpc</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">MaxConcurrentStreams</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">math</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">MaxUint32</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tpb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">RegisterCentrifugoProxyServer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">streamerServer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlokiQuerierClient</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> querierClient</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Server listening on\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> addr</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Serve</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">lis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatalf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"failed to serve: %v\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Things to note:</p>\n<ul>\n<li class=\"\">Loki also supports GRPC interface to tail logs, so we use it here. We could also use Loki WebSocket endpoint <a href=\"https://grafana.com/docs/loki/latest/reference/api/#stream-log-messages\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>/loki/api/v1/tail</code></a> but this would mean establishing new connection for every tail operation - with GRPC we can use many concurrent tail requests all multiplexed over a single network connection.</li>\n<li class=\"\">When subscription stream initialized from Centrifugo side we start tailing logs from Loki and resend them to Centrifugo</li>\n<li class=\"\">Centrifugo then packs data to WebSocket connection and delivers to browser.</li>\n</ul>\n<div class=\"theme-admonition theme-admonition-caution admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>caution</div><div class=\"admonitionContent_BuS1\"><p>Note, we bypass some security considerations in this example. In practice you must be more careful with query supplied by user in the form - validate and sanitize it before passing to Loki. Proxy subscription GRPC contract allows you to communicate custom errors with the client-side.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>Subscription streams may be a very powerful generic feature in your arsenal. Here we've shown how simple it could be to make a proof of concept of the real-time application which consumes individual data from third-party streaming provider.</p>\n<p>Centrifugo provides WebSocket SDKs for popular languages used to build UI layer, provides authentication and proper management of real-time connections. And with subscription streams feature Centrifugo gives you an answer on how to quickly translate real-time data based on individual query to user.</p>",
            "url": "https://centrifugal.dev/blog/2024/03/18/stream-loki-logs-to-browser-with-websocket-to-grpc-subscriptions",
            "title": "Stream logs from Loki to browser with Centrifugo Websocket-to-GRPC subscriptions",
            "summary": "Centrifugo has GRPC subscription streams feature, in this post we show how this feature may simplify a task of delivering data to application UI in real-time. We integrate with Loki, injest log entries and stream logs to the browser based on user-supplied query",
            "date_modified": "2024-03-18T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "loki",
                "grpc"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications",
            "content_html": "<p>In our <a class=\"\" href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released\">v5 release post</a>, we announced the upcoming launch of Centrifugo PRO. We are happy to say that it was released soon after that, and at this point, we already have several customers of the PRO version.</p>\n<p>I think it's time to look at the current state of the PRO version and finally start talking more about its benefits. In this post, we will talk more about one of the coolest PRO features we have at this point: the push notifications API.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"centrifugo-pro-goals\">Centrifugo PRO goals<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#centrifugo-pro-goals\" class=\"hash-link\" aria-label=\"Direct link to Centrifugo PRO goals\" title=\"Direct link to Centrifugo PRO goals\" translate=\"no\">​</a></h2>\n<p>When Centrifugo was originally created, its main goal was to help introduce real-time messaging features to existing systems, written in traditional frameworks which work on top of the worker/thread model. Serving many concurrent connections is a non-trivial task in general, and without native efficient concurrency support, it becomes mostly impossible without a shift in the technology stack. Integrating with Centrifugo makes it simple to introduce an efficient real-time layer, while keeping the existing application architecture.</p>\n<p>As time went on, Centrifugo got some unique features which now justify its usage even in conjunction with languages/frameworks with good concurrency support. Simply using Centrifugo for at-most-once PUB/SUB may already save a lot of development time. The task, which seems trivial at first glance, has a lot of challenges in practice: client SDKs with reconnect and channel multiplexing, scalability to many nodes, WebSocket fallbacks, etc.</p>\n<p>The combination of useful possibilities has made Centrifugo an attractive component for building enterprise-level applications. Let's be honest here - for pet projects, developers often prefer writing WebSocket communications themselves, and Centrifugo may be too heavy and an extra dependency. But in a corporate environment, the decision on which technology to use should take into account a lot of factors, like those we just mentioned above. Using a mature technology is often preferred to building from scratch and making all the mistakes along the way.</p>\n<p>With the PRO version, our goal is to provide even more value for established businesses when switching to Centrifugo. We want to solve tricky cases and simplify them for our customers; we want to step into related areas where we see we can provide sufficient value.</p>\n<p>One rule we try to follow for PRO features that extend Centrifugo’s scope is this: we are not trying to replicate something that already exists in other systems, but rather, we strive to improve upon it. We focus on solving practical issues that we observe, providing a unique value proposition for our customers. This post describes one such example — we will demonstrate our approach to push notifications, which is <a class=\"\" href=\"https://centrifugal.dev/docs/pro/overview#features\">one the features</a> of Centrifugo PRO.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"why-providing-push-notifications-api\">Why providing push notifications API<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#why-providing-push-notifications-api\" class=\"hash-link\" aria-label=\"Direct link to Why providing push notifications API\" title=\"Direct link to Why providing push notifications API\" translate=\"no\">​</a></h2>\n<img src=\"https://centrifugal.dev/img/push_characters.jpg\">\n<p>Why provide a push notifications API at all? Well, actually, real-time messages and push notifications are so close that many developers hardly see the difference before starting to work with both more closely.</p>\n<p>I’ve heard several stories where chat functionality on mobile devices was implemented using only native push notifications — without using a separate real-time transport like WebSocket while the app is in the foreground. While this is not a recommended approach due to the delivery properties of push notifications, it proves that real-time messages and push notifications are closely related concepts and sometimes may interchange with each other.</p>\n<p>When developers introduce WebSocket communication in an application, they often ask the question—what should I do next to deliver some important messages to a user who is currently not actively using the application? WebSockets are great when the app is in the foreground, but when the app goes to the background, the recommended approach is to close the WebSocket connection. This is important to save battery, and operating systems force the closing of connections after some time anyway.</p>\n<p>The delivery of important app data is then possible over push notifications. See a <a href=\"https://web.dev/articles/push-notifications-overview\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">good overview of them on web.dev</a>.</p>\n<p>Previously, Centrifugo positioned itself solely as a transport layer for real-time messages. In our FAQ, we emphasized this fact and suggested using separate software products to send push notifications.</p>\n<p>Now, with Centrifugo PRO, we provide this functionality to our customers. We have extended our server API with methods to manage and send push notifications. I promised to tell you why we believe our implementation is super cool. Let’s dive into the details.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"push-notifications-api-like-no-one-provides\">Push notifications API like no one provides<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#push-notifications-api-like-no-one-provides\" class=\"hash-link\" aria-label=\"Direct link to Push notifications API like no one provides\" title=\"Direct link to Push notifications API like no one provides\" translate=\"no\">​</a></h2>\n<p>Push notifications are super handy, but there’s a bit to do to get them working right. Let's break it down!</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"on-the-users-side-frontend\">On the user's side (frontend)<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#on-the-users-side-frontend\" class=\"hash-link\" aria-label=\"Direct link to On the user's side (frontend)\" title=\"Direct link to On the user's side (frontend)\" translate=\"no\">​</a></h4>\n<ul>\n<li class=\"\">Request permission from the user to receive push notifications.</li>\n<li class=\"\">Integrate with the platform-specific notification service (e.g., Apple Push Notification Service for iOS, Firebase Cloud Messaging for Android) to obtain the device token.</li>\n<li class=\"\">Send the device token to the server for storage and future use.</li>\n<li class=\"\">Integrate with the platform-specific notification handler to listen for incoming push notifications</li>\n<li class=\"\">Handle incoming push notifications: display the notification content to the user, either as a banner, alert, or in-app message, depending on the user's preferences and the type of notification. Handle user actions on the notification, such as opening the app, dismissing the notification, or taking a specific action related to the notification content.</li>\n</ul>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"on-the-server-backend\">On the server (backend)<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#on-the-server-backend\" class=\"hash-link\" aria-label=\"Direct link to On the server (backend)\" title=\"Direct link to On the server (backend)\" translate=\"no\">​</a></h4>\n<ul>\n<li class=\"\">Store device tokens in a database when received from the client side</li>\n<li class=\"\">Regularly clean up the database to remove stale or invalid device tokens. and handle scenarios where a device token becomes invalid or is revoked by the user, ensuring that no further notifications are sent to that device.</li>\n<li class=\"\">Integrate with platform-specific notification services (e.g., APNS, FCM) to send notifications to devices. Handle errors or failures in sending notifications and implement retry mechanisms if necessary.</li>\n<li class=\"\">Track the delivery status of each push notification sent out. Monitor the open rates, click-through rates, and other relevant metrics for the notifications.</li>\n<li class=\"\">Use analytics to understand user behavior in response to notifications and refine the notification strategy based on insights gained.</li>\n</ul>\n<p>We believe that we were able to achieve a unique combination of design decisions which allows us to provide push notification support like no one else provides. Let’s dive into what makes our approach special!</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"frontend-decisions\">Frontend decisions<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#frontend-decisions\" class=\"hash-link\" aria-label=\"Direct link to Frontend decisions\" title=\"Direct link to Frontend decisions\" translate=\"no\">​</a></h2>\n<p>When providing the push notification feature, other solutions like Pusher or Ably also offer their own SDKs for managing notifications on the client side.</p>\n<p>What we've learned, though, during the Centrifugo life cycle, is that creating and maintaining client SDKs for various environments (iOS, Android, Web, Flutter) is one of the hardest parts of the Centrifugo project.</p>\n<p>So the decision here was simple and natural: Centrifugo PRO does not introduce any client SDKs for push notifications on the client side.</p>\n<p>When integrating with Centrifugo, you can simply use the native SDKs provided by each platform. We bypass the complexities of SDK development and concentrate on server-side improvements. With this decision, we are not introducing any limitations to the client side.</p>\n<p>You get:</p>\n<ul>\n<li class=\"\">Wealthy documentation and community support. Platforms like APNs provide comprehensive documentation, tutorials, and best practices, making the integration process smoother.</li>\n<li class=\"\">Stability and reliability: native SDKs are rigorously tested and frequently updated by the platform providers. This ensures that they are stable, reliable, and free from critical bugs.</li>\n<li class=\"\">Access to the latest features. As platform providers roll out new features or enhancements, native SDKs are usually the first to get updated. This ensures that your application can leverage the latest functionalities without waiting for SDKs to catch up.</li>\n</ul>\n<p>This approach was not possible with our real-time SDKs, as WebSocket communication is very low-level, and Centrifugo’s main goal was to provide some high-level features on top of it. However, with push notifications, proceeding without a custom SDK seems like a choice beneficial for everyone.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"server-implementation\">Server implementation<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#server-implementation\" class=\"hash-link\" aria-label=\"Direct link to Server implementation\" title=\"Direct link to Server implementation\" translate=\"no\">​</a></h2>\n<p>The main work we did was on the server side. Let's go through the entire workflow of push notification delivery and describe what Centrifugo PRO provides for each step.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"how-we-keep-tokens\">How we keep tokens<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#how-we-keep-tokens\" class=\"hash-link\" aria-label=\"Direct link to How we keep tokens\" title=\"Direct link to How we keep tokens\" translate=\"no\">​</a></h3>\n<p>Let's suppose you got the permission from the user and received the device push token. At this point you must save it to database for sending notifications later using this token. Centrifugo PRO provides API called <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications#device_register\">device_register</a> to do exactly this.</p>\n<p>At this point, we use PostgreSQL for storing tokens – which is a very popular SQL database. Probably we will add more storage backend options in the future.</p>\n<p>When calling Centrifugo <code>device_register</code> API you can provide user ID, list of topics to subscribe, platform from which the user came from (ios, android, web), also push notifications provider. To deliver push notifications to devices Centrifugo PRO integrates with the following push notification providers:</p>\n<ul>\n<li class=\"\">fcm - <a href=\"https://firebase.google.com/docs/cloud-messaging\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Firebase Cloud Messaging (FCM)</a> <i class=\"bi bi-android2\" style=\"color:yellowgreen\"></i> <i class=\"bi bi-apple\" style=\"color:cornflowerblue\"></i> <i class=\"bi bi-globe\" style=\"color:orange\"></i></li>\n<li class=\"\">hms - <a href=\"https://developer.huawei.com/consumer/en/hms/huawei-pushkit/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Huawei Messaging Service (HMS) Push Kit</a> <i class=\"bi bi-android2\" style=\"color:yellowgreen\"></i> <i class=\"bi bi-apple\" style=\"color:cornflowerblue\"></i> <i class=\"bi bi-globe\" style=\"color:orange\"></i></li>\n<li class=\"\">apns - <a href=\"https://developer.apple.com/documentation/usernotifications\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Apple Push Notification service (APNs) </a> <i class=\"bi bi-apple\" style=\"color:cornflowerblue\"></i></li>\n</ul>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"Push\" src=\"https://centrifugal.dev/assets/images/push_notifications-97d3e940efd8a02ab6781f048031f100.png\" width=\"1799\" height=\"938\" class=\"img_ev3q\"></p>\n<p>So we basically cover all the most popular platforms out of the box.</p>\n<p>After registering the device token, Centrifugo PRO returns a <code>device_id</code> to you. This device ID must be stored on the client device. As long as the frontend has this <code>device_id</code>, it can update the device's push token information from time to time to keep it current (by just calling <code>device_register</code> again, but with <code>device_id</code> attached).</p>\n<p>After saving the token, your backend can start sending push notifications to devices.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"how-we-send-notifications\">How we send notifications<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#how-we-send-notifications\" class=\"hash-link\" aria-label=\"Direct link to How we send notifications\" title=\"Direct link to How we send notifications\" translate=\"no\">​</a></h3>\n<p>To send push notifications we provide another API called <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications#send_push_notification\">send_push_notification</a>. You need to provide some filter in the API request to tell Centrifugo who you want to send notification. You also need to provide push notification payload. For example, using Centrifugo HTTP API:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">curl -X POST http://localhost:8000/api/send_push_notification \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">-H \"Authorization: apikey &lt;KEY&gt;\" \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">-d @- &lt;&lt;'EOF'</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">{</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    \"recipient\": {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        \"filter\": {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            \"topics\": [\"test\"]</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    },</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    \"notification\": {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        \"fcm\": {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            \"message\": {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                \"notification\": {\"title\": \"Hello\", \"body\": \"How are you?\"}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">EOF</span><br></div></code></pre></div></div>\n<p>Here is another important decision we made: Centrifugo PRO allows you to specify raw JSON objects for each provider we support. In other words, we do not wrap the push notifications API for FCM, APNS, HMS - we give you a way to construct the entire push notification message.</p>\n<p>This means the Centrifugo push API supports all the fields of push notification payloads out-of-the-box, for all push providers. You can simply use the documentation of FCM, APNs, and send the constructed requests to Centrifugo. There is no need for us to update Centrifugo PRO in any way to support new fields added by providers to push APIs.</p>\n<p>When you send a push notification with a filter and push payload for each provider you want, it's queued by Centrifugo. We use Redis Streams for queuing and optionally a queue based on PostgreSQL (less efficient, but still robust enough).</p>\n<p>The fact that the notification is being queued means a very fast response time – so you can integrate with Centrifugo from within the hot paths of your application backend. You may additionally provide a push expiration time and a unique push identifier. If you have not provided a unique identifier, Centrifugo generates one for you and returns it in the response. The unique identifier may later be used to track push status in Centrifugo PRO's push notification analytics.</p>\n<p>We then have efficient workers which process the queue with minimal latency and send push notifications using batch requests for each provider - i.e., we do this in the most effective way possible. We conducted a benchmark of our worker system with FCM – and we can easily send <strong>several million pushes per minute</strong>.</p>\n<p>Another decision we made - Centrifugo PRO supports sending push notifications to a raw list of tokens. This makes it possible for our customers to use their own token storage. For example, such storage could already exist before you started using Centrifugo, or you might need a different storage/schema. In such cases, you can use Centrifugo just as an effective push sender server.</p>\n<p>Finally, Centrifugo PRO supports sending delayed push notification - to queue push for a later delivery, so for example you can send notification based on user time zone and let Centrifugo PRO send it when needed. Or you may send slightly delayed push notification together with real-time message and if client provided an ack to real-time message - <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications#cancel_push\">cancel push notification</a>.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"secure-unified-topics\">Secure unified topics<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#secure-unified-topics\" class=\"hash-link\" aria-label=\"Direct link to Secure unified topics\" title=\"Direct link to Secure unified topics\" translate=\"no\">​</a></h3>\n<p>FCM and HMS have a built-in way of sending notification to large groups of devices over topics mechanism (the same for HMS). One problem with native FCM or HMS topics though is that device can subscribe to any topic from the frontend side without any permission check. In today's world this is usually not desired. So Centrifugo PRO re-implements FCM and HMS topics by introducing an additional API to manage device subscriptions to topics.</p>\n<p>Centrifugo PRO device topic subscriptions also <strong>add a way to introduce the missing topic semantics for APNs</strong>.</p>\n<p>Centrifugo PRO additionally provides an API to create persistent bindings of user to notification topics. See <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications#user_topic_list\">user_topic_list</a> and <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications#user_topic_update\">user_topic_update</a>. As soon as user registers a device – it will be automatically subscribed to its own topics pre-created over the API. As soon as user logs out from the app and you update user ID of the device - user topics binded to the device automatically removed/switched.</p>\n<p>This design solves one of the issues with push notifications (with FCM in particular) – if two different users use the same device it's becoming problematic to unsubscribe the device from large number of topics upon logout. Also, as soon as user to topic binding added (using <code>user_topic_update</code> API) – it will be synchronized across all user active devices. You can still manage such persistent subscriptions on the application backend side if you prefer and provide the full list inside <code>device_register</code> call - Centrifugo PRO API gives you freedom here.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"push-analytics\">Push analytics<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#push-analytics\" class=\"hash-link\" aria-label=\"Direct link to Push analytics\" title=\"Direct link to Push analytics\" translate=\"no\">​</a></h3>\n<p>Centrifugo PRO offers the ability to inspect sent push notifications using <a class=\"\" href=\"https://centrifugal.dev/docs/pro/analytics\">ClickHouse analytics</a>. Push providers may also offer their own analytics, such as FCM, which provides insight into push notification delivery. Centrifugo PRO also offers a way to analyze push notification delivery and interaction using the <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications#update_push_status\">update_push_status</a> API. This API allows updating ClickHouse table and add status for each push sent:</p>\n<ul>\n<li class=\"\"><code>delivered</code></li>\n<li class=\"\">or <code>interacted</code></li>\n</ul>\n<p>It's then possible to make queries to ClickHouse and build various analytical reports. Or use ClickHouse for real-time graphs - for example, from Grafana.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"push-notifications-ui\">Push notifications UI<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#push-notifications-ui\" class=\"hash-link\" aria-label=\"Direct link to Push notifications UI\" title=\"Direct link to Push notifications UI\" translate=\"no\">​</a></h3>\n<p>Finally, Centrifugo PRO provides a simple web UI for inspecting registered devices. It can simplify development, provide a way to look at live data, and send simple push notification alerts to users or topics.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/push_ui-01989161306e71d882a064b605395dcb.png\" width=\"2728\" height=\"1094\" class=\"img_ev3q\"></p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>We really believe in our push notifications and will be working hard to make them even better. The API we already have serves well to cover common push notification delivery use cases, but we won't stop here. Some areas for improvements are: functionality of built-in push notifications web UI, extending push analytics by providing user friendly UI for the insights about push delivery and engagement. The good thing is that we already have a ground for making this.</p>\n<p>Take a look at the documentation of <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications\">Centrifugo PRO push notification API</a> for more formal details and some things not mentioned here. Probably at the time you are reading this we already added something great to the API.</p>\n<p>Even though Centrifugo PRO is pretty new, it already has a lot of helpful features, and we have plans to add even more. You can see what’s coming up next on our <a href=\"https://github.com/orgs/centrifugal/projects/3/views/1\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo PRO planned features board</a>. We're excited to share more blog posts like this one in the future.</p>",
            "url": "https://centrifugal.dev/blog/2023/10/29/discovering-centrifugo-pro-push-notifications",
            "title": "Discovering Centrifugo PRO: push notifications API",
            "summary": "We start talking more about recently launched Centrifugo PRO. In this post, we share details about Centrifugo PRO push notification API implementation - how it works and what makes it special and practical.",
            "date_modified": "2023-10-29T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "pro",
                "push notifications"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2023/08/29/using-centrifugo-in-rabbitx",
            "content_html": "<p>This post introduces a new format in Centrifugal blog – interview with a Centrifugo user! Let's dive into an exciting chat with the engineering team of <a href=\"https://landing.rabbitx.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RabbitX platform</a>, a global permissionless perpetuals exchange powered on Starknet. We will discover how Centrifugo helped RabbitX to build a broker platform with current trading volume of 25 million USD daily! 🚀🎉</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"q-hey-team---thanks-for-your-desire-to-share-your-centrifugo-use-case-first-of-all-could-you-provide-some-information-about-rabbitx---what-is-it\">[Q] Hey team - thanks for your desire to share your Centrifugo use case. First of all, could you provide some information about RabbitX - what is it?<a href=\"https://centrifugal.dev/blog/2023/08/29/using-centrifugo-in-rabbitx#q-hey-team---thanks-for-your-desire-to-share-your-centrifugo-use-case-first-of-all-could-you-provide-some-information-about-rabbitx---what-is-it\" class=\"hash-link\" aria-label=\"Direct link to [Q] Hey team - thanks for your desire to share your Centrifugo use case. First of all, could you provide some information about RabbitX - what is it?\" title=\"Direct link to [Q] Hey team - thanks for your desire to share your Centrifugo use case. First of all, could you provide some information about RabbitX - what is it?\" translate=\"no\">​</a></h4>\n<p>RabbitX is a global permissionless perpetuals exchange built on Starknet. RabbitX is building the most secure and liquid global derivatives network, giving you 24/7 access to global markets anywhere in the world, with 20x leverage. In its core there is an orderbook - where traders match against market makers, which require to support high throughput and low latency tech stack.</p>\n<p>The technologies that we are using:</p>\n<ul>\n<li class=\"\">Tarantool as in-memory database and business logic server</li>\n<li class=\"\">Centrifugo as our major websocket server</li>\n<li class=\"\">Different stark tech to support decentralized settlement</li>\n</ul>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"q-great-what-is-the-goal-of-centrifugo-in-your-project-which-real-time-features-you-have\">[Q] Great! What is the goal of Centrifugo in your project? Which real-time features you have?<a href=\"https://centrifugal.dev/blog/2023/08/29/using-centrifugo-in-rabbitx#q-great-what-is-the-goal-of-centrifugo-in-your-project-which-real-time-features-you-have\" class=\"hash-link\" aria-label=\"Direct link to [Q] Great! What is the goal of Centrifugo in your project? Which real-time features you have?\" title=\"Direct link to [Q] Great! What is the goal of Centrifugo in your project? Which real-time features you have?\" translate=\"no\">​</a></h4>\n<p>Almost all the information users see in our terminal is streamed over Centrifugo. We use it for financial order books, candlestick chart updates, and stat number updates. We can also send real-time personal user notifications via Centrifugo. Instead of all the words, here is a short recording of our terminal trading BTC:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/rabbitx.mp4?v=1\"></video>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"q-we-know-that-you-are-using-centrifugo-tarantool-engine---could-you-explain-why-and-how-it-works-in-your-case\">[Q] We know that you are using Centrifugo Tarantool engine - could you explain why and how it works in your case?<a href=\"https://centrifugal.dev/blog/2023/08/29/using-centrifugo-in-rabbitx#q-we-know-that-you-are-using-centrifugo-tarantool-engine---could-you-explain-why-and-how-it-works-in-your-case\" class=\"hash-link\" aria-label=\"Direct link to [Q] We know that you are using Centrifugo Tarantool engine - could you explain why and how it works in your case?\" title=\"Direct link to [Q] We know that you are using Centrifugo Tarantool engine - could you explain why and how it works in your case?\" translate=\"no\">​</a></h4>\n<p>Well, that's an interesting thing. We heavily use Tarantool in our system. It grants us immense flexibility, performance, and the power to craft whatever we envision. It ensures the atomicity essential for trading match-making.</p>\n<p>When we were in search of a WebSocket real-time bus for messages, we were pleasantly surprised to discover that Centrifugo integrates with Tarantool. In our scenario, this allowed us to bypass additional network round-trips, as we can stream data directly from Tarantool to Centrifugo channels. Reducing latency is paramount for financial instruments.</p>\n<p>Furthermore, I can mention that over our nine months in production, we didn't encounter any issues with Centrifugo – it performed flawlessly!</p>\n<p>Regarding authentication, we employ Centrifugo's JWT authentication and subscribe proxy. Thus, subscriptions are authorized on our specialized service written in Go. We're also actively using Centrifugo possibility to send initial channel data in the subscribe proxy response.</p>\n<p>One challenge we overcame was bridging the gap between the subscription's initial request and the continuous message stream in the order book component. To address this, we employed our own sequence numbers in events, coupled with Centrifugo's channel history – this allowed us to deal with missed events when needed. Actually the gaps in event stream are rare in practice and our workaround not needed most of the time, but now we're confident our users never experience this issue.</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"q-looking-at-rabbitx-terminal-app-we-see-quite-modern-ui---could-you-share-more-details-about-it-too\">[Q] Looking at RabbitX terminal app we see quite modern UI - could you share more details about it too?<a href=\"https://centrifugal.dev/blog/2023/08/29/using-centrifugo-in-rabbitx#q-looking-at-rabbitx-terminal-app-we-see-quite-modern-ui---could-you-share-more-details-about-it-too\" class=\"hash-link\" aria-label=\"Direct link to [Q] Looking at RabbitX terminal app we see quite modern UI - could you share more details about it too?\" title=\"Direct link to [Q] Looking at RabbitX terminal app we see quite modern UI - could you share more details about it too?\" translate=\"no\">​</a></h4>\n<p>Our frontend is built on top of React in combination with <a href=\"https://www.tradingview.com/chart/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">TradingView Supercharts</a>. And of course we are using <code>centrifuge-js</code> SDK for establishing connections with Centrifugo.</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"q-so-you-are-nine-months-in-production-at-this-point-can-you-share-some-real-world-numbers-related-to-your-centrifugo-setup\">[Q] So you are nine months in production at this point. Can you share some real world numbers related to your Centrifugo setup?<a href=\"https://centrifugal.dev/blog/2023/08/29/using-centrifugo-in-rabbitx#q-so-you-are-nine-months-in-production-at-this-point-can-you-share-some-real-world-numbers-related-to-your-centrifugo-setup\" class=\"hash-link\" aria-label=\"Direct link to [Q] So you are nine months in production at this point. Can you share some real world numbers related to your Centrifugo setup?\" title=\"Direct link to [Q] So you are nine months in production at this point. Can you share some real world numbers related to your Centrifugo setup?\" translate=\"no\">​</a></h4>\n<p>At this point we can have up to a thousand active concurrent traders and send more than 60 messages per second towards one client in peak hours. All the load is served with a single Centrifugo instance (and we have one standby instance).</p>\n<h4 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"q-anything-else-you-want-to-share-with-readers-of-centrifugal-blog\">[Q] Anything else you want to share with readers of Centrifugal blog?<a href=\"https://centrifugal.dev/blog/2023/08/29/using-centrifugo-in-rabbitx#q-anything-else-you-want-to-share-with-readers-of-centrifugal-blog\" class=\"hash-link\" aria-label=\"Direct link to [Q] Anything else you want to share with readers of Centrifugal blog?\" title=\"Direct link to [Q] Anything else you want to share with readers of Centrifugal blog?\" translate=\"no\">​</a></h4>\n<p>When we designed the system the main goal was to have a homogeneous tech zoo, with a small amount of different technologies, to keep the number of failure points as small as possible. Tarantool is a sort of technology that really allows us to achieve this, we were able to add different decentralized mechanics to our system because of that. It’s not only an in-memory database, but in reality the app server as well.</p>\n<p>In our case, the fact Centrifugo supports Tarantool broker was a big discovery – the integration went smoothly, and everything has been working great since then.</p>",
            "url": "https://centrifugal.dev/blog/2023/08/29/using-centrifugo-in-rabbitx",
            "title": "Using Centrifugo in RabbitX",
            "summary": "In this post, the engineering team of RabbitX platform shares details about the usage of Centrifugo in their product.",
            "date_modified": "2023-08-29T00:00:00.000Z",
            "author": {
                "name": "Centrifugal Labs + RabbitX"
            },
            "tags": [
                "centrifugo",
                "interview",
                "usecase"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos",
            "content_html": "<p>Centrifugo provides HTTP and GRPC APIs for publishing messages into channels. Publish server API is very straighforward to use - it's a simple request with a channel and data to be delivered to active WebSocket connections subscribed to a channel.</p>\n<p>Sometimes though Centrifugo users want to avoid synchronous calls to Centrifugo API delegating this work to asynchronous tasks. Many companies have convenient infrastructure for messaging processing tasks - like Kafka, Nats Jetstream, Redis, RabbitMQ, etc. Some using transactional outbox pattern to reliably deliver events upon database changes and have a ready infrastructure to push such events to some queue. From which want to re-publish events to Centrifugo.</p>\n<p>In this post we get familiar with a tool called <a href=\"https://www.benthos.dev/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Benthos</a> and show how it may simplify integrating your asynchronous message flow with Centrifugo. And we discuss some non-obvious pitfalls of asynchronous publishing approach in regards to real-time applications.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"start-centrifugo\">Start Centrifugo<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#start-centrifugo\" class=\"hash-link\" aria-label=\"Direct link to Start Centrifugo\" title=\"Direct link to Start Centrifugo\" translate=\"no\">​</a></h2>\n<p>First start Centrifugo (with debug logging to see incoming API requests in logs):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo genconfig</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo -c config.json --log_level debug</span><br></div></code></pre></div></div>\n<p>Hope this step is already simple for you, if not - check out <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/quickstart\">Quickstart tutorial</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"install-and-run-benthos\">Install and run Benthos<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#install-and-run-benthos\" class=\"hash-link\" aria-label=\"Direct link to Install and run Benthos\" title=\"Direct link to Install and run Benthos\" translate=\"no\">​</a></h2>\n<p>Benthos is an awesome tool which allows consuming data from various inputs, process data, then output it into configured outputs. See more detailed description <a href=\"https://www.benthos.dev/docs/about\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">on Benthos' website</a>.</p>\n<p>The number of inputs supported by Benthos is huge: <a href=\"https://www.benthos.dev/docs/components/inputs/about#categories\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">check it out here</a>. Most of the major systems widely used these days are supported. Benthos also supports <a href=\"https://www.benthos.dev/docs/components/outputs/about#categories\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">many outputs</a> – but here we only interested in message transfer to Centrifugo. There is no built-in Centrifugo output in Benthos but it provides a generic <code>http_client</code> output which may be used to send requests to any HTTP server. Benthos may also help with retries, provides tools for additional data processing and transformations.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/benthos-1eb5dd899b464895e7b27fa75e0e6857.svg\" width=\"1920\" height=\"900\" class=\"img_ev3q\"></p>\n<p>Just like Centrifugo Benthos written in Go language – so its installation is very straighforward and similar to Centrifugo. See <a href=\"https://www.benthos.dev/docs/guides/getting_started\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">official instructions</a>.</p>\n<p>Let's assume you've installed Benthos and have <code>benthos</code> executable available in your system. Let's create Benthos configuration file:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">benthos create &gt; config.yaml</span><br></div></code></pre></div></div>\n<p>Take a look at generated <code>config.yaml</code> - it contains various options for Benthos instance, the most important (for the context of this post) are <code>input</code> and <code>output</code> sections.</p>\n<p>And after that you can start Benthos instance with:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">benthos -c config.yaml</span><br></div></code></pre></div></div>\n<p>Now we need to tell Benthos from where to get data and how to send it to Centrifugo.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"configure-benthos-input-and-output\">Configure Benthos input and output<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#configure-benthos-input-and-output\" class=\"hash-link\" aria-label=\"Direct link to Configure Benthos input and output\" title=\"Direct link to Configure Benthos input and output\" translate=\"no\">​</a></h2>\n<p>For our example here we will user Redis List as an input, won't add any additional data processing and will output messages consumed from Redis List into Centrifugo publish server HTTP API.</p>\n<p>To achieve this add the following as input in Benthos <code>config.yaml</code>:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token key atrule\">input</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">label</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"centrifugo_redis_consumer\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">redis_list</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">url</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redis://127.0.0.1:6379\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"centrifugo.publish\"</span><br></div></code></pre></div></div>\n<p>And configure the output like this:</p>\n<div class=\"language-yaml codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-yaml codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token key atrule\">output</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">label</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"centrifugo_http_publisher\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token key atrule\">http_client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">url</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:8000/api/publish\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">verb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> POST</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">headers</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token key atrule\">X-API-Key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"&lt;CENTRIFUGO_API_KEY&gt;\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">timeout</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> 5s</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token key atrule\">max_in_flight</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">20</span><br></div></code></pre></div></div>\n<p>The output points to Centrifugo <a class=\"\" href=\"https://centrifugal.dev/docs/server/server_api#publish\">publish HTTP API</a>. Replace <code>&lt;CENTRIFUGO_API_KEY&gt;</code> with your Centrifugo <code>api_key</code> (found in Centrifugo configuration file).</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"push-messages-to-redis-queue\">Push messages to Redis queue<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#push-messages-to-redis-queue\" class=\"hash-link\" aria-label=\"Direct link to Push messages to Redis queue\" title=\"Direct link to Push messages to Redis queue\" translate=\"no\">​</a></h2>\n<p>Start Benthos instance:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">benthos -c config.yaml</span><br></div></code></pre></div></div>\n<p>You will see errors while Benthos tries to connect to input Redis source. So start Redis server:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker run --rm -it --name redis redis:7</span><br></div></code></pre></div></div>\n<p>Now connect to Redis (using <code>redis-cli</code>):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker exec -it redis redis-cli</span><br></div></code></pre></div></div>\n<p>And push command to Redis list:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">127.0.0.1:6379&gt; rpush centrifugo.publish '{\"channel\": \"chat\", \"data\": {\"input\": \"test\"}}'</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">(integer) 1</span><br></div></code></pre></div></div>\n<p>This message will be consumed from Redis list by Benthos and published to Centrifugo HTTP API. If you have active subscribers to channel <code>chat</code> – you will see messages delivered to them. That's it!</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>When using Redis List input you can scale out Benthos instances to run several of them if needed.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"demo\">Demo<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#demo\" class=\"hash-link\" aria-label=\"Direct link to Demo\" title=\"Direct link to Demo\" translate=\"no\">​</a></h2>\n<p>Here is a quick demonstration of the described integration. See how we push messages into Redis List and those are delivered to WebSocket clients:</p>\n<video width=\"100%\" controls=\"\"><source src=\"/img/benthos.mp4\" type=\"video/mp4\"><p>Sorry, your browser doesn't support embedded video.</p></video>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"pitfalls-of-async-publishing\">Pitfalls of async publishing<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#pitfalls-of-async-publishing\" class=\"hash-link\" aria-label=\"Direct link to Pitfalls of async publishing\" title=\"Direct link to Pitfalls of async publishing\" translate=\"no\">​</a></h2>\n<p>This all seems simple. But publishing messages asynchronously may highlight some pitfalls not visible or not applicable for synchronous publishing to Centrifugo API.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"late-delivery\">Late delivery<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#late-delivery\" class=\"hash-link\" aria-label=\"Direct link to Late delivery\" title=\"Direct link to Late delivery\" translate=\"no\">​</a></h3>\n<p>Most of the time it will work just fine. But one day you can come across intermediate queue growth and increased delivery lag. This may happen due to temporary Centrifugo or worker process unavailability. As soon as system comes back to normal queued messages will be delivered.</p>\n<p>Depending on the real-time feature implemented this may be a concern to think about and decide whether this is desired or not. Your application should be designed accordingly to deal with late delivery.</p>\n<p>BTW late delivery may be a case even with synchronous publishing – it just almost never strikes. But theoretically client can reload browser page and load initial app state while message flying from the backend to client over Centrifugo. It's not Centrifugo specific actually - it's just a nature of networks and involved latencies.</p>\n<p>In general solution to prevent late delivery UX issues completely is using object versioning. Object version should be updated in the database on every change from which the real-time event is generated. Attach object version information to every real-time message. Also get object version on initial state load. This way you can compare versions and drop non-actual real-time messages on client side.</p>\n<p>Possible strategy may be using synchronous API for real-time features where at most once delivery is acceptable and use asynchronous delivery where you need to deliver messages with at least once guarantees. In a latter case you most probably designed proper idempotency behaviour on client side anyway.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"ordering-concerns\">Ordering concerns<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#ordering-concerns\" class=\"hash-link\" aria-label=\"Direct link to Ordering concerns\" title=\"Direct link to Ordering concerns\" translate=\"no\">​</a></h3>\n<p>Another thing to consider is message ordering. Centrifugo itself <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/design#message-order-guarantees\">may provide message ordering in channels</a>. If you published one message to Centrifugo API, then another one – you can expect that messages will be delivered to a client in the same order. But as soon as you have an intermediary layer like Benthos or any other asynchronous worker process – then you must be careful about ordering. In case of Benthos and example here you can set <code>max_in_flight</code> parameter to <code>1</code> instead of <code>20</code> and keep only one instance of Benthos running to preserve ordering.</p>\n<p>In case of streaming from Kafka you can rely on Kafka message partitioning feature to preserve message ordering.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"throughput-when-ordering-preserved\">Throughput when ordering preserved<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#throughput-when-ordering-preserved\" class=\"hash-link\" aria-label=\"Direct link to Throughput when ordering preserved\" title=\"Direct link to Throughput when ordering preserved\" translate=\"no\">​</a></h3>\n<p>If you preserved ordering in your asynchronous workers then the next thing to consider is throughput limitations.</p>\n<p>You have a limited number of workers, these workers send requests to Centrifugo one by one. In this case throughput is limited by the number of workers and RTT (round-trip time) between worker process and Centrifugo.</p>\n<p>If we talk about using Redis List structure as a queue - you can possibly shard data amongst different Redis List queues by some key to improve throughput. In this case you need to push messages where order should be preserved into a specific queue. In this case your get a setup similar to Kafka partitioning.</p>\n<p>In case of using manually partitioned queues or using Kafka you can have parallelism equal to the number of partitions.</p>\n<p>Let's say you have 20 workers which can publish messages in parallel and 5ms RTT time between worker and Centrifugo. In this case you can publish 20*(1000/5) = 4000 messages per second max.</p>\n<p>To improve throughput futher consider increasing worker number or batching publish requests to Centrifugo (using Centrifugo's batch API).</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"error-handling\">Error handling<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#error-handling\" class=\"hash-link\" aria-label=\"Direct link to Error handling\" title=\"Direct link to Error handling\" translate=\"no\">​</a></h3>\n<p>When publishing asynchronously you should also don't forget about error handling. Benthos will handle network errors automatically for you. But there could be internal errors from Centrifugo returned as part of response. It's not very convenient to handle with Benthos out of the box – so we think about <a href=\"https://github.com/centrifugal/centrifugo/pull/690\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">adding transport-level error mode</a> to Centrifugo.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>Sometimes you want to publish to Centrifugo asynchronously using messaging systems convenient for your company. Usually you can write worker process to re-publish messages to Centrifugo. Sometimes it may be simplified using helpful tools like Benthos.</p>\n<p>Here we've shown how Benthos may be used to transfer messages from Redis List queue to Centrifugo API. With some modifications you can achieve the same for other input sources - such as Kafka, RabbitMQ, Nats Jetstream, etc.</p>\n<p>But publishing messages asynchronously highlights several pifalls - like late delivery, ordering issues,  throughput considerations and error handling – which should be carefully addressed. Different real-time features may require different strategies.</p>",
            "url": "https://centrifugal.dev/blog/2023/08/19/asynchronous-message-streaming-to-centrifugo-with-benthos",
            "title": "Asynchronous message streaming to Centrifugo with Benthos",
            "summary": "In this post, we'll demonstrate how to asynchronously stream messages into Centrifugo channels from external data providers using Benthos tool. We also highlight some pitfalls which become more important in asynchronous publishing scenario.",
            "date_modified": "2023-08-19T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "benthos",
                "tutorial"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released",
            "content_html": "<p>In Centrifugo v5 we're phasing out old client protocol support, introducing a more intuitive HTTP API, adjusting token management behaviour in SDKs, improving configuration process, and refactoring the history meta ttl option. As the result you get a cleaner, more user-friendly, and optimized Centrifugo experience. And we have important news about the project - check it out in the end of this post.</p>\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>What is Centrifugo?</div><div class=\"admonitionContent_BuS1\"><p>Centrifugo is an open-source scalable real-time messaging server. Centrifugo can instantly deliver messages to application online users connected over supported transports (WebSocket, HTTP-streaming, SSE/EventSource, GRPC, SockJS, WebTransport). Centrifugo has the concept of a channel – so it's a user-facing PUB/SUB server.</p><p>Centrifugo is language-agnostic and can be used to build chat apps, live comments, multiplayer games, real-time data visualizations, collaborative tools, etc. in combination with any backend. It is well suited for modern architectures and allows decoupling the business logic from the real-time transport layer.</p><p>Several official client SDKs for browser and mobile development wrap the bidirectional protocol. In addition, Centrifugo supports a unidirectional approach for simple use cases with no SDK dependency.</p></div></div>\n<p>Let's proceed and take a look at most notable changes of Centrifugo v5.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"dropping-old-client-protocol\">Dropping old client protocol<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#dropping-old-client-protocol\" class=\"hash-link\" aria-label=\"Direct link to Dropping old client protocol\" title=\"Direct link to Dropping old client protocol\" translate=\"no\">​</a></h2>\n<p>With the introduction of Centrifugo v4, our previous major release, <a class=\"\" href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#unified-client-sdk-api\">we rolled out</a> a new version of the client protocol along with a set of client SDKs designed to work in conjunction with it. Nevertheless, we maintained support for the old client protocol in Centrifugo v4 to facilitate a seamless migration of applications.</p>\n<p>In Centrifugo v5 we are discontinuing support for the old protocol. If you have been using Centrifugo v4 with the latest SDKs, this change should have no impact on you. From our perspective, removing support for the old protocol allows us to eliminate a considerable amount of non-obvious branching in the source code and cleanup Protobuf schema definitions.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"token-behaviour-adjustments-in-sdks\">Token behaviour adjustments in SDKs<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#token-behaviour-adjustments-in-sdks\" class=\"hash-link\" aria-label=\"Direct link to Token behaviour adjustments in SDKs\" title=\"Direct link to Token behaviour adjustments in SDKs\" translate=\"no\">​</a></h2>\n<p>In Centrifugo v5 we are adjusting <a class=\"\" href=\"https://centrifugal.dev/docs/transports/client_api\">client SDK specification</a> in the aspect of connection token management. Previously, returning an empty token string from <code>getToken</code> callback resulted in client disconnection with <code>unauthorized</code> reason.</p>\n<p>There was some problem with it though. We did not take into account the fact that empty token may be a valid scenario actually. Centrifugo supports options to avoid using token at all for anonymous access. So the lack of possibility to switch between <code>token</code>/<code>no token</code> scenarios did not allow users to easily implement login/logout workflow. The only way was re-initializing SDK.</p>\n<p>Now returning an empty string from <code>getToken</code> is a valid scenario which won't result into disconnect on the client side. It's still possible to disconnect client by returning a special error from <code>getToken</code> function.</p>\n<p>And we are putting back <code>setToken</code> method to our SDKs – so it's now possible to reset the token to be empty upon user logout.</p>\n<p>An abstract example in Javascript which demonstrates how login/logout flow may be now implemented with our SDK:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'ws://localhost:8000/connection/websocket'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Provide function which returns empty string for anonymous users,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// and proper JWT for authenticated users.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">getToken</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> getTokenImplementation</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">loginButton</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">addEventListener</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'click'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">disconnect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Do actual login here.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">logoutButton</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">addEventListener</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'click'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">disconnect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Reset token - so that getToken will be called on next connect attempt.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">setToken</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Do actual logout here.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>We updated all our SDKs to inherit described behaviour - check out v5 <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/migration_v5\">migration guide</a> for more details.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"history_meta_ttl-refactoring\">history_meta_ttl refactoring<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#history_meta_ttl-refactoring\" class=\"hash-link\" aria-label=\"Direct link to history_meta_ttl refactoring\" title=\"Direct link to history_meta_ttl refactoring\" translate=\"no\">​</a></h2>\n<p>One of Centrifugo's key advantages for real-time messaging tasks is its ephemeral channels and per-channel history. In version 5, we've improved one aspect of handling history by offering users the ability to tune the history meta TTL option at the channel namespace level.</p>\n<p>The history meta TTL is the duration Centrifugo retains meta information about each channel stream, such as the latest offset and current stream epoch. This data allows users to successfully restore connections upon reconnection, particularly useful when subscribed to mostly inactive channels where publications are infrequent. Although the history meta TTL can usually be reasonably large (significantly larger than history TTL), there are certain scenarios where it's beneficial to minimize it as much as possible.</p>\n<p>One such use case is illustrated in this <a href=\"https://github.com/centrifugal/examples/tree/master/v4/go_async_processing\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">example</a>. Using Centrifugo SDK and channels with history, it's possible to reliably stream results of asynchronous tasks to clients.</p>\n<p>As another example, consider a ChatGPT use case where clients ask questions, subscribe to a channel with the answer, and then the response is streamed towards the client token by token. This all may be done over a secure, separate channel protected with a token. With the ability to use a relatively small history meta TTL in the channel namespace, implementing such things is now simpler.</p>\n<p>Hence, <code>history_meta_ttl</code> is now an option at the channel namespace level (instead of per-engine). However, setting it is optional as we have a global default value for it - see <a class=\"\" href=\"https://centrifugal.dev/docs/server/channels#history_meta_ttl\">details in the doc</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"node-communication-protocol-update\">Node communication protocol update<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#node-communication-protocol-update\" class=\"hash-link\" aria-label=\"Direct link to Node communication protocol update\" title=\"Direct link to Node communication protocol update\" translate=\"no\">​</a></h2>\n<p>When running in cluster Centrifugo nodes can communicate between each other using broker's PUB/SUB. Nodes exchange some information - like statistics, emulation requests, etc.</p>\n<p>In Centrifugo v5 we are simplifying and making inter-node communication protocol slightly faster by removing extra encoding layers from it's format. Something similar to what we did for our client protocol in Centrifugo v4.</p>\n<p>This change, however, leads to an incompatibility between Centrifugo v4 and v5 nodes in terms of their communication protocols. Consequently, Centrifugo v5 cannot be part of a cluster with Centrifugo v4 nodes.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"new-http-api-format\">New HTTP API format<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#new-http-api-format\" class=\"hash-link\" aria-label=\"Direct link to New HTTP API format\" title=\"Direct link to New HTTP API format\" translate=\"no\">​</a></h2>\n<p>From the beginning Centrifugo HTTP API exposed one <code>/api</code> endpoint to make requests with all command types.</p>\n<p>To work properly HTTP API had to add one additional layer to request JSON payload to be able to distinguish between different API methods:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">curl --header \"Content-Type: application/json\" \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  --header \"Authorization: apikey API_KEY\" \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  --request POST \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  --data '{\"method\": \"publish\", \"params\": {\"channel\": \"test\", \"data\": {\"x\": 1}}}' \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  http://localhost:8000/api</span><br></div></code></pre></div></div>\n<p>And it worked fine. It additionally supported request batching where users could send many commands to Centrifugo in one request using line-delimited JSON.</p>\n<p>However, the fact that we were accommodating various commands via a single API endpoint resulted in nested serialized payloads for each command. The top-level method would determine the structure of the params. We addressed this issue in the client protocol in Centrifugo v4, and now we're addressing a similar issue in the inter-node communication protocol in Centrifugo v5.</p>\n<p>At some point we introduced GRPC API in Centrifugo. In GRPC case we don't have a way to send batches of commands without defining a separate method to do so.</p>\n<p>These developments highlighted the need for us to align the HTTP API format more closely with the GRPC API. Specifically, we need to separate the command method from the actual method payload, moving towards a structure like this:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">curl --header \"Content-Type: application/json\" \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  --header \"X-API-Key: API_KEY\" \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  --request POST \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  --data '{\"channel\": \"test\", \"data\": {\"x\": 1}}' \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  http://localhost:8000/api/publish</span><br></div></code></pre></div></div>\n<p>Note:</p>\n<ul>\n<li class=\"\"><code>/api/publish</code> instead of <code>/api</code> in path</li>\n<li class=\"\">payload does not include <code>method</code> and <code>params</code> keys anymore</li>\n<li class=\"\">we also support <code>X-API-Key</code> header for setting API key to be closer to OpenAPI specification (see more about OpenAPI below)</li>\n</ul>\n<p>In v5 we implemented the approach above. Many thanks to <a href=\"https://github.com/StringNick\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">@StringNick</a> for the help with the implementation and discussions.</p>\n<p>Our HTTP and GRPC API are very similar now. We've also introduced a new batch method to send multiple commands in both HTTP and GRPC APIs, a feature that was previously unavailable in GRPC.</p>\n<p>Documentation for v5 was updated to reflect this change. But it worth noting - old API format id still supported. It will be supported for some time while we are migrating our HTTP API libraries to use modern API version. Hopefully users won't be affected by this migration a lot, just switching to a new version of library at some point.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"openapi-spec-and-swagger-ui\">OpenAPI spec and Swagger UI<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#openapi-spec-and-swagger-ui\" class=\"hash-link\" aria-label=\"Direct link to OpenAPI spec and Swagger UI\" title=\"Direct link to OpenAPI spec and Swagger UI\" translate=\"no\">​</a></h2>\n<p>One additional benefit of moving to the new HTTP format is the possibility to define a clear OpenAPI schema for each API method Centrifugo has. It was previously quite tricky due to the fact we had one endpoint capable to work with all kinds of commands.</p>\n<p>This change paves the way for generating HTTP clients based on our OpenAPI specification.</p>\n<p>We now have Swagger UI built-in. To access it, launch Centrifugo with the <code>\"swagger\": true</code> option and navigate to <code>http://localhost:8000/swagger</code>.</p>\n<p>The Swagger UI operates on the internal port, so if you're running Centrifugo using our Kubernetes Helm chart, it won't be exposed to the same ingress as client connection endpoints. This is similar to how our Prometheus, admin, API, and debug endpoints currently work.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"opentelemetry-for-server-api\">OpenTelemetry for server API<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#opentelemetry-for-server-api\" class=\"hash-link\" aria-label=\"Direct link to OpenTelemetry for server API\" title=\"Direct link to OpenTelemetry for server API\" translate=\"no\">​</a></h2>\n<p>Another good addition is an OpenTelemetry tracing support for HTTP and GRPC server API requests. If you are using OpenTelemetry in your services you can now now enable tracing export in Centrifugo and find Centrifugo API request exported traces in your tracing collector UI.</p>\n<p>Description and simple example with Jaeger may be found <a class=\"\" href=\"https://centrifugal.dev/docs/server/observability#opentelemetry\">in observability chapter</a>. We only support OTLP HTTP export format and trace format defined in W3C spec: <a href=\"https://www.w3.org/TR/trace-context/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://www.w3.org/TR/trace-context/</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"separate-config-for-subscription-token\">Separate config for subscription token<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#separate-config-for-subscription-token\" class=\"hash-link\" aria-label=\"Direct link to Separate config for subscription token\" title=\"Direct link to Separate config for subscription token\" translate=\"no\">​</a></h2>\n<p>With the introduction of JWKS support in Centrifugo v4 (a way to validate JWT tokens using a remote endpoint which manages keys and key rotation - see <a href=\"https://datatracker.ietf.org/doc/html/rfc7517\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">JWK spec</a>) Centrifugo users can rely on JWKS provider (like Keycloak, AWS Cognito) for making authentication.</p>\n<p>But at the same time developers may want to work with channels using subscription tokens managed in a custom way – without using the same JWKS configuration used for connection tokens.</p>\n<p>Centrifugo v5 allows doing by introducing the <code>separate_subscription_token_config</code> option.</p>\n<p>When <code>separate_subscription_token_config</code> is <code>true</code> Centrifugo does not look at general token options at all when verifying subscription tokens and uses config options starting from <code>subscription_token_</code> prefix instead.</p>\n<p>Here is an example how to use JWKS for connection tokens, but have HMAC-based verification for subscription tokens:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"token_jwks_public_endpoint\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"https://example.com/openid-connect/certs\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"separate_subscription_token_config\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"subscription_token_hmac_secret_key\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"separate_secret_which_must_be_strong\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"unknown-config-keys-warnings\">Unknown config keys warnings<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#unknown-config-keys-warnings\" class=\"hash-link\" aria-label=\"Direct link to Unknown config keys warnings\" title=\"Direct link to Unknown config keys warnings\" translate=\"no\">​</a></h2>\n<p>With every release, Centrifugo offers more and more options. One thing we've noticed is that some options from previous Centrifugo options, which were already removed, still persist in the user's configuration file.</p>\n<p>Another issue is that a single typo in the configuration key can cost hours of debugging especially for Centrifugo new users. What is worse, the typo might result in unexpected behavior if the feature isn't properly tested before being run in production.</p>\n<p>In Centrifugo v5, we are addressing these problems. Now, Centrifugo logs on WARN level about unknown keys found in the configuration upon server start-up. Not only in the configuration file but also verifying the validity of environment variables (looking at those starting with <code>CENTRIFUGO_</code> prefix). This should help clean up the configuration to align with the latest Centrifugo release and catch typos at an early stage.</p>\n<p>It looks like this:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">08:25:33 [WRN] unknown key found in the namespace object key=watch namespace_name=xx</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">08:25:33 [WRN] unknown key found in the proxy object key=type proxy_name=connect</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">08:25:33 [WRN] unknown key found in the configuration file key=granulr_proxy_mode</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">08:25:33 [WRN] unknown key found in the environment key=CENTRIFUGO_ADDRES</span><br></div></code></pre></div></div>\n<p>These warnings do not prevent server to start so you can gradually clean up the configuration.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"simplifying-protocol-debug-with-postman\">Simplifying protocol debug with Postman<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#simplifying-protocol-debug-with-postman\" class=\"hash-link\" aria-label=\"Direct link to Simplifying protocol debug with Postman\" title=\"Direct link to Simplifying protocol debug with Postman\" translate=\"no\">​</a></h2>\n<p>Centrifugo v5 supports a special url parameter for bidirectional websocket which turns on using native WebSocket frame ping-pong mechanism instead of server-to-client application level pings Centrifugo uses by default. This simplifies debugging Centrifugo protocol with tools like Postman, wscat, websocat, etc.</p>\n<p>Previously it was inconvenient due to the fact Centrifugo sends periodic ping message to the client (<code>{}</code> in JSON protocol scenario) and expects pong response back within some time period. Otherwise Centrifugo closes connection. This results in problems with mentioned tools because you had to manually send <code>{}</code> pong message upon ping message. So typical session in <code>wscat</code> could look like this:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">❯ wscat --connect ws://localhost:8000/connection/websocket</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Connected (press CTRL+C to quit)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">&gt; {\"id\": 1, \"connect\": {}}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">&lt; {\"id\":1,\"connect\":{\"client\":\"9ac9de4e-5289-4ad6-9aa7-8447f007083e\",\"version\":\"0.0.0\",\"ping\":25,\"pong\":true}}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">&lt; {}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Disconnected (code: 3012, reason: \"no pong\")</span><br></div></code></pre></div></div>\n<p>The parameter is called <code>cf_ws_frame_ping_pong</code>, to use it connect to Centrifugo bidirectional WebSocket endpoint like <code>ws://localhost:8000/connection/websocket?cf_ws_frame_ping_pong=true</code>. Here is an example which demonstrates working with Postman WebSocket where we connect to local Centrifugo and subscribe to two channels <code>test1</code> and <code>test2</code>:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/postman.mp4\"></video>\n<p>You can then proceed to Centrifugo <a class=\"\" href=\"https://centrifugal.dev/docs/server/admin_web\">admin web UI</a>, publish something to these channels and see publications in Postman.</p>\n<p>Note, how we sent several JSON commands in one WebSocket frame to Centrifugo from Postman in the example above - this is possible since Centrifugo protocol supports batches of commands in line-delimited format.</p>\n<p>We consider this feature to be used only for debugging, <strong>in production prefer using our SDKs without using <code>cf_ws_frame_ping_pong</code> parameter</strong> – because app-level ping-pong is more efficient and our SDKs detect broken connections due to it.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"the-future-of-sockjs\">The future of SockJS<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#the-future-of-sockjs\" class=\"hash-link\" aria-label=\"Direct link to The future of SockJS\" title=\"Direct link to The future of SockJS\" translate=\"no\">​</a></h2>\n<p>As you know SockJS is deprecated in Centrifugal ecosystem since Centrifugo v4. In this release we are not removing support for it – but we may do this in the next release.</p>\n<p>Unfortunately, SockJS client repo is poorly maintained these days. And some of its iframe-based transports are becoming archaic. If you depend on SockJS and you really need fallback for WebSocket – consider switching to Centrifugo own bidirectional emulation for the browser which works over HTTP-streaming (using modern fetch API with Readable streams) or SSE. It should be more performant and work without sticky sessions requirement (sticky sessions is an optimization in our implementation). More details may be found in <a class=\"\" href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#modern-websocket-emulation-in-javascript\">Centrifugo v4 release post</a>.</p>\n<p>If you think SockJS is still required for your use case - reach us out so we could think about the future steps together.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"introducing-centrifugal-labs-ltd\">Introducing Centrifugal Labs LTD<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#introducing-centrifugal-labs-ltd\" class=\"hash-link\" aria-label=\"Direct link to Introducing Centrifugal Labs LTD\" title=\"Direct link to Introducing Centrifugal Labs LTD\" translate=\"no\">​</a></h2>\n<p>Finally, some important news we mentioned in the beginning!</p>\n<p>Centrifugo is now backed by the company called <strong>Centrifugal Labs LTD</strong> - a Cyprus-registered technology company. This should help us to finally launch <a class=\"\" href=\"https://centrifugal.dev/docs/pro/overview\">Centrifugo PRO</a> offering – the product we have been working on for a couple of years now and which has some unique and powerful features like <a class=\"\" href=\"https://centrifugal.dev/docs/pro/analytics\">real-time analytics</a> or <a class=\"\" href=\"https://centrifugal.dev/docs/pro/push_notifications\">push notification API</a>.</p>\n<p>As a Centrifugo user you will start noticing mentions of Centrifugal Labs LTD in our licenses, Github organization, throughout this web site. And that's mostly it - no radical changes at this point. We will still be working on improving Centrifugo trying to find a balance between OSS and PRO versions. Which is difficult TBH – but we will try.</p>\n<p>An ideal plan for us – make Centrifugo development sustainable enough to have the possibility for features from the PRO version flow to the OSS version eventually. The reality may be harder than this of course.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>That's all about most notable things happened in Centrifugo v5. We updated documentation to reflect the changes in v5, also some documentation chapters were rewritten. For example, take a look at the refreshed <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/design\">Design overview</a>. Several more changes and details outlined in the <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/migration_v5\">migration guide for Centifugo v5</a>. Please feel free to contact in the community rooms if you have questions about the release. And as usual, let the Centrifugal force be with you!</p>",
            "url": "https://centrifugal.dev/blog/2023/06/29/centrifugo-v5-released",
            "title": "Centrifugo v5 released",
            "summary": "We are excited to announce a new version of Centrifugo. It's an evolutionary step which makes Centrifugo cleaner and more intuitive to use.",
            "date_modified": "2023-06-29T00:00:00.000Z",
            "author": {
                "name": "Centrifugal team"
            },
            "tags": [
                "centrifugo",
                "release"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2023/03/31/keycloak-sso-centrifugo",
            "content_html": "<p>Securing user authentication and management can often be a challenging task when developing a modern application. As a result, many developers choose to delegate this responsibility to third-party identity providers, such as Okta, Auth0, or Keycloak.</p>\n<p>In this blog post, we'll go through the process of setting up Single Sign-On (SSO) authentication using Keycloak - popular and powerful identity provider. After setting up SSO we will create React application and connect to Centrifugo using access token generated by Keycloak for our test user:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/keycloak.mp4\"></video>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"tldr\">TLDR<a href=\"https://centrifugal.dev/blog/2023/03/31/keycloak-sso-centrifugo#tldr\" class=\"hash-link\" aria-label=\"Direct link to TLDR\" title=\"Direct link to TLDR\" translate=\"no\">​</a></h2>\n<p>The integraion is possible since Centrifugo works with <a class=\"\" href=\"https://centrifugal.dev/docs/server/authentication\">standard JWT for authentication</a> and additionally <a href=\"https://centrifugal.dev/docs/server/authentication#json-web-key-support\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">supports JSON Web Key</a> specification.</p>\n<p>Here is a final <a href=\"https://github.com/centrifugal/examples/tree/master/v4/keycloak_sso_auth\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">source code</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"keycloak\">Keycloak<a href=\"https://centrifugal.dev/blog/2023/03/31/keycloak-sso-centrifugo#keycloak\" class=\"hash-link\" aria-label=\"Direct link to Keycloak\" title=\"Direct link to Keycloak\" translate=\"no\">​</a></h2>\n<p>First, run Keycloak using the following Docker command:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker run --rm -it -p 8080:8080 \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    -e KEYCLOAK_ADMIN=admin \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    -e KEYCLOAK_ADMIN_PASSWORD=admin \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    quay.io/keycloak/keycloak:21.0.1 start-dev</span><br></div></code></pre></div></div>\n<p>After starting Keycloak, go to <a href=\"http://localhost:8080/admin\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:8080/admin</a> and login. Then perform the following tasks:</p>\n<ol>\n<li class=\"\">Create a new realm named <code>myrealm</code>.</li>\n<li class=\"\">Create a new client named <code>myclient</code>. Set valid redirect URIs to <code>http://localhost:5173/*</code>, and web origins as <code>http://localhost:5173</code>.</li>\n<li class=\"\">Create a user named <code>myuser</code> and set a password for it (in Credentials tab).</li>\n</ol>\n<p>See <a href=\"https://www.keycloak.org/getting-started/getting-started-docker\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">this guide</a> for additional details and illustrations of the process.</p>\n<p>Make sure your created client is <code>public</code> (this is default) since we will request token directly from the web application.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"centrifugo\">Centrifugo<a href=\"https://centrifugal.dev/blog/2023/03/31/keycloak-sso-centrifugo#centrifugo\" class=\"hash-link\" aria-label=\"Direct link to Centrifugo\" title=\"Direct link to Centrifugo\" translate=\"no\">​</a></h2>\n<p>Next, run Centrifugo using the following Docker command:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker run --rm -it -p 8000:8000 \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    -e CENTRIFUGO_ALLOWED_ORIGINS=\"http://localhost:5173\" \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    -e CENTRIFUGO_TOKEN_JWKS_PUBLIC_ENDPOINT=\"http://host.docker.internal:8080/realms/myrealm/protocol/openid-connect/certs\" \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    -e CENTRIFUGO_ALLOW_USER_LIMITED_CHANNELS=true \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    -e CENTRIFUGO_ADMIN=true \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    -e CENTRIFUGO_ADMIN_SECRET=secret \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    -e CENTRIFUGO_ADMIN_PASSWORD=admin \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifugo/centrifugo:v4.1.2 centrifugo</span><br></div></code></pre></div></div>\n<p>Some comments about environment variables used here:</p>\n<ul>\n<li class=\"\">CENTRIFUGO_TOKEN_JWKS_PUBLIC_ENDPOINT allows tell Centrifugo to use JSON Web Key spec when validating tokens, we point to Keycloak's JWKS endpoint</li>\n<li class=\"\">CENTRIFUGO_ALLOWED_ORIGINS is required since we will build Vite + React based app running on <a href=\"http://localhost:5173/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:5173</a></li>\n<li class=\"\">CENTRIFUGO_ALLOW_USER_LIMITED_CHANNELS - not required to connect, but you will see in the source code that we additionally subscribe to a user personal channel</li>\n<li class=\"\">CENTRIFUGO_ADMIN, CENTRIFUGO_ADMIN_SECRET, CENTRIFUGO_ADMIN_PASSWORD - to enable Centrifugo admin web UI</li>\n</ul>\n<p>Also note we are using <code>host.docker.internal</code> to access host port from inside the Docker network.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"react-app-with-vite\">React app with Vite<a href=\"https://centrifugal.dev/blog/2023/03/31/keycloak-sso-centrifugo#react-app-with-vite\" class=\"hash-link\" aria-label=\"Direct link to React app with Vite\" title=\"Direct link to React app with Vite\" translate=\"no\">​</a></h2>\n<p>Now, let's create a new React app using very popular <a href=\"https://vitejs.dev/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Vite</a> tool:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">npm create vite@latest keycloak_sso_auth -- --template react</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">cd keycloak_sso_auth</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">npm install</span><br></div></code></pre></div></div>\n<p>Also, install the necessary additional packages for the React app:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">npm install --save @react-keycloak/web centrifuge keycloak-js</span><br></div></code></pre></div></div>\n<p>And start the development server:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">npm run dev</span><br></div></code></pre></div></div>\n<p>Navigate to <a href=\"http://localhost:5173/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:5173/</a>. You should see default Vite template working, we are going to modify it a bit.</p>\n<div class=\"theme-admonition theme-admonition-caution admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>caution</div><div class=\"admonitionContent_BuS1\"><p>Use <code>localhost</code>, not <code>127.0.0.1</code> - since we used <code>localhost</code> for Keyloak and Centrifugo configurations above.</p></div></div>\n<p>Add the following into <code>main.jsx</code>:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports maybe-class-name\">React</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'react'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports maybe-class-name\">ReactDOM</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'react-dom/client'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token imports\"> </span><span class=\"token imports maybe-class-name\">ReactKeycloakProvider</span><span class=\"token imports\"> </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'@react-keycloak/web'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports maybe-class-name\">App</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'./App'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'./index.css'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports maybe-class-name\">Keycloak</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"keycloak-js\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> keycloakClient </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">url</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:8080\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">realm</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"myrealm\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">clientId</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"myclient\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token maybe-class-name\">ReactDOM</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">createRoot</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'root'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">render</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token maybe-class-name\">ReactKeycloakProvider</span><span class=\"token plain\"> authClient</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">keycloakClient</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token maybe-class-name\">React</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access maybe-class-name\">StrictMode</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token maybe-class-name\">App</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token maybe-class-name\">React</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access maybe-class-name\">StrictMode</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token maybe-class-name\">ReactKeycloakProvider</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>Note that we configured <code>Keycloak</code> instance pointing it to our Keycloak server. We also use <code>@react-keycloak/web</code> package to wrap React app into <code>ReactKeycloakProvider</code> component. It simplifies working with Keycloak by providing some useful hooks - we are using this hook below.</p>\n<p>Our <code>App</code> component inside <code>App.jsx</code> may look like this:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports maybe-class-name\">React</span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token imports\"> </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token imports\"> useState</span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token imports\"> useEffect </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'react'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports\">logo</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'./assets/centrifugo.svg'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token imports\"> </span><span class=\"token imports maybe-class-name\">Centrifuge</span><span class=\"token imports\"> </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"centrifuge\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token imports\"> useKeycloak </span><span class=\"token imports punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'@react-keycloak/web'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'./App.css'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function maybe-class-name\" style=\"color:rgb(130, 170, 255)\">App</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> initialized </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">useKeycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token plain\">initialized</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token keyword null nil\" style=\"font-style:italic\">null</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\">div</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\">header</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\">p</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token constant\" style=\"color:rgb(130, 170, 255)\">SSO</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">with</span><span class=\"token plain\"> </span><span class=\"token maybe-class-name\">Keycloak</span><span class=\"token plain\"> and </span><span class=\"token maybe-class-name\">Centrifugo</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">p</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">authenticated</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">?</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\">div</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\">p</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token maybe-class-name\">Logged</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">in</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">as</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">tokenParsed</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">?.</span><span class=\"token plain\">preferred_username</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">p</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\">button type</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"button\"</span><span class=\"token plain\"> onClick</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">logout</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">              </span><span class=\"token maybe-class-name\">Logout</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">button</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">div</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\">button type</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"button\"</span><span class=\"token plain\"> onClick</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">login</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token maybe-class-name\">Login</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">button</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">header</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">div </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword module\" style=\"font-style:italic\">export</span><span class=\"token plain\"> </span><span class=\"token keyword module\" style=\"font-style:italic\">default</span><span class=\"token plain\"> </span><span class=\"token maybe-class-name\">App</span><br></div></code></pre></div></div>\n<p>This is actually enough for SSO flow to start working! You can click on login button and make sure that it's possible to use <code>myuser</code> credentials to log into the application. And log out after that.</p>\n<p>The only missing part is Centrifugo. We can initialize connection inside <code>useEffect</code> hook of <code>App</code> component:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">useEffect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token plain\">initialized </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">||</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token plain\">keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">authenticated</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"ws://localhost:8000/connection/websocket\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">token</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">getToken</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Promise</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">resolve</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> reject</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">updateToken</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">5</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">then</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">resolve</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token keyword control-flow\" style=\"font-style:italic\">catch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">reject</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">logout</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">disconnect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> initialized</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>The important thing here is how we configure tokens: we are using Keycloak client methods to set initial token and refresh the token when required.</p>\n<p>I also added some extra elements to the code to make it look a bit nicer. For example, we can listen to Centriffuge client state changes and show connection indicator on the page:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function maybe-class-name\" style=\"color:rgb(130, 170, 255)\">App</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">connectionState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> setConnectionState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">useState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"disconnected\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> stateToEmoji </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string-property property\">\"disconnected\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"🔴\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string-property property\">\"connecting\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"🟠\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string-property property\">\"connected\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"🟢\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">useEffect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'state'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">setConnectionState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">newState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\">span className</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"connectionState \"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> connectionState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">stateToEmoji</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">connectionState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\">span</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;</span><br></div></code></pre></div></div>\n<p>You can find more details about Centrifugo client SDK API and states in <a class=\"\" href=\"https://centrifugal.dev/docs/transports/client_api\">client SDK spec</a>.</p>\n<p>If you look at source code on Github - you will also find an example of channel subscription to a user personal channel:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function maybe-class-name\" style=\"color:rgb(130, 170, 255)\">App</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">publishedData</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> setPublishedData</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">useState</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">useEffect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> userChannel </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"#\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> keycloak</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">tokenParsed</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">?.</span><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">userChannel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"publication\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">setPublishedData</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token known-class-name class-name\" style=\"color:rgb(255, 203, 107)\">JSON</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">stringify</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token spread operator\" style=\"color:rgb(137, 221, 255)\">...</span><br></div></code></pre></div></div>\n<p>You can now:</p>\n<ul>\n<li class=\"\">test the SSO setup by logging into application</li>\n<li class=\"\">making sure connection is successful</li>\n<li class=\"\">try publishing a message into a user channel via the <a href=\"http://localhost:8000/#/actions\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo Web UI</a>. The published message will appear on application screen in real-time.</li>\n</ul>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/keycloak_publish.mp4\"></video>\n<p>That's it! We have successfully set up Keycloak SSO authentication with Centrifugo and a React application. Again, <a href=\"https://github.com/centrifugal/examples/tree/master/v4/keycloak_sso_auth\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">source code</a> is on Github.</p>",
            "url": "https://centrifugal.dev/blog/2023/03/31/keycloak-sso-centrifugo",
            "title": "Setting up Keycloak SSO authentication flow and connecting to Centrifugo WebSocket",
            "summary": "This tutorial shows how to connect to Centrifugo when using Keycloak SSO flow for user authentication. Here we build a simple demo app using React and Vite.",
            "date_modified": "2023-03-31T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "keycloak",
                "sso",
                "authentication"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance",
            "content_html": "<p>The main objective of Centrifugo is to manage persistent client connections established over various real-time transports (including WebSocket, HTTP-Streaming, SSE, WebTransport, etc – see <a href=\"https://centrifugal.dev/docs/transports/overview\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">here</a>) and offer an API for publishing data towards established connections. Clients subscribe to channels, hence Centrifugo implements PUB/SUB mechanics to transmit published data to all online channel subscribers.</p>\n<p>Centrifugo employs <a href=\"https://redis.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Redis</a> as its primary scalability option – so that it's possible to distribute client connections amongst numerous Centrifugo nodes without worrying about channel subscribers connected to separate nodes. Redis is incredibly mature, simple, and fast in-memory storage. Due to various built-in data structures and PUB/SUB support Redis is a perfect fit to be both Centrifugo <code>Broker</code> and <code>PresenceManager</code> (we will describe what's this shortly).</p>\n<p>In Centrifugo v4.1.0 we introduced an updated implementation of our Redis Engine (<code>Engine</code> in Centrifugo == <code>Broker</code> + <code>PresenceManager</code>) which provides sufficient performance improvements to our users. This post discusses the factors that prompted us to update Redis Engine implementation and provides some insight into the results we managed to achieve. We'll examine a few well-known Go libraries for Redis communication and contrast them against Centrifugo tasks.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"broker-and-presencemanager\">Broker and PresenceManager<a href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance#broker-and-presencemanager\" class=\"hash-link\" aria-label=\"Direct link to Broker and PresenceManager\" title=\"Direct link to Broker and PresenceManager\" translate=\"no\">​</a></h2>\n<p>Before we get started, let's define what Centrifugo's <code>Broker</code> and <code>PresenceManager</code> terms mean.</p>\n<p><a href=\"https://github.com/centrifugal/centrifuge/blob/f6e948a15fd49000627377df2a7c94cadda1daf8/broker.go#L97\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Broker</a> is an interface responsible for maintaining subscriptions from different Centrifugo nodes (initiated by client connections). That helps to scale client connections over many Centrifugo instances and not worry about the same channel subscribers being connected to different nodes – since all Centrifugo nodes connected with PUB/SUB. Messages published to one node are delivered to a channel subscriber connected to another node.</p>\n<p>Another major part of <code>Broker</code> is keeping an expiring publication history for channels (streams). So that Centrifugo may provide a fast cache for messages missed by clients upon going offline for a short period and compensate at most once delivery of Redis PUB/SUB using <a href=\"https://github.com/centrifugal/centrifuge/blob/f6e948a15fd49000627377df2a7c94cadda1daf8/broker.go#L9\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Publication</a> incremental offsets. Centrifugo uses STREAM and HASH data structures in Redis to store channel history and stream meta information.</p>\n<p>In general Centrifugo architecture may be perfectly illustrated by this picture (Gophers are Centrifugo nodes all connected to <code>Broker</code>, and sockets are WebSockets):</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/QOJ1M9a.png\" alt=\"gopher-broker\" class=\"img_ev3q\"></p>\n<p><a href=\"https://github.com/centrifugal/centrifuge/blob/f6e948a15fd49000627377df2a7c94cadda1daf8/presence.go#L12\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">PresenceManager</a> is an interface responsible for managing online presence information - list of currently active channel subscribers. While the connection is alive we periodically update presence entries for channels connection subscribed to (for channels where presence is enabled). Presence data should expire if not updated by a client connection for some time. Centrifugo uses two Redis data structures for managing presence in channels - HASH and ZSET.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"redigo\">Redigo<a href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance#redigo\" class=\"hash-link\" aria-label=\"Direct link to Redigo\" title=\"Direct link to Redigo\" translate=\"no\">​</a></h2>\n<p>For a long time, the <a href=\"https://github.com/gomodule/redigo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">gomodule/redigo</a> package served as the foundation for the Redis Engine implementation in Centrifugo. Huge props go to <a href=\"https://github.com/garyburd\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Mr Gary Burd</a> for creating it.</p>\n<p>Redigo offers a connection <a href=\"https://pkg.go.dev/github.com/gomodule/redigo/redis#Pool\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Pool</a> to Redis. A simple usage of it involves getting the connection from the pool, issuing request to Redis over that connection, and then putting the connection back to the pool after receiving the result from Redis.</p>\n<p>Let's write a simple benchmark which demonstrates simple usage of Redigo and measures SET operation performance:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">BenchmarkRedigo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">b </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">testing</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">B</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tpool </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> redigo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Pool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tMaxIdle</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">   </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">128</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tMaxActive</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">128</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tWait</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">      </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tDial</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">redigo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Conn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> redigo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Dial</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"tcp\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\":6379\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">defer</span><span class=\"token plain\"> pool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Close</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">ResetTimer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">SetParallelism</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">128</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">ReportAllocs</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">RunParallel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">pb </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">testing</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">PB</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> pb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Next</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tc </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> pool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Get</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> c</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Do</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"SET\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redigo\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"test\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tc</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Close</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Let's run it:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkRedigo-8        228804        4648 ns/op        62 B/op         2 allocs/op</span><br></div></code></pre></div></div>\n<p>Seems pretty fast, but we can improve it further.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"redigo-with-pipelining\">Redigo with pipelining<a href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance#redigo-with-pipelining\" class=\"hash-link\" aria-label=\"Direct link to Redigo with pipelining\" title=\"Direct link to Redigo with pipelining\" translate=\"no\">​</a></h2>\n<p>To increase a throughput in Centrifugo, instead of using Redigo's <code>Pool</code> for each operation, we acquired a dedicated connection from the <code>Pool</code> and utilized <a href=\"https://redis.io/docs/manual/pipelining/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Redis pipelining</a> to send multiple commands where possible.</p>\n<p>Redis pipelining improves performance by executing multiple commands using a single client-server-client round trip. Instead of executing many commands one by one, you can queue the commands in a pipeline and then execute the queued commands as if it is a single command. Redis processes commands in order and sends individual response for each command. Given a single CPU nature of Redis, reducing the number of active connections when using pipelining has a positive impact on throughput – therefore pipelining is beneficial from this angle as well.</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"Redis pipeline\" src=\"https://centrifugal.dev/assets/images/redis_pipeline-7fdadb07739a87c92e520d0fba06696d.png\" width=\"2556\" height=\"1564\" class=\"img_ev3q\"></p>\n<p>You can quickly estimate the benefits of pipelining by running Redis locally and running <code>redis-benchmark</code> which comes with Redis distribution over it:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">&gt; redis-benchmark -n 100000 set key value</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Summary:</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  throughput summary: 84674.01 requests per second</span><br></div></code></pre></div></div>\n<p>And with pipelining:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">&gt; redis-benchmark -n 100000 -P 64 set key value</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Summary:</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  throughput summary: 666880.00 requests per second</span><br></div></code></pre></div></div>\n<p>In Centrifugo we are using smart batching technique for collecting pipeline (also described in <a class=\"\" href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket\">one of the previous posts</a> in this blog).</p>\n<p>To demonstrate benefits from using pipelining let's look at the following benchmark:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tmaxCommandsInPipeline </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">512</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tnumPipelineWorkers    </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> command </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\terrCh </span><span class=\"token keyword\" style=\"font-style:italic\">chan</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> sender </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tcmdCh </span><span class=\"token keyword\" style=\"font-style:italic\">chan</span><span class=\"token plain\"> command</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tpool  redigo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Pool</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">newSender</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">pool redigo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Pool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">sender </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tp </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">sender</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tcmdCh</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">make</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">chan</span><span class=\"token plain\"> command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tpool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">  pool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">go</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> i </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> i </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\"> numPipelineWorkers</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> i</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">++</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">runPipelineRoutine</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> p</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">sender</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">send</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\terrCh </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">make</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">chan</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tcmd </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\terrCh</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> errCh</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Submit command to be executed by runPipelineRoutine.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\ts</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">cmdCh </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\"> cmd</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\">errCh</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">sender</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">runPipelineRoutine</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tconn </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> p</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">pool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Get</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">defer</span><span class=\"token plain\"> conn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Close</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">select</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> cmd </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\">s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">cmdCh</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tcommands </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">command</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">cmd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tconn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Send</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"set\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redigo\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"test\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tloop</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Collect batch of commands to send to Redis in one RTT.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> i </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> i </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\"> maxCommandsInPipeline</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> i</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">++</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">select</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> cmd </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\">s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">cmdCh</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t\tcommands </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">append</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">commands</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cmd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t\tconn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Send</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"set\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redigo\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"test\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">default</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">break</span><span class=\"token plain\"> loop</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Flush all collected commands to the network.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\terr </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> conn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Flush</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> i </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> i </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">len</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">commands</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> i</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">++</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t\tcommands</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">i</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">errCh </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">continue</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Read responses to commands, they come in order.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> i </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> i </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">len</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">commands</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> i</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">++</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> conn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Receive</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tcommands</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\">i</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">errCh </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">BenchmarkRedigoPipelininig</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">b </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">testing</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">B</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tpool </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> redigo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Pool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tWait</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tDial</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">redigo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Conn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> redigo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Dial</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"tcp\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\":6379\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">defer</span><span class=\"token plain\"> pool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Close</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tsender </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">newSender</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">pool</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">ResetTimer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">SetParallelism</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">128</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">ReportAllocs</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">RunParallel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">pb </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">testing</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">PB</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> pb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Next</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\terr </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> sender</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">send</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>This is a strategy that we employed in Centrifugo for a long time. As you can see code with automatic pipelining gets more complex, and in real life it's even more complicated to support different types of commands, channel send timeouts, and server shutdowns.</p>\n<p>What about the performance of this approach?</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkRedigo-8               228804      4648 ns/op       62 B/op     2 allocs/op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkRedigoPipelininig-8   1840758     604.7 ns/op      176 B/op     4 allocs/op</span><br></div></code></pre></div></div>\n<p>Operation latency reduced from 4648 ns/op to 604.7 ns/op – not bad right?</p>\n<p>It's worth mentioning that upon increased RTT between application and Redis the approach with pipelining will provide worse throughput. But it still can be better than in pool-based approach. Let's say we have latency 5ms between app and Redis. This means that with pool size of 128 you will be able to issue up to <code>128 * (1000 / 5) = 25600</code> requests per second over 128 connections. With the pipelining approach above the theoretical limit is <code>512 * (1000 / 5) = 102400</code> requests per second over a single connection (though in case of using code for pipelining shown above we need to have larger parallelism, say 512 instead of 128). And it can scale further if you increase <code>numPipelineWorkers</code> to work over several connections in paralell. Though increasing <code>numPipelineWorkers</code> has negative effect on CPU – we will discuss this later in this post.</p>\n<p>Redigo is an awesome battle-tested library that served us great for a long time.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"motivation-to-migrate\">Motivation to migrate<a href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance#motivation-to-migrate\" class=\"hash-link\" aria-label=\"Direct link to Motivation to migrate\" title=\"Direct link to Motivation to migrate\" translate=\"no\">​</a></h2>\n<p>There are three modes in which Centrifugo can work with Redis these days:</p>\n<ol>\n<li class=\"\">Connecting to a standalone single Redis instance</li>\n<li class=\"\">Connecting to Redis in master-replica configuration, where Redis Sentinel controls the failover process</li>\n<li class=\"\">Connecting to Redis Cluster</li>\n</ol>\n<p>All modes additionally can be used with client-side consistent sharding. So it's possible to scale Redis even without a Redis Cluster setup.</p>\n<p>Unfortunately, with pure Redigo library, it's only possible to implement [ 1 ] – i.e. connecting to a single standalone Redis instance.</p>\n<p>To support the scheme with Sentinel you whether need to have a proxy between the application and Redis which proxies the connection to Redis master. For example, with Haproxy it's possible in this way:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">listen redis</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    server redis-01 127.0.0.1:6380 check port 6380 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2 on-marked-down shutdown-sessions on-marked-up shutdown-backup-sessions</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    server redis-02 127.0.0.1:6381 check port 6381 check inter 2s weight 1 inter 2s downinter 5s rise 10 fall 2 backup</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    bind *:6379</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    mode tcp</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    option tcpka</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    option tcplog</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    option tcp-check</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    tcp-check send PING\\r\\n</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    tcp-check expect string +PONG</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    tcp-check send info\\ replication\\r\\n</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    tcp-check expect string role:master</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    tcp-check send QUIT\\r\\n</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    tcp-check expect string +OK</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    balance roundrobin</span><br></div></code></pre></div></div>\n<p>Or, you need to additionally import <a href=\"https://github.com/FZambia/sentinel\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">FZambia/sentinel</a> library - which provides a communication layer with Redis Sentinel on top of Redigo's connection Pool.</p>\n<p>For communicating with Redis Cluster one more library may be used – <a href=\"https://github.com/mna/redisc\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">mna/redisc</a> which is also a layer on top of <code>redigo</code> basic functionality.</p>\n<p>Combining <code>redigo</code> + <code>FZambia/sentinel</code> + <code>mna/redisc</code> we managed to implement all three connection modes. This worked, though resulted in rather tricky Redis setup. Also, it was difficult to re-use existing pipelining code we had for a standalone Redis with Redis Cluster. As a result, Centrifugo only used pipelining in a standalone or Sentinel Redis cases. When using Redis Cluster, however, Centrifugo merely used the connection pool to issue requests thus not benefiting from request pipelining. Due to this we had some code duplication to send the same requests in various Redis configurations.</p>\n<p>Another thing is that Redigo uses <code>interface{}</code> for command construction. To send command to Redis Redigo has <code>Do</code> method which accepts name of the command and variadic <code>interface{}</code> arguments to construct command arguments:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Do</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">commandName </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> args </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token keyword\" style=\"font-style:italic\">interface</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">reply </span><span class=\"token keyword\" style=\"font-style:italic\">interface</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>While this works well and you can issue any command to Redis, you need to be very accurate when constructing a command. This also adds some allocation overhead. As we know more memory allocations lead to the increased CPU utilization because the allocation process itself requires more processing power and the GC is under more strain.</p>\n<p>At some point we felt that eliminating additional dependencies (even though I am the author of one of them) and reducing allocations in Redis communication layer is a nice step forward for Centrifugo. So we started looking around for <code>redigo</code> alternatives.</p>\n<p>To summarize, here is what we wanted from Redis library:</p>\n<ul>\n<li class=\"\">Possibility to work with all three Redis setup options we support: standalone, master-replica(s) with Sentinel, Redis Cluster, so we can depend on one library instead of three</li>\n<li class=\"\">Less memory allocations (and more type-safety API is a plus)</li>\n<li class=\"\">Support working with RESP2-only Redis servers as we need that for backwards compatibility. And some vendors like Redis Enterprise still support RESP2 protocol only</li>\n<li class=\"\">The library should be actively maintained</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"go-redisredis\">Go-redis/redis<a href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance#go-redisredis\" class=\"hash-link\" aria-label=\"Direct link to Go-redis/redis\" title=\"Direct link to Go-redis/redis\" translate=\"no\">​</a></h2>\n<p>The most obvious alternative to Redigo is <a href=\"https://github.com/go-redis/redis\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">go-redis/redis</a> package. It's popular, regularly gets updates, used by a huge amount of Go projects (Grafana, Thanos, etc.). And maintained by\n<a href=\"https://github.com/vmihailenco\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Vladimir Mihailenco</a> who created several more awesome Go libraries, like <a href=\"https://github.com/vmihailenco/msgpack\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">msgpack</a> for example. I personally successfully used <code>go-redis/redis</code> in several other projects I worked on.</p>\n<p>To avoid setup boilerplate for various Redis installation variations <code>go-redis/redis</code> has <a href=\"https://pkg.go.dev/github.com/go-redis/redis/v9#UniversalClient\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">UniversalClient</a>. From docs:</p>\n<blockquote>\n<p>UniversalClient is a wrapper client which, based on the provided options, represents either a ClusterClient, a FailoverClient, or a single-node Client. This can be useful for testing cluster-specific applications locally or having different clients in different environments.</p>\n</blockquote>\n<p>In terms of implementation <code>go-redis/redis</code> also has internal pool of connections to Redis, similar to <code>redigo</code>. It's also possible to use <a href=\"https://pkg.go.dev/github.com/go-redis/redis/v9#Client.Pipeline\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Client.Pipeline</a> method to allocate a <a href=\"https://pkg.go.dev/github.com/go-redis/redis/v9#Pipeliner\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Pipeliner</a> interface and use it for pipelining. So <code>UniversalClient</code> reduces setup boilerplate for different Redis installation types and number of dependencies we had, and it provide very similar way to pipeline requests so we could easily re-implement things we had with Redigo.</p>\n<p>Go-redis also provides more type-safety when constructing commands compared to Redigo, almost every command in Redis is implemented as a separate method of <code>Client</code>, for example <code>Publish</code> <a href=\"https://pkg.go.dev/github.com/go-redis/redis/v9#Client.Publish\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">defined</a> as:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">c Client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Publish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> channel </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> message </span><span class=\"token keyword\" style=\"font-style:italic\">interface</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">IntCmd</span><br></div></code></pre></div></div>\n<p>You can see though that we still have <code>interface{}</code> here for <code>message</code> argument type. I suppose this was implemented in such way for convenience – to pass both <code>string</code> or <code>[]byte</code>. But it still produces some extra allocations.</p>\n<p>Without pipelining the simplest program with <code>go-redis/redis</code> may look like this:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">BenchmarkGoredis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">b </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">testing</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">B</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tclient </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> redis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewUniversalClient</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">redis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">UniversalOptions</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tAddrs</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\":6379\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tPoolSize</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">128</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">defer</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Close</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">ResetTimer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">SetParallelism</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">128</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">ReportAllocs</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">RunParallel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">pb </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">testing</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">PB</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> pb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Next</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tresp </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Set</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Background</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"goredis\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"test\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> resp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">resp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Let's run it:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkRedigo-8        228804        4648 ns/op        62 B/op         2 allocs/op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkGoredis-8       268444        4561 ns/op       244 B/op         8 allocs/op</span><br></div></code></pre></div></div>\n<p>Result is pretty comparable to Redigo, though Go-redis allocates more (btw most of allocations come from the connection liveness check upon getting from the pool which can not be turned off).</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/goredis_allocs-3b6ad3ecb2543d679e812619438962c3.png\" width=\"1884\" height=\"986\" class=\"img_ev3q\"></p>\n<p>It's interesting – if we dive deeper into what is it we can discover that this is the only way in Go to check connection was closed without reading data from it. The approach was originally introduced <a href=\"https://github.com/go-sql-driver/mysql/blob/41dd159e6ec9afad00d2b90144bbc083ea860db1/conncheck.go#L23\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">by go-sql-driver/mysql</a>, it's not cross-platform, and <a href=\"https://github.com/golang/go/issues/15735\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">related issue</a> may be found in Go issue tracker.</p>\n<p>But as I said in Centrifugo we already used pipelining over the dedicated connection for all operations so we avoid frequently getting connections from the pool. And early experiments proved that <code>go-redis</code> may provide some performance benefits for our use case.</p>\n<p>At some point <a href=\"https://github.com/j178\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">@j178</a> sent <a href=\"https://github.com/centrifugal/centrifuge/pull/235\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">a pull request</a> to Centrifuge library with <code>Broker</code> and <code>PresenceManager</code> implementations based on <code>go-redis/redis</code>. The amount of code to cover all the various Redis setups was reduced, we got only one dependency instead of three 🔥</p>\n<p>But what about performance? Here we will show results for several operations which are typical for Centrifugo:</p>\n<ol>\n<li class=\"\">Publish a message to a channel without saving it to the history - this is just a Redis PUBLISH command going through Redis PUB/SUB system (<code>RedisPublish</code>)</li>\n<li class=\"\">Publish message to a channel with saving it to history - this involves executing the LUA script on Redis side where we add a publication to STREAM data structure, update meta information HASH, and finally PUBLISH to PUB/SUB (<code>RedisPublish_History</code>)</li>\n<li class=\"\">Subscribe to a channel - that's a SUBSCRIBE Redis command, this is important to have it fast as Centrifugo should be able to re-subscribe to all the channels in the system upon <a class=\"\" href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#massive-reconnect\">mass client reconnect scenario</a> (<code>RedisSubscribe</code>)</li>\n<li class=\"\">Recovering missed publication state from channel STREAM, this is again may be called lots of times when all clients reconnect at once (<code>RedisRecover</code>).</li>\n<li class=\"\">Updating connection presence information - many connections may periodically update their channel online presence information in Redis (<code>RedisAddPresence</code>)</li>\n</ol>\n<p>Here are the benchmark results we got when comparing <code>redigo</code> (v1.8.9) implementation (old) and <code>go-redis/redis</code> (v9.0.0-rc.2) implementation (new) with Redis v6.2.7 on Mac with M1 processor and benchmark paralellism 128:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">❯ benchstat redigo_p128.txt goredis_p128.txt</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">name                      old time/op    new time/op    delta</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish-8            1.45µs ±10%    1.88µs ± 4%  +29.32%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish_History-8    12.5µs ± 6%     9.7µs ± 3%  -22.77%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisSubscribe-8          1.47µs ±24%    1.47µs ±10%     ~     (p=0.469 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisRecover-8            18.4µs ± 2%     6.3µs ± 0%  -65.78%  (p=0.000 n=10+8)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisAddPresence-8        3.72µs ± 1%    3.40µs ± 1%   -8.74%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">name                      old alloc/op   new alloc/op   delta</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish-8              483B ± 0%      499B ± 0%   +3.37%  (p=0.000 n=9+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish_History-8    1.30kB ± 0%    1.08kB ± 0%  -16.67%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisSubscribe-8            892B ± 2%      662B ± 6%  -25.83%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisRecover-8            1.25kB ± 1%    1.00kB ± 0%  -19.91%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisAddPresence-8          907B ± 0%      827B ± 0%   -8.82%  (p=0.002 n=7+8)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">name                      old allocs/op  new allocs/op  delta</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish-8              10.0 ± 0%       9.0 ± 0%  -10.00%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish_History-8      29.0 ± 0%      25.0 ± 0%  -13.79%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisSubscribe-8            22.0 ± 0%      14.0 ± 0%  -36.36%  (p=0.000 n=8+7)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisRecover-8              29.0 ± 0%      23.0 ± 0%  -20.69%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisAddPresence-8          18.0 ± 0%      17.0 ± 0%   -5.56%  (p=0.000 n=10+10)</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span>danger</div><div class=\"admonitionContent_BuS1\"><p>Please note that this benchmark is not a pure performance comparison of two Go libraries for Redis – it's a performance comparison of Centrifugo Engine methods upon switching to a new library.</p></div></div>\n<p>Or visualized in Grafana:</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/redis_vis01-8364cd7474502a278b9362d5f5e71245.png\" width=\"2268\" height=\"1336\" class=\"img_ev3q\"></p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>Centrifugo benchmarks results shown in the post use parallelism 128. If someone interested to check numbers for paralellism 1 or 16 – <a href=\"https://github.com/centrifugal/centrifugal.dev/pull/18#issuecomment-1356263272\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">check out this comment on Github</a>.</p></div></div>\n<p>We observe a noticeable reduction in allocations in these benchmarks and in most benchmarks (presented here and other not listed in this post) we observed a reduced latency.</p>\n<p>Overall, results convinced us that the migration from <code>redigo</code> to <code>go-redis/redis</code> may provide Centrifugo with everything we aimed for – all the goals for a <code>redigo</code> alternative outlined above were successfully fullfilled.</p>\n<p>One good thing <code>go-redis/redis</code> allowed us to do is to use Redis pipelining also in a Redis Cluster case. It's possible due to the fact that <code>go-redis/redis</code> <a href=\"https://github.com/go-redis/redis/blob/c561f3ca7e5cf44ce1f1d3ef30f4a10a9c674c8a/cluster.go#L1062\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">re-maps pipeline objects internally</a> based on keys to execute pipeline on the correct node of Redis Cluster. Actually, we could do the same based on <code>redigo</code> + <code>mna/redisc</code>, but here we got it for free.</p>\n<p>BTW, there is <a href=\"https://redis.uptrace.dev/guide/go-redis-vs-redigo.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">a page with comparison</a> between <code>redigo</code> and <code>go-redis/redis</code> in <code>go-redis/redis</code> docs which outlines some things I mentioned here and some others.</p>\n<p>But we have not migrated to <code>go-redis/redis</code> in the end. And the reason is another library – <code>rueidis</code>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"rueidis\">Rueidis<a href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance#rueidis\" class=\"hash-link\" aria-label=\"Direct link to Rueidis\" title=\"Direct link to Rueidis\" translate=\"no\">​</a></h2>\n<p>While results were good with <code>go-redis/redis</code> we also made an attempt to implement Redis Engine on top of <a href=\"https://github.com/rueian/rueidis\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">rueian/rueidis</a> library written by <a href=\"https://github.com/rueian\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">@rueian</a>. According to docs, <code>rueidis</code> is:</p>\n<blockquote>\n<p>A fast Golang Redis client that supports Client Side Caching, Auto Pipelining, Generics OM, RedisJSON, RedisBloom, RediSearch, RedisAI, RedisGears, etc.</p>\n</blockquote>\n<p>The readme of <code>rueidis</code> contains benchmark results where it hugely outperforms <code>go-redis/redis</code> in terms of operation latency/throughput in both single Redis and Redis Custer setups:</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/rueidis_1-05b9a03175ec64b4ec70679313bd4a38.png\" width=\"2846\" height=\"1448\" class=\"img_ev3q\"></p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/rueidis_2-55b7b75cf531852fed8f73ceb7883b4d.png\" width=\"2846\" height=\"1438\" class=\"img_ev3q\"></p>\n<p><code>rueidis</code> works with standalone Redis, Sentinel Redis and Redis Cluster out of the box. Just like <code>UniversalClient</code> of <code>go-redis/redis</code>. So it also allowed us to reduce code boilerplate to work with all these setups.</p>\n<p>Again, let's try to write a simple program like we had for Redigo and Go-redis above:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">BenchmarkRueidis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">b </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">testing</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">B</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tclient</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> rueidis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewClient</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">rueidis</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">ClientOption</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tInitAddress</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\":6379\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">ResetTimer</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">SetParallelism</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">128</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">ReportAllocs</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">RunParallel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">pb </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">testing</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">PB</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> pb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Next</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tcmd </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">B</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Set</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"rueidis\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Value</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"test\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Build</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tres </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Do</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Background</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cmd</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>And run it:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkRedigo-8        228804        4648 ns/op        62 B/op         2 allocs/op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkGoredis-8       268444        4561 ns/op       244 B/op         8 allocs/op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkRueidis-8      2908591        418.5 ns/op        4 B/op         1 allocs/op</span><br></div></code></pre></div></div>\n<p><code>rueidis</code> library comes with <strong>automatic implicit pipelining</strong>, so you can send each request in isolated way while <code>rueidis</code> makes sure the request becomes part of the pipeline sent to Redis – thus utilizing the connection between an application and Redis most efficiently with maximized throughput. The idea of implicit pipelining with Redis is not new and Go ecosystem already had <a href=\"https://github.com/joomcode/redispipe\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">joomcode/redispipe</a> library which implemented it (though it comes with some limitations which made it unsuitable for Centrifugo use case).</p>\n<p>So <strong>applications that use a pool-based approach</strong> for communication with Redis may observe dramatic improvements in latency and throughput when switching to the Rueidis library.</p>\n<p>For Centrifugo we didn't expect such a huge speed-up as shown in the above graphs since we already used pipelining in Redis Engine. But <code>rueidis</code> implements some ideas which allow it to be efficient. Insights about these ideas are provided by Rueidis author in a \"Writing a High-Performance Golang Client Library\" series of posts on Medium:</p>\n<ul>\n<li class=\"\"><a href=\"https://betterprogramming.pub/writing-high-performance-golang-client-library-part-1-batching-on-pipeline-97988fe3211\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Part 1: Batching on Pipeline</a></li>\n<li class=\"\"><a href=\"https://betterprogramming.pub/working-on-high-performance-golang-client-library-reading-again-from-channels-5e98ff3538cf\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Part 2: Reading Again From Channels?</a></li>\n<li class=\"\"><a href=\"https://betterprogramming.pub/working-on-high-performance-golang-client-library-remove-the-bad-busy-loops-with-the-sync-cond-e262b3fcb458\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Part 3: Remove the Bad Busy Loops With the Sync.Cond</a></li>\n</ul>\n<p>I did some prototypes with <code>rueidis</code> which were super-promising in terms of performance. There were some issues found during that early prototyping (mostly with PUB/SUB) – but all of them were quickly resolved by Rueian.</p>\n<p>Until <code>v0.0.80</code> release <code>rueidis</code> did not support RESP2 though, so we could not replace our Redis Engine implementation with it. But as soon as it got RESP2 support we opened <a href=\"https://github.com/centrifugal/centrifuge/pull/262\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">a pull request with alternative implementation</a>.</p>\n<p>Since auto-pipelining is used in <code>rueidis</code> by default we were able to remove some of our own pipelining management code – so the Engine implementation is more concise now. One more thing to mention is a simpler PUB/SUB code we were able to write with <code>rueidis</code>. One example is that in <code>redigo</code> case we had to periodically PING PUB/SUB connection to maintain it alive, <code>rueidis</code> does this automatically.</p>\n<p>Regarding performance, here are the benchmark results we got when comparing <code>redigo</code> (v1.8.9) implementation (old) and <code>rueidis</code> (v0.0.90) implementation (new):</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">❯ benchstat redigo_p128.txt rueidis_p128.txt</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">name                      old time/op    new time/op    delta</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish-8            1.45µs ±10%    0.56µs ± 1%  -61.53%  (p=0.000 n=10+9)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish_History-8    12.5µs ± 6%     9.7µs ± 1%  -22.43%  (p=0.000 n=10+9)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisSubscribe-8          1.47µs ±24%    1.45µs ± 1%     ~     (p=0.484 n=10+9)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisRecover-8            18.4µs ± 2%     6.2µs ± 1%  -66.08%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisAddPresence-8        3.72µs ± 1%    3.60µs ± 1%   -3.34%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">name                      old alloc/op   new alloc/op   delta</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish-8              483B ± 0%       91B ± 0%  -81.16%  (p=0.000 n=9+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish_History-8    1.30kB ± 0%    0.39kB ± 0%  -70.08%  (p=0.000 n=10+8)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisSubscribe-8            892B ± 2%      360B ± 0%  -59.66%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisRecover-8            1.25kB ± 1%    0.36kB ± 1%  -71.52%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisAddPresence-8          907B ± 0%      151B ± 1%  -83.34%  (p=0.000 n=7+9)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">name                      old allocs/op  new allocs/op  delta</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish-8              10.0 ± 0%       2.0 ± 0%  -80.00%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish_History-8      29.0 ± 0%      10.0 ± 0%  -65.52%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisSubscribe-8            22.0 ± 0%       6.0 ± 0%  -72.73%  (p=0.002 n=8+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisRecover-8              29.0 ± 0%       7.0 ± 0%  -75.86%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisAddPresence-8          18.0 ± 0%       3.0 ± 0%  -83.33%  (p=0.000 n=10+10)</span><br></div></code></pre></div></div>\n<p>Or visualized in Grafana:</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/redis_vis02-a8e078e75bac446b0c907beeeaf368ae.png\" width=\"2278\" height=\"1326\" class=\"img_ev3q\"></p>\n<p>2.5x times more publication throughput than we had before! Instead of 700k publications/sec, we went towards 1.7 million publications/sec due to drastically decreased publish operation latency (1.45µs -&gt; 0.59µs). This means that our previous Engine implementation under-utilized Redis, and Rueidis just pushes us towards Redis limits. The latency of most other operations is also reduced.</p>\n<p>The allocation effectiveness of the implementation based on \"rueidis\" is best. As you can see <code>rueidis</code> helped us to generate sufficiently fewer memory allocations for all our Redis operations. Allocation improvements directly affect Centrifugo node CPU usage. Though we will talk about CPU more later below.</p>\n<p>For Redis Cluster case we also got benchmark results similar to the standalone Redis results above.</p>\n<p>I might add that I enjoyed building commands with <code>rueidis</code>. All Redis commands may be constructed using a builder approach. Rueidis comes with builders generated for all Redis commands. As an illustration, this is a process of building a PUBLISH Redis command:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/rueidis_cmd.mp4\"></video>\n<p>This drastically reduces a chance to make a stupid mistake while constructing a command. Instead of always opening Redis docs to see a command syntax it's now possible to just start typing - and quickly come to the complete command to send.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"switching-to-rueidis-reducing-cpu-usage\">Switching to Rueidis: reducing CPU usage<a href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance#switching-to-rueidis-reducing-cpu-usage\" class=\"hash-link\" aria-label=\"Direct link to Switching to Rueidis: reducing CPU usage\" title=\"Direct link to Switching to Rueidis: reducing CPU usage\" translate=\"no\">​</a></h2>\n<p>After making all these benchmarks and implementing Engine in Rueidis I decided to check whether Centrifugo consumes less CPU with it. I expected a notable CPU reduction as Rueidis Engine implementation allocates much less than Redigo-based. Turned out it's not that simple.</p>\n<p>I ran Centrifugo with some artificial load and noticed that CPU consumption of the new implementation is actually... worse than we had with Redigo-based engine under equal conditions!😩 But why?</p>\n<p>As I mentioned above Redis pipelining is a technique when several commands may be combined into one batch to send over the network. In case of automatic pipelining the size of generated batches start playing a crucial role in application and Redis CPU usage – since smaller command batches result into more read/write system calls to the kernel on both application and Redis server sides. That's why projects like <a href=\"https://github.com/twitter/twemproxy\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Twemproxy</a> which sit between app and Redis have sich a good effect on Redis CPU usage among other things.</p>\n<p>As we have seen above, Rueidis provides a better throughput and latency, but it's more agressive in terms of flushing data to the network. So in its default configuration we get smaller batches under th equal conditions than we had before in our own pipelining implementation based on Redigo (shown in the beginning of this post).</p>\n<p>Luckily, there is an option in Rueidis called <code>MaxFlushDelay</code> which allows to slow down write loop a bit to give Rueidis a chance to collect more commands to send in one batch. When this option is used Rueidis will make a pause after each network flush not bigger than selected value of <code>MaxFlushDelay</code> (please note, that this is a delay after flushing collected pipeline commands, not an additional delay for each request). Using some reasonable value it's possible to drastically reduce both application and Redis CPU utilization.</p>\n<p>To demonstrate this I created a repo: <a href=\"https://github.com/FZambia/pipelines\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://github.com/FZambia/pipelines</a>.</p>\n<p>This repo contains three benchmarks where we use automatic pipelining: based on <code>redigo</code>, based on <code>go-redis/redis</code> and <code>rueidis</code>. In these benchmarks we produce concurrent requests, but instead of pushing the system towards the limits we are limiting number of requests sent to Redis, so we put all libraries in equal conditions.</p>\n<p>To rate limit requests we are using <a href=\"https://github.com/uber-go/ratelimit\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">uber-go/ratelimit</a> library. For example, to allow rate no more than 100k commands per second we can do sth like this:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">rl </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> ratelimit</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">100</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ratelimit</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Per</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Millisecond</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\trl</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Take</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">...</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>We limit requests per second we could actually just write <code>ratelimit.New(100000)</code> – but we aim to get a more smooth distribution of requests over time - so using millisecond resolution.</p>\n<p>Let's run all the benchmarks in the default configuration:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/redis_b1.mp4\"></video>\n<p>Average CPU usage during the test (a bit rough but enough for demonstration):</p>\n<table><thead><tr><th></th><th>Redigo</th><th>Go-redis/redis</th><th>Rueidis</th></tr></thead><tbody><tr><td>Application CPU, %</td><td>95</td><td>99</td><td><span style=\"color:red\">116</span></td></tr><tr><td>Redis CPU, %</td><td>36</td><td>35</td><td><span style=\"color:red\">42</span></td></tr></tbody></table>\n<p>OK, Rueidis-based implementation is the worst here despite of allocating less than others. So let's try to change this by setting <code>MaxFlushDelay</code> to sth like 100 microseconds:</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/redis_b2.mp4\"></video>\n<p>Now CPU usage is:</p>\n<table><thead><tr><th></th><th>Redigo</th><th>Go-redis/redis</th><th>Rueidis</th></tr></thead><tbody><tr><td>Application CPU, %</td><td>95</td><td>99</td><td><span style=\"color:green\">59</span></td></tr><tr><td>Redis CPU, %</td><td>36</td><td>35</td><td><span style=\"color:green\">12</span></td></tr></tbody></table>\n<p>So we can achieve great CPU usage reduction. CPU went from 116% to 59% for the application side, and from 42% to only 12% for Redis! We are sacrificing latency though. Given the fact the CPU utilization reduction is very notable the trade-off is pretty fair.</p>\n<div class=\"theme-admonition theme-admonition-caution admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>caution</div><div class=\"admonitionContent_BuS1\"><p>It's definitely possible to improve CPU usage in Redigo and Go-redis/redis cases too – using similar technique. But the goal here was to improve Rueidis-based engine implementation to make it comparable or better than our Redigo-based implementation in terms of CPU utilization.</p></div></div>\n<p>As you can see we were able to achieve better CPU results just by using 100 microseconds delay after each network flush. In real life, where we are not running Redis on localhost and have some network latency in between application and Redis, this delay should be insignificant at all. Indeed, adding <code>MaxFlushDelay</code> can even improve (!) the latency you have. You may wonder what happened with benchmarks we showed above after we added <code>MaxFlushDelay</code> option. In Centrifugo we chose default value 100 microseconds, and here are results on localhost (<code>old</code> without delay, <code>new</code> with delay):</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">&gt; benchstat rueidis_p128.txt rueidis_delay_p128.txt</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">name                      old time/op    new time/op    delta</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish-8             559ns ± 1%     468ns ± 0%  -16.35%  (p=0.000 n=9+8)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish_History-8    9.72µs ± 1%    9.67µs ± 1%   -0.52%  (p=0.007 n=9+8)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisSubscribe-8          1.45µs ± 1%    1.27µs ± 1%  -12.49%  (p=0.000 n=9+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisRecover-8            6.25µs ± 1%    5.85µs ± 0%   -6.32%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisAddPresence-8        3.60µs ± 1%    3.33µs ± 1%   -7.52%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">(rest is not important here...)</span><br></div></code></pre></div></div>\n<p>It's even better for this set of benchmarks. Though while it's better for these benchmarks the numbers may differ for other under different conditions. For example, in the benchmarks we run we use concurrency 128, if we reduce concurrency we will notice reduced throughput – as batches Rueidis collects become smaller. Smaller batches + some delay to collect = less requests per second to send.</p>\n<p>The problem is that the value to pause Rueidis write loop is a very use case specific, it's pretty hard to provide a reasonable default for it. Depending on request rate/size, network latency etc. you may choose a larger or smaller delay. In v4.1.0 we start with hardcoded 100 microsecond <code>MaxFlushDelay</code> which seems sufficient for most use cases and showed good results in the benchmarks - though possibly we will have to make it tunable later.</p>\n<p>To check that Centrifugo benchmarks also utilize less CPU I added rate limiter (50k rps per second) to benchmarks and compared version without <code>MaxFlushDelay</code> and with 100 microsecond <code>MaxFlushDelay</code>:</p>\n<table><thead><tr><th>50k req per second</th><th>Without delay</th><th>With 100mks delay</th></tr></thead><tbody><tr><td>BenchmarkPublish</td><td>Centrifugo - 75%, Redis - 24%</td><td>Centrifugo - 44%, Redis - 9%</td></tr><tr><td>BenchmarkPublish_History</td><td>Centrifugo - 80% , Redis - 67%</td><td>Centrifugo - 55%, Redis - 50%</td></tr><tr><td>BenchmarkSubscribe</td><td>Centrifugo - 80%, Redis - 30%</td><td>Centrifugo - 45% , Redis - 14%</td></tr><tr><td>BenchmarkRecover</td><td>Centrifugo - 84%, Redis - 51%</td><td>Centrifugo - 51%, Redis - 36%</td></tr><tr><td>BenchmarkPresence</td><td>Centrifugo - 114%, Redis - 69%</td><td>Centrifugo - 90%, Redis - 60%</td></tr></tbody></table>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>In this test I replaced <code>BenchmarkAddPresence</code> with <code>BenchmarkPresence</code> (get information about all online subscribers in channel) to also make sure we have CPU reduction when using read-intensive method, i.e. when Redis response is reasonably large.</p></div></div>\n<p>We observe a notable CPU usage improvement here.</p>\n<p>Hope you understand now why increasing <code>numPipelineWorkers</code> value in the pipelining code showed before results into increased CPU usage on app and Redis sides – due to smaller batch sizes and more read/write system calls as the consequence.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>BTW, would it be a nice thing if Go benchmarking suite could show a CPU usage of the process in addition to time and alloc stats? 🤔</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"adding-latency\">Adding latency<a href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance#adding-latency\" class=\"hash-link\" aria-label=\"Direct link to Adding latency\" title=\"Direct link to Adding latency\" translate=\"no\">​</a></h2>\n<p>The last thing to check is how new implementation works upon increased RTT between application and Redis. To add artificial latency on localhost on Linux one can use <code>tc</code> tool as <a href=\"https://daniel.haxx.se/blog/2010/12/14/add-latency-to-localhost/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">shown here</a> by Daniel Stenberg. But I am on MacOS so the simplest way I found was using <a href=\"https://github.com/Shopify/toxiproxy\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Shopify/toxiproxy</a>. Sth like running a server:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">toxyproxy-server</span><br></div></code></pre></div></div>\n<p>And then in another terminal I used <code>toxiproxy-cli</code> to create toxic Redis proxy with additional latency on port 26379:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">toxiproxy-cli create -l localhost:26379 -u localhost:6379 toxic_redis</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">toxiproxy-cli toxic add -t latency -a latency=5 toxic_redis</span><br></div></code></pre></div></div>\n<p>The benchmark results are (<code>old</code> is Redigo-based, new is Rueidis-based):</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">&gt; benchstat redigo_latency_p128.txt rueidis_delay_latency_p128.txt</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">name                      old time/op    new time/op    delta</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish-8            31.5µs ± 1%     5.6µs ± 3%   -82.26%  (p=0.000 n=9+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisPublish_History-8    62.8µs ± 3%    10.6µs ± 4%   -83.05%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisSubscribe-8          1.52µs ± 5%    6.05µs ± 8%  +298.70%  (p=0.000 n=8+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisRecover-8            48.3µs ± 3%     7.3µs ± 4%   -84.80%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">RedisAddPresence-8        52.3µs ± 4%     5.8µs ± 2%   -88.94%  (p=0.000 n=10+10)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">(rest is not important here...)</span><br></div></code></pre></div></div>\n<p>We see that new Engine implementation behaves much better for most cases. But what happened to <code>Subscribe</code> operation? It did not change at all in Redigo case – the same performance as if there is no additional latency involved!</p>\n<p>Turned out that when we call <code>Subscribe</code> in Redigo case, Redigo only flushes data to the network without waiting synchronously for subscribe result.</p>\n<p>It makes sense in general and we can listen to subscribe notifications asynchronously, but in Centrifugo we relied on the returned error thinking that it includes succesful subscription result from Redis - meaning that we already subscribed to a channel at that point. And this could theoretically lead to some rare bugs in Centrifugo.</p>\n<p>Rueidis library waits for subscribe response. So here the behavior of <code>rueidis</code> while differs from <code>redigo</code> in terms of throughput under increased latency just fits Centrifugo better in terms of behavior. So we go with it.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>Migrating from Redigo to Rueidis library was not just a task of rewriting code, we had to carefully test various aspects of Redis Engine behaviour – latency, throughput, CPU utilization of application, and even CPU utilization of Redis itself under the equal application load conditions.</p>\n<p>I think that we will find more projects in Go ecosystem using <code>rueidis</code> library shortly. Not just because of its allocation efficiency and out-of-the-box throughput, but also due to a convenient type-safe command API.</p>\n<p>For most Centrifugo users this migration means more efficient CPU usage as new implementation allocates less memory (less work to allocate and less strain on GC) and we tried to find a reasonable batch size to reduce the number of system calls for common operations. While latency and throughput of single Centrifugo node should be better as we make concurrent Redis calls from many goroutines.</p>\n<p>Hopefully readers will learn some tips from this post which can help to achieve effective communication with Redis from Go or another programming language.</p>\n<p>A few key takeaways:</p>\n<ul>\n<li class=\"\">Redis pipelining may increase throughput and reduce latency, it can also reduce CPU utilization of Redis</li>\n<li class=\"\">Don't blindly trust Go benchmark numbers but also think about CPU effect of changes you made (sometimes of the external system also)</li>\n<li class=\"\">Reduce the number of system calls to decrease CPU utilization</li>\n<li class=\"\">Everything is a trade-off – latency or resource usage? Your own WebSocket server or Centrifugo?</li>\n<li class=\"\">Don't rely on someone's else benchmarks, including those published here. <strong>Measure for your own use case</strong>. Take into account your load profile, paralellism, network latency, data size, etc.</li>\n</ul>\n<p>P.S. One thing worth mentioning and which may be helpful for someone is that during our comparison experiments we discovered that Redis 7 has a major latency increase compared to Redis 6 when executing Lua scripts. So if you have performance sensitive code with Lua scripts take a look at <a href=\"https://github.com/redis/redis/issues/10981\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">this Redis issue</a>. With the help of Redis developers some things already improved in <code>unstable</code> Redis branch, hopefully that issue will be closed at the time you read this post.</p>",
            "url": "https://centrifugal.dev/blog/2022/12/20/improving-redis-engine-performance",
            "title": "Improving Centrifugo Redis Engine throughput and allocation efficiency with Rueidis Go library",
            "summary": "In this post we share some details about Centrifugo Redis Engine implementation and its recent performance improvements with the help of Rueidis Go library",
            "date_modified": "2022-12-20T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "redis",
                "go"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe",
            "content_html": "<p>Let's say you develop an application and want a real-time connection which is subscribed to one channel. Let's also assume that this channel is used for user personal notifications. So only one user in the application can subcribe to that channel to receive its notifications in real-time.</p>\n<p>In this post we will look at various ways to achieve this with Centrifugo, and consider trade-offs of the available approaches. The main goal of this tutorial is to help Centrifugo newcomers be aware of all the ways to control channel permissions by reading just one document.</p>\n<p>And... well, there are actually 8 ways I found, not 101 😇</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"setup\">Setup<a href=\"https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe#setup\" class=\"hash-link\" aria-label=\"Direct link to Setup\" title=\"Direct link to Setup\" translate=\"no\">​</a></h2>\n<p>To make the post a bit easier to consume let's setup some things. Let's assume that the user for which we provide all the examples in this post has ID <code>\"17\"</code>. Of course in real-life the examples given here can be extrapolated to any user ID.</p>\n<p>When you create a real-time connection to Centrifugo the connection is authenticated using the one of the following ways:</p>\n<ul>\n<li class=\"\">using <a class=\"\" href=\"https://centrifugal.dev/docs/server/authentication\">connection JWT</a></li>\n<li class=\"\">using connection request proxy from Centrifugo to the configured endpoint of the application backend (<a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy#connect-proxy\">connect proxy</a>)</li>\n</ul>\n<p>As soon as the connection is successfully established and authenticated Centrifugo knows the ID of connected user. This is important to understand.</p>\n<p>And let's define a namespace in Centrifugo configuration which will be used for personal user channels:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    ...</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"namespaces\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"personal\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"presence\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Defining namespaces for each new real-time feature is a good practice in Centrifugo. As an awesome improvement we also enabled <code>presence</code> in the <code>personal</code> namespace, so whenever users subscribe to a channel in this namespace Centrifugo will maintain online presence information for each channel. So you can find out all connections of the specific user existing at any moment. Defining <code>presence</code> is fully optional though - turn it of if you don't need presence information and don't want to spend additional server resources on maintaining presence.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"1--user-limited-channel\">#1 – user-limited channel<a href=\"https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe#1--user-limited-channel\" class=\"hash-link\" aria-label=\"Direct link to #1 – user-limited channel\" title=\"Direct link to #1 – user-limited channel\" translate=\"no\">​</a></h2>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>Probably the most performant approach.</p></div></div>\n<p>All you need to do is to extend namespace configuration with <code>allow_user_limited_channels</code> option:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"namespaces\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"personal\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"presence\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"allow_user_limited_channels\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>On the client side you need to have sth like this (of course the ID of current user will be dynamic in real-life):</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'personal:#17'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Here you are subscribing to a channel in <code>personal</code> namespace and listening to publications coming from a channel. Having <code>#</code> in channel name tells Centrifugo that this is a user-limited channel (because <code>#</code> is a special symbol that is treated in a special way by Centrifugo as soon as <code>allow_user_limited_channels</code> enabled).</p>\n<p>In this case the user ID part of user-limited channel is <code>\"17\"</code>. So Centrifugo allows user with ID <code>\"17\"</code> to subscribe on <code>personal:#17</code> channel. Other users won't be able to subscribe on it.</p>\n<p>To publish updates to subscription all you need to do is to publish to <code>personal:#17</code> using server publish API (HTTP or GRPC).</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"2---channel-token-authorization\">#2 - channel token authorization<a href=\"https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe#2---channel-token-authorization\" class=\"hash-link\" aria-label=\"Direct link to #2 - channel token authorization\" title=\"Direct link to #2 - channel token authorization\" translate=\"no\">​</a></h2>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>Probably the most flexible approach, with reasonably good performance characteristics.</p></div></div>\n<p>Another way we will look at is using subscription JWT for subscribing. When you create Subscription object on the client side you can pass it a subscription token, and also provide a function to retrieve subscription token (useful to automatically handle token refresh, it also handles initial token loading).</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> token </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">getSubscriptionToken</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'personal:17'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'personal:17'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">token</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> token</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Inside <code>getSubscriptionToken</code> you can issue a request to the backend, for example in browser it's possible to do with fetch API.</p>\n<p>On the backend side you know the ID of current user due to the native session mechanism of your app, so you can decide whether current user has permission to subsribe on <code>personal:17</code> or not. If yes – return subscription JWT according to our rules. If not - return empty string so subscription will go to unsubscribed state with <code>unauthorized</code> reason.</p>\n<p>Here are examples for generating subscription HMAC SHA-256 JWTs for channel <code>personal:17</code> and HMAC secret key <code>secret</code>:</p>\n<!-- -->\n<div class=\"theme-tabs-container tabs-container tabList__CuJ\"><ul role=\"tablist\" aria-orientation=\"horizontal\" class=\"tabs unique-tabs\"><li role=\"tab\" tabindex=\"0\" aria-selected=\"true\" class=\"tabs__item tabItem_LNqP tabs__item--active\">Python</li><li role=\"tab\" tabindex=\"-1\" aria-selected=\"false\" class=\"tabs__item tabItem_LNqP\">NodeJS</li></ul><div class=\"margin-top--md\"><div role=\"tabpanel\" class=\"tabItem_Ymn6\"><div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> jwt</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> time</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">claims </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"sub\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"17\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"channel\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"personal:17\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"exp\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">int</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">30</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">60</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">token </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> jwt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">encode</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">claims</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"secret\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> algorithm</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"HS256\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">decode</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">print</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div></div><div role=\"tabpanel\" class=\"tabItem_Ymn6\" hidden=\"\"><div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> jose </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">require</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'jose'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">main</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> secret </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">TextEncoder</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">encode</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'secret'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> alg </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'HS256'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> token </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">jose</span><span class=\"token class-name punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">SignJWT</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token string-property property\">'sub'</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'17'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string-property property\">'channel'</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'personal:17'</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">setProtectedHeader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> alg </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">setExpirationTime</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'30m'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">sign</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">secret</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div></div></div></div>\n<p>Since we set expiration time for subscription JWT tokens we also need to provide a <code>getToken</code> function to a client on the frontend side:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'personal:17'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function-variable function\" style=\"color:rgb(130, 170, 255)\">getToken</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> token </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">getSubscriptionToken</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'personal:17'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token plain\"> token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>This function will be called by SDK automatically to refresh subscription token when it's going to expire. And note that we omitted setting <code>token</code> option here – since SDK is smart enough to call provided <code>getToken</code> function to extract initial subscription token from the backend.</p>\n<p>The good thing in using subscription JWT approach is that you can provide token expiration time, so permissions to subscribe on a channel will be validated from time to time while connection is active. You can also provide additional channel context info which will be attached to presence information (using <code>info</code> claim of subscription JWT). And you can granularly control channel permissions using <code>allow</code> claim of token – and give client capabilities to publish, call history or presence information (this is Centrifugo PRO feature at this point). Token also allows to override some namespace options on per-subscription basis (with <code>override</code> claim).</p>\n<p>Using subscription tokens is a general approach for any channels where you need to check access first, not only for personal user channels.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"3---subscribe-proxy\">#3 - subscribe proxy<a href=\"https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe#3---subscribe-proxy\" class=\"hash-link\" aria-label=\"Direct link to #3 - subscribe proxy\" title=\"Direct link to #3 - subscribe proxy\" translate=\"no\">​</a></h2>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>Probably the most secure approach.</p></div></div>\n<p>Subscription JWT gives client a way to subscribe on a channel, and avoid requesting your backend for permission on every resubscribe. Token approach is very good in massive reconnect scenario, when you have many connections and they all resubscribe at once (due to your load balancer reload, for example). But this means that if you unsubscribed client from a channel using server API, client can still resubscribe with token again - until token will expire. In some cases you may want to avoid this.</p>\n<p>Also, in some cases you want to be notified when someone subscribes to a channel.</p>\n<p>In this case you may use subscribe proxy feature. When using subscribe proxy every attempt of a client to subscribe on a channel will be translated to request (HTTP or GRPC) from Centrifugo to the application backend. Application backend can decide whether client is allowed to subscribe or not.</p>\n<p>One advantage of using subscribe proxy is that backend can additionally provide initial channel data for the subscribing client. This is possible using <code>data</code> field of subscribe result generated by backend subscribe handler.</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"proxy_subscribe_endpoint\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:9000/centrifugo/subscribe\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"namespaces\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"personal\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"presence\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"proxy_subscribe\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>And on the backend side define a route <code>/centrifugo/subscribe</code>, check permissions of user upon subscription and return result to Centrifugo according to our subscribe proxy docs. Or simply run GRPC server using our proxy definitions and react on subscription attempt sent from Centrifugo to backend over GRPC.</p>\n<p>On the client-side code is as simple as:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">newSubscription</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'personal:17'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"4---server-side-channel-in-connection-jwt\">#4 - server-side channel in connection JWT<a href=\"https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe#4---server-side-channel-in-connection-jwt\" class=\"hash-link\" aria-label=\"Direct link to #4 - server-side channel in connection JWT\" title=\"Direct link to #4 - server-side channel in connection JWT\" translate=\"no\">​</a></h2>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>The approach where you don't need to manage client-side subscriptions.</p></div></div>\n<p><a class=\"\" href=\"https://centrifugal.dev/docs/server/server_subs\">Server-side subscriptions</a> is a way to consume publications from channels without even create Subscription objects on the client side. In general, client side Subscription objects provide a more flexible and controllable way to work with subscriptions. Clients can subscribe/unsubscribe on channels at any point. Client-side subscriptions provide more details about state transitions.</p>\n<p>With server-side subscriptions though you are consuming publications directly from Client instance:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> client </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'ws://localhost:8000/connection/websocket'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">token</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'CONNECTION-JWT'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publication received from server-side channel'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>In this case you don't have separate Subscription objects and need to look at <code>ctx.channel</code> upon receiving publication or to publication content to decide how to handle it. Server-side subscriptions could be a good choice if you are using Centrifugo unidirectional transports and don't need dynamic subscribe/unsubscribe behavior.</p>\n<p>The first way to subscribe client on a server-side channel is to include <code>channels</code> claim into connection JWT:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"sub\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"17\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"channels\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"personal:17\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Upon successful connection user will be subscribed to a server-side channel by Centrifugo. One downside of using server-side channels is that errors in one server-side channel (like impossible to recover missed messages) may affect the entire connection and result into reconnects, while with client-side subscriptions individual subsription failures do not affect the entire connection.</p>\n<p>But having one server-side channel per-connection seems a very reasonable idea to me in many cases. And if you have stable set of subscriptions which do not require lifetime state management – this can be a nice approach without additional protocol/network overhead involved.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"5---server-side-channel-in-connect-proxy\">#5 - server-side channel in connect proxy<a href=\"https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe#5---server-side-channel-in-connect-proxy\" class=\"hash-link\" aria-label=\"Direct link to #5 - server-side channel in connect proxy\" title=\"Direct link to #5 - server-side channel in connect proxy\" translate=\"no\">​</a></h2>\n<p>Similar to the previous one for cases when you are authenticating connections over connect proxy instead of using JWT.</p>\n<p>This is possible using <code>channels</code> field of connect proxy handler result. The code on the client-side is the same as in Option #4 – since we only change the way how list of server-side channels is provided.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"6---automatic-personal-channel-subscription\">#6 - automatic personal channel subscription<a href=\"https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe#6---automatic-personal-channel-subscription\" class=\"hash-link\" aria-label=\"Direct link to #6 - automatic personal channel subscription\" title=\"Direct link to #6 - automatic personal channel subscription\" translate=\"no\">​</a></h2>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>Almost no code approach.</p></div></div>\n<p>As we pointed above Centrifugo knows an ID of the user due to authentication process. So why not combining this knowledge with automatic server-side personal channel subscription? Centrifugo provides exactly this with user personal channel feature.</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"user_subscribe_to_personal\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"user_personal_channel_namespace\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"personal\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"namespaces\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"personal\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"presence\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>This feature only subscribes non-anonymous users to personal channels (those with non-empty user ID). The configuration above will subscribe our user <code>\"17\"</code> to channel <code>personal:#17</code> automatically after successful authentication.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"7--capabilities-in-connection-jwt\">#7 – capabilities in connection JWT<a href=\"https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe#7--capabilities-in-connection-jwt\" class=\"hash-link\" aria-label=\"Direct link to #7 – capabilities in connection JWT\" title=\"Direct link to #7 – capabilities in connection JWT\" translate=\"no\">​</a></h2>\n<p>Allows using client-side subscriptions, but skip receiving subscription token. This is only available in Centrifugo PRO at this point.</p>\n<p>So when generating JWT you can provide additional <code>caps</code> claim which contains channel resource capabilities:</p>\n<div class=\"theme-tabs-container tabs-container tabList__CuJ\"><ul role=\"tablist\" aria-orientation=\"horizontal\" class=\"tabs unique-tabs\"><li role=\"tab\" tabindex=\"0\" aria-selected=\"true\" class=\"tabs__item tabItem_LNqP tabs__item--active\">Python</li><li role=\"tab\" tabindex=\"-1\" aria-selected=\"false\" class=\"tabs__item tabItem_LNqP\">NodeJS</li></ul><div class=\"margin-top--md\"><div role=\"tabpanel\" class=\"tabItem_Ymn6\"><div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> jwt</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> time</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">claims </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"sub\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"17\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"exp\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">int</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">30</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">60</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"caps\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"channels\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"personal:17\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"allow\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"sub\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">token </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> jwt</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">encode</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">claims</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"secret\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> algorithm</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"HS256\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">decode</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">print</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div></div><div role=\"tabpanel\" class=\"tabItem_Ymn6\" hidden=\"\"><div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> jose </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">require</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'jose'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">main</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> secret </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">TextEncoder</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">encode</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'secret'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> alg </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'HS256'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> token </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">jose</span><span class=\"token class-name punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">SignJWT</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">sub</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'17'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">caps</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string-property property\">\"channels\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"personal:17\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string-property property\">\"allow\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"sub\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">setProtectedHeader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> alg </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">setExpirationTime</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'30m'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">sign</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">secret</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">token</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div></div></div></div>\n<p>While in case of single channel the benefit of using this approach is not really obvious, it can help when you are using several channels with stric access permissions per connection, where providing capabilities can help to save some traffic and CPU resources since we avoid generating subscription token for each individual channel.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"8--capabilities-in-connect-proxy\">#8 – capabilities in connect proxy<a href=\"https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe#8--capabilities-in-connect-proxy\" class=\"hash-link\" aria-label=\"Direct link to #8 – capabilities in connect proxy\" title=\"Direct link to #8 – capabilities in connect proxy\" translate=\"no\">​</a></h2>\n<p>This is very similar to the previous approach, but capabilities are passed to Centrifugo in connect proxy result. So if you are using connect proxy for auth then you can still provide capabilities in the same form as in JWT. This is also a Centrifugo PRO feature.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"teardown\">Teardown<a href=\"https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe#teardown\" class=\"hash-link\" aria-label=\"Direct link to Teardown\" title=\"Direct link to Teardown\" translate=\"no\">​</a></h2>\n<p>Which way to choose? Well, it depends. Since your application will have more than only a personal user channel in many cases you should decide which approach suits you better in each particular case – it's hard to give the universal advice.</p>\n<p>Client-side subscriptions are more flexible in general, so I'd suggest using them whenever possible. Though you may use unidirectional transports of Centrifugo where subscribing to channels from the client side is not simple to achieve (though still possible using our server subscribe API). Server-side subscriptions make more sense there.</p>\n<p>The good news is that all our official bidirectional client SDKs support all the approaches mentioned in this post. Hope designing the channel configuration on top of Centrifugo will be a pleasant experience for you.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Attributions</div><div class=\"admonitionContent_BuS1\"><ul>\n<li class=\"\">\n<a href=\"https://www.freepik.com/vectors/internet-network\">Internet network vector created by rawpixel.com - </a><a href=\"http://www.freepik.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">www.freepik.com</a>\n</li>\n<li class=\"\">\n<a href=\"https://www.flaticon.com/free-icons/cyber-security\" title=\"cyber security icons\">Cyber security icons created by Smashicons - Flaticon</a>\n</li>\n</ul></div></div>",
            "url": "https://centrifugal.dev/blog/2022/07/29/101-way-to-subscribe",
            "title": "101 ways to subscribe user on a personal channel in Centrifugo",
            "summary": "In this post we are discussing vaious ways developers can use to subscribe user to a personal channel in Centrifugo",
            "date_modified": "2022-07-29T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "tutorial"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released",
            "content_html": "<p>Today we are excited to announce the next generation of Centrifugo – Centrifugo v4. The release takes Centrifugo to the next level in terms of client protocol performance, WebSocket fallback simplicity, SDK ecosystem and channel security model. It also comes with a couple of cutting-edge technologies to experiment with such as HTTP/3 and WebTransport.</p>\n<div class=\"theme-admonition theme-admonition-info admonition_xJq3 alert alert--info\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z\"></path></svg></span>About Centrifugo</div><div class=\"admonitionContent_BuS1\"><p>If you've never heard of Centrifugo before, it's an open-source scalable real-time messaging server written in Go language. Centrifugo can instantly deliver messages to application online users connected over supported transports (WebSocket, HTTP-streaming, Server-Sent Events (SSE), GRPC, SockJS). Centrifugo has the concept of a channel – so it's a user-facing PUB/SUB server.</p><p>Centrifugo is language-agnostic and can be used to build chat apps, live comments, multiplayer games, real-time data visualizations, collaborative tools, etc. in combination with any backend. It is well suited for modern architectures and allows decoupling the business logic from the real-time transport layer.</p><p>Several official client SDKs for browser and mobile development wrap the bidirectional protocol. In addition, Centrifugo supports a unidirectional approach for simple use cases with no SDK dependency.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"centrifugo-v3-flashbacks\">Centrifugo v3 flashbacks<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#centrifugo-v3-flashbacks\" class=\"hash-link\" aria-label=\"Direct link to Centrifugo v3 flashbacks\" title=\"Direct link to Centrifugo v3 flashbacks\" translate=\"no\">​</a></h2>\n<p>Let's start from looking back a bit. Centrifugo v3 was released last year. It had a great list of improvements – like unidirectional transports support (EventSource, HTTP-streaming and GRPC), GRPC transport for proxy, history iteration API, faster JSON protocol, super-fast but experimental Tarantool engine implementation, and others.</p>\n<p>During the Centrifugo v3 lifecycle we added even more JSON protocol optimizations and introduced a granular proxy mode. Experimental Tarantool engine has also evolved a bit.</p>\n<p>But Centrifugo v3 did not contain anything... let's say <strong>revolutional</strong>. Revolutional for Centrifugo itself, community, or even the entire field of open-source real-time messaging.</p>\n<p>With this release, we feel that we bring innovation to the ecosystem. Now let's talk about it and introduce all the major things of the brand new v4 release.</p>\n<video width=\"100%\" loop=\"\" autoplay=\"\" muted=\"\" src=\"/img/v4_logo.mp4\"></video>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"unified-client-sdk-api\">Unified client SDK API<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#unified-client-sdk-api\" class=\"hash-link\" aria-label=\"Direct link to Unified client SDK API\" title=\"Direct link to Unified client SDK API\" translate=\"no\">​</a></h2>\n<p>The most challenging part of Centrifugo project is not a server itself. Client SDKs are the hardest part of the ecosystem. We try to time additional improvements to the SDKs with each major release of the server. But this time the SDKs are the centerpiece of the v4 release.</p>\n<p>Centrifugo uses bidirectional asynchronous protocol between client and server. On top of this protocol SDK provides a request-response over an asynchronous connection, reconnection logic, subscription management and multiplexing, timeout and error handling, ping-pong, token refresh, etc. Some of these things are not that trivial to implement. And all this should be implemented in different programming languages. As you may know, we have official real-time SDKs in Javascript, Dart, Swift, Java and Go.</p>\n<p>While implementing the same protocol and same functions, all SDKs behaved slightly differently. That was the result of the missing SDK specification. Without a strict SDK spec, it was hard to document things, hard to explain the exact details of the real-time SDK behavior. What we did earlier in the Centrifugo documentation – was pointing users to specific SDK Github repo to look for behaviour details.</p>\n<p>The coolest thing about Centrifugo v4 is the next generation SDK API. We now have a <a class=\"\" href=\"https://centrifugal.dev/docs/transports/client_api\">client SDK API specification</a>. It's a source of truth for SDKs behavior which try to follow the spec closely.</p>\n<p>The new SDK API is the result of several iterations and reflections on possible states, transitions, token refresh mechanism, etc. Users in our Telegram group may remember how it all started:</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"Centrifugo scheme\" src=\"https://centrifugal.dev/assets/images/states_prototype-104a12d8315c3ad7dfd6fc983979e134.jpg\" width=\"1280\" height=\"960\" class=\"img_ev3q\"></p>\n<p>And after several iterations these prototypes turned into working mechanisms with well-defined behaviour:</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"Centrifugo scheme\" src=\"https://centrifugal.dev/assets/images/client_state-34264b7a7eee2792baa58bb5bb525d46.png\" width=\"2352\" height=\"1700\" class=\"img_ev3q\"></p>\n<p>A few things that have been revised from the ground up:</p>\n<ul>\n<li class=\"\">Client states, transitions, events</li>\n<li class=\"\">Subscription states, transitions, events</li>\n<li class=\"\">Connection and subscription token refresh behavior</li>\n<li class=\"\">Ping-pong behavior (see details below)</li>\n<li class=\"\">Resubscribe logic (SDKs can now resubscribe with backoff)</li>\n<li class=\"\">Error handling</li>\n<li class=\"\">Unified backoff behavior (based on <a href=\"https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">full jitter technique</a>)</li>\n</ul>\n<p>We now also have a separation between temporary and non-temporary protocol errors – this allows us to handle subscription internal server errors on the SDK level, making subscriptions more resilient, with automatic resubscriptions, and to ensure individual subscription failures do not affect the entire connection.</p>\n<p>The mechanics described in the client SDK API specification are now implemented in all of our official SDKs. The SDKs now support all major client protocol features that currently exist. We believe this is a big step forward for the Centrifugo ecosystem and community.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"modern-websocket-emulation-in-javascript\">Modern WebSocket emulation in Javascript<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#modern-websocket-emulation-in-javascript\" class=\"hash-link\" aria-label=\"Direct link to Modern WebSocket emulation in Javascript\" title=\"Direct link to Modern WebSocket emulation in Javascript\" translate=\"no\">​</a></h2>\n<p>WebSocket is supported almost everywhere these days. But there is a case that we believe is the last one preventing users to connect over WebSocket - corporate proxies. With the root certificate installed on employee computer machines, these proxies can block WebSocket traffic, even if it's wrapped in a TLS layer. That's really annoying, and often developers choose to not support clients connecting from such \"broken\" environments at all.</p>\n<p>Prior to v4, Centrifugo users could use the SockJS polyfill library to fill this gap.</p>\n<p>SockJS is great software – stable and field proven. It is still used by some huge real-time messaging players out there to polyfill the WebSocket transport.</p>\n<p>But SockJS is an extra frontend dependency with a bunch of legacy transports, and <a href=\"https://github.com/sockjs/sockjs-client/issues/592\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">the future of it is unknown</a>.</p>\n<p>SockJS comes with a notable overhead – it's an aditional protocol wrapper, consumes more memory per connection on a server (at least when using SockJS-Go library – the only choice for implementing SockJS server in Go language these days). When using SockJS, Centrifugo users were losing the ability to use our main pure WebSocket transport because SockJS uses its own WebSocket implementation on a server side.</p>\n<p>SockJS does not support binary data transfer – only JSON format can be used with it. As you know, our main WebSocket transport works fine with binary in case of using Protobuf protocol format. So with SockJS we don't have fallback for WebSocket with a binary data transfer.</p>\n<p>And finally, if you want to use SockJS with a distributed backend, you must enable sticky session support on the load-balancer level. This way you can point requests from the client to the server to the correct server node – the one which maintains a persistent unidirectional HTTP connection.</p>\n<p>We danced around the idea of replacing SockJS for a long time. But only now we are ready to provide our alternative to it – meet Centrifugo own <strong>bidirectional emulation layer</strong>. It's based on two additional transports:</p>\n<ul>\n<li class=\"\">HTTP-streaming (using modern browser <a href=\"https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">ReadableStream API</a> in JavaScript, supports both binary Protobuf and JSON transfer)</li>\n<li class=\"\">Eventsource (Server-Sent Events, SSE) – while a bit older choice and works with JSON only EventSource transport is loved by many developers and can provide fallback in slightly older browsers which don't have ReadableStream, so we implemented bidirectional emulation with it too.</li>\n</ul>\n<p>So when the fallback is used, you always have a real-time, persistent connection in server -&gt; to -&gt; client direction. Requests in client -&gt; to -&gt; server direction are regular HTTP – similar to how SockJS works. But our bidirectional emulation layer does not require sticky sessions – Centrifugo can proxy client-to-server requests to the correct node in the cluster. <strong>Having sticky sessions is an optimization</strong> for Centrifugo bidirectional emulation layer, <strong>not a requirement</strong>. We believe that this is a game changer for our users – no need to bother about proper load balancing, especially since in most cases 95% or even more users will be able to connect using the WebSocket transport.</p>\n<p>Here is a simplified diagram of how it works:</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"Scheme\" src=\"https://centrifugal.dev/assets/images/emulation_scheme-7488282ce671166bd0ca4369bb8af842.png\" width=\"2713\" height=\"1474\" class=\"img_ev3q\"></p>\n<p>The bidirectional emulation layer is only supported by the Javascript SDK (<code>centrifuge-js</code>) – as we think fallbacks mostly make sense for browsers. If we find use cases where other SDKs can benefit from HTTP based transport – we can expand on them later.</p>\n<p>Let's look at example of using this feature from the Javascript side. To use fallbacks, all you need to do is to set up a list of desired transports with endpoints:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> transports </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'websocket'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'wss://your_centrifugo.com/connection/websocket'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'http_stream'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'https://your_centrifugo.com/connection/http_stream'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'sse'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'https://your_centrifugo.com/connection/sse'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">transports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>We are using explicit transport endpoints in the above example due to the fact that transport endpoints can be configured separately in Centrifugo – there is no single entry point for all transports. Like the one in Socket.IO or SockJS when developer can only point client to the base address. In Centrifugo case, we are requesting an explicit transport/endpoint configuration from the SDK user.</p></div></div>\n<p>By the way, a few advantages of HTTP-based transport over WebSocket:</p>\n<ul>\n<li class=\"\">Sessions can be automatically multiplexed within a single connection by the browser when the server is running over HTTP/2, while with WebSocket browsers open a separate connection in each browser tab</li>\n<li class=\"\">Better compression support (may be enabled on load balancer level)</li>\n<li class=\"\">WebSocket requires special configuration in some load balancers to get started (ex. Nginx)</li>\n</ul>\n<p>SockJS is still supported by Centrifugo and <code>centrifuge-js</code>, but it's now DEPRECATED.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"no-layering-in-client-protocol\">No layering in client protocol<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#no-layering-in-client-protocol\" class=\"hash-link\" aria-label=\"Direct link to No layering in client protocol\" title=\"Direct link to No layering in client protocol\" translate=\"no\">​</a></h2>\n<p>Not only the API of client SDK has changed, but also the format of Centrifugo protocol messages. New format is more human-readable (in JSON case, of course), has a more compact ping message size (more on that below).</p>\n<p>The client protocol is now one-shot encode/decode compatible. Previously, Centrifugo protocol had a layered structure and we had to encode some messages before appending them to the top-level message. Or decode two or three times to unwrap the message envelope. To achieve good performance when encoding and decoding client protocol messages, Centrifugo had to use various optimization techniques – like buffer memory pools, byte slice memory pools.</p>\n<p>By restructuring the message format, we were able to avoid layering, which allowed us to slightly increase the performance of encoding/decoding without additional optimization tricks.</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"Scheme\" src=\"https://centrifugal.dev/assets/images/avoid_protocol_nesting-fb312e08bc008cbd7d4318c9c8357b44.png\" width=\"4796\" height=\"833\" class=\"img_ev3q\"></p>\n<p>We also simplified the <a class=\"\" href=\"https://centrifugal.dev/docs/transports/client_protocol\">client protocol</a> documentation overview a bit.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"redesigned-ping-pong\">Redesigned PING-PONG<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#redesigned-ping-pong\" class=\"hash-link\" aria-label=\"Direct link to Redesigned PING-PONG\" title=\"Direct link to Redesigned PING-PONG\" translate=\"no\">​</a></h2>\n<p>In many cases in practice (when dealing with persistent connections like WebSocket), pings and pongs are the most dominant types of messages passed between client and server. Your application may have many concurrent connections, but only a few of them receive the useful payload. But at the same time, we still need to send pings and respond with pongs. Thus, optimizing the ping-pong process can significantly reduce server resource usage.</p>\n<p>One optimization comes from the revised PING-PONG behaviour. Previous versions of Centrifugo and SDKs sent ping/pong in both \"client-&gt;to-&gt;server\" and \"server-&gt;to-&gt;client\" directions (for WebSocket transport). This allowed finding non-active connections on both client and server sides.</p>\n<p>In Centrifugo v4 we only send pings from a server to a client and expect pong from a client. On the client-side, we have a timer which fires if there hasn't been a ping from the server within the configured time, so we still have a way to detect closed connections.</p>\n<p>Sending pings only in one direction results in 2 times less ping-pong messages - and this should be really noticable for Centrifugo installations with thousands of concurrent connections. In our experiments with 10k connections, server CPU usage was reduced by 30% compared to Centrifugo v3.</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"Scheme\" src=\"https://centrifugal.dev/assets/images/ping_pong_v3_v4-b6f5699b9dc3e68059833185661a1f1f.png\" width=\"2767\" height=\"1445\" class=\"img_ev3q\"></p>\n<p>Pings and pongs are application-level messages. Ping is just an empty asynchronous reply – for example in JSON case it's a 2-byte message: <code>{}</code>. Pong is an empty command – also, <code>{}</code> in JSON case. Having application-level pings from the server also allows unifying the PING format for all unidirectional transports.</p>\n<p>Another improvement is that Centrifugo now randomizes the time it sends first ping to the client (but no longer than the configured ping interval). This allows to spread ping-pongs in time, providing a smoother CPU profile, especially after a massive reconnect scenario.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"secure-by-default-channel-namespaces\">Secure by default channel namespaces<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#secure-by-default-channel-namespaces\" class=\"hash-link\" aria-label=\"Direct link to Secure by default channel namespaces\" title=\"Direct link to Secure by default channel namespaces\" translate=\"no\">​</a></h2>\n<p>Data security and privacy are more important than ever in today's world. And as Centrifugo becomes more popular and widely used, the need to be <code>secure by default</code> only increases.</p>\n<p>Previously, by default, clients could subcribe to all channels in a namespace (except private channels, which are now revised – see details below). It was possible to use <code>\"protected\": true</code> option to make namespace protected, but we are not sure if everyone did that. This is extra configuration and additional knowledge on how Centrifugo works.</p>\n<p>Also, a common confusion we ran into: if server-side subscriptions were dictated by a connection JWT, many users would expect client-side subscriptions to those channels to not work. But without the <code>protected</code> option enabled, this was not the case.</p>\n<p>In Centrifugo v4, by default, it is not possible to subscribe to a channel in a namespace. The namespace must be configured to allow subscriptions from clients, or token authorization must be used. There are a bunch of new namespace options to tune the namespace behavior. Also the ability to provide a regular expression for channels in the namespace.</p>\n<p>The new permission-related channel option names better reflect the purpose of the option. For example, compare <code>\"publish\": true</code> and <code>\"allow_publish_for_client\": true</code>. The second one is more readable and provides a better understanding of the effect once turned on.</p>\n<p>Centrifugo is now more strict when checking channel name. Only ASCII symbols allowed – it was already mentioned in docs before, but wasn't actually enforced. Now we are fixing this.</p>\n<p>We understand that these changes will make running Centrifugo more of a challenge, especially when all you want is a public access to all the channels without worrying too much about permissions. It's still possible to achieve, but now the intent must be expicitly expressed in the config.</p>\n<p>Check out the updated documentation about <a class=\"\" href=\"https://centrifugal.dev/docs/server/channels\">channels and namespaces</a>. Our v4 migration guide contains an <strong>automatic converter</strong> for channel namespace options.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"private-channel-concept-revised\">Private channel concept revised<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#private-channel-concept-revised\" class=\"hash-link\" aria-label=\"Direct link to Private channel concept revised\" title=\"Direct link to Private channel concept revised\" translate=\"no\">​</a></h2>\n<p>A private channel is a special channel starting with <code>$</code> that could not be subscribed to without a subscription JWT. Prior to v4, having a known prefix allowed us to distinguish between public channels and private channels. But since namespaces are now non-public by default, this distinction is not really important.</p>\n<p>This means 2 things:</p>\n<ul>\n<li class=\"\">it's now possible to subscribe to any channel by having a valid subscription JWT (not just those that start with <code>$</code>)</li>\n<li class=\"\">channels beginning with <code>$</code> can only be subscribed with a subscription JWT, even if they belong to a namespace where subscriptions allowed for all clients. This is for security compatibility between v3 and v4.</li>\n</ul>\n<p>Another notable change in a subscription JWT – <code>client</code> claim is now DEPRECATED. There is no need to put it in the subscription token anymore. Centrifugo supports it only for backwards compatibility, but it will be completely removed in the future releases.</p>\n<p>The reason we're removing <code>client</code> claim is actually interesting. Due to the fact that <code>client</code> claim was a required part of the subscription JWT applications could run into a situation where during the <a class=\"\" href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#massive-reconnect\">massive reconnect scenario</a> (say, million connections reconnect) many requests for new subscription tokens can be generated because the subscription token must contain the client ID generated by Centrifugo for the new connection. That could make it unusually hard for the application backend to handle the load. With a connection JWT we had no such problem – as connections could simply reuse the previous token to reconnect to Centrifugo.</p>\n<p>Now the subscription token behaves just like the connection token, so we get a scalable solution for token-based subscriptions as well.</p>\n<p>What's more, this change paved the way for another big improvement...</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"optimistic-subscriptions\">Optimistic subscriptions<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#optimistic-subscriptions\" class=\"hash-link\" aria-label=\"Direct link to Optimistic subscriptions\" title=\"Direct link to Optimistic subscriptions\" translate=\"no\">​</a></h2>\n<p>The improvement we just mentioned is called optimistic subscriptions. If any of you are familiar with the <a href=\"https://en.wikipedia.org/wiki/QUIC\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">QUIC</a> protocol, then optimistic subscriptions are somewhat similar to the 0-RTT feature in QUIC. The idea is simple – we can include subscription commands to the first frame sent to the server.</p>\n<p>Previously, we sent subscriptions only after receiving a successful Connect Reply to a Connect Command from a server. But with the new changes in token behaviour, it seems so logical to put subscribe commands within the initial connect frame. Especially since Centrifugo protocol always supported batching of commands. Even token-based subscriptions can now be included into the initial frame during reconnect process, since the previous token can be reused now.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/optimistic_subs-8a424a694e135797511598c93f3fb23b.png\" width=\"3498\" height=\"1143\" class=\"img_ev3q\"></p>\n<p>The benefit is awesome – in most scenarios, we save one RTT of latency when connecting to Centrifugo and subscribing to channels (which is actually the most common way to use Centrifugo). While not visible on localhost, this is pretty important in real-life. And this is less syscalls for the server after all, resulting in less CPU usage.</p>\n<p>Optimistic subscriptions are also great for bidirectional emulation with HTTP, as they avoid the long path of proxying a request to the correct Centrifugo node when connecting.</p>\n<p>Optimistic subscriptions are now only part of <code>centrifuge-js</code>. At some point, we plan to roll out this important optimization to all other client SDKs.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"channel-capabilities\">Channel capabilities<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#channel-capabilities\" class=\"hash-link\" aria-label=\"Direct link to Channel capabilities\" title=\"Direct link to Channel capabilities\" translate=\"no\">​</a></h2>\n<p>The channel capabilities feature is introduced as part of <a class=\"\" href=\"https://centrifugal.dev/docs/pro/overview\">Centrifugo PRO</a>. Initially, we aimed to make it a part of the OSS version. But the lack of feedback on this feature made us nervous it's really needed. So adding it to PRO, where we still have room to evaluate the idea, seemed like the safer decision at the moment.</p>\n<p>Centrifugo allows configuring channel permissions on a per-namespace level. When creating a new real-time feature, it is recommended to create a new namespace for it and configure permissions. But to achieve a better channel permission control within a namespace the Channel capabilities can be used now.</p>\n<p>The channel capability feature provides a possibility to set capabilities on an individual connection basis, or an individual channel subscription basis.</p>\n<p>For example, in a connection JWT developers can set sth like:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"caps\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"channels\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"news\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"user_42\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"allow\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"sub\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>And this tells Centrifugo that the connection is able to subscribe on channels <code>news</code> or <code>user_42</code> using client-side subscriptionsat any time while the connection is active. Centrifugo also supports wildcard and regex channel matches.</p>\n<p>Subscription JWT can provide capabilities for the channel too, so permissions may be controlled on an individual subscription basis, ex. the ability to publish and call history API may be expressed with <code>allow</code> claim in subscription JWT:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"allow\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"pub\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"hst\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Read more about this mechanism in <a class=\"\" href=\"https://centrifugal.dev/docs/pro/capabilities\">Channel capabilities</a> chapter.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"better-connections-api\">Better connections API<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#better-connections-api\" class=\"hash-link\" aria-label=\"Direct link to Better connections API\" title=\"Direct link to Better connections API\" translate=\"no\">​</a></h2>\n<p>Another addition to Centrifugo PRO is the improved <a class=\"\" href=\"https://centrifugal.dev/docs/pro/connections\">connection API</a>. Previously, we could only return all connections from a specific user.</p>\n<p>The API now supports filtering all connections: by user ID, by subscribed channel, by additional meta information attached to the connection.</p>\n<p>The filtering works by user ID or with a help of <a href=\"https://opensource.google/projects/cel\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">CEL expressions</a> (Common Expression Language). CEL expressions provide a developer-friendly, fast and secure (as they are not Turing-complete) way to evaluate some conditions. They are used in some Google services (ex. Firebase), in Envoy RBAC configuration, etc. If you've never seen it before – take a look, cool project. We are also evaluating how to use CEL expressions for a dynamic and efficient channel permission checks, but that's an early story.</p>\n<p>The <code>connections</code> API call result contains more useful information: a list of client's active channels, information about the tokens used to connect and subscribe, meta information attached to the connection.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"javascript-client-moved-to-typescript\">Javascript client moved to TypeScript<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#javascript-client-moved-to-typescript\" class=\"hash-link\" aria-label=\"Direct link to Javascript client moved to TypeScript\" title=\"Direct link to Javascript client moved to TypeScript\" translate=\"no\">​</a></h2>\n<p>It's no secret that <code>centrifuge-js</code> is the most popular SDK in the Centrifugo ecosystem. We put additional love to it – and <code>centrifuge-js</code> is now fully written in Typescript ❤️</p>\n<p>This was a long awaited improvement, and it finally happened! The entire public API is strictly typed. The cool thing is that even <code>EventEmitter</code> events and event handlers are the subject to type checks - this should drastically simplify and speedup development and also help to reduce error possibility.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"experimenting-with-http3\">Experimenting with HTTP/3<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#experimenting-with-http3\" class=\"hash-link\" aria-label=\"Direct link to Experimenting with HTTP/3\" title=\"Direct link to Experimenting with HTTP/3\" translate=\"no\">​</a></h2>\n<p>Centrifugo v4 has an <strong>experimental</strong> <a href=\"https://en.wikipedia.org/wiki/HTTP/3\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">HTTP/3</a> support. Once TLS is enabled and <code>\"http3\": true</code> option is set all the endpoints on an external port will be served by a HTTP/3 server based on <a href=\"https://github.com/lucas-clemente/quic-go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">lucas-clemente/quic-go</a> implementation.</p>\n<p>It's worth noting that WebSocket will still use HTTP/1.1 for its Upgrade request (there is an interesting IETF draft BTW about <a href=\"https://www.ietf.org/archive/id/draft-ietf-httpbis-h3-websockets-02.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Bootstrapping WebSockets with HTTP/3</a>). But HTTP-streaming and EventSource should work just fine with HTTP/3.</p>\n<p>HTTP/3 does not currently work with our ACME autocert TLS - i.e. you need to explicitly provide paths to cert and key files <a class=\"\" href=\"https://centrifugal.dev/docs/server/tls#using-crt-and-key-files\">as described here</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"experimenting-with-webtransport\">Experimenting with WebTransport<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#experimenting-with-webtransport\" class=\"hash-link\" aria-label=\"Direct link to Experimenting with WebTransport\" title=\"Direct link to Experimenting with WebTransport\" translate=\"no\">​</a></h2>\n<p>Having HTTP/3 on board allowed us to make one more thing. Some of you may remember the post <a class=\"\" href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport\">Experimenting with QUIC and WebTransport</a> published in our blog before. We danced around the idea to add <a href=\"https://web.dev/webtransport/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WebTransport</a> to Centrifugo since then. <a href=\"https://datatracker.ietf.org/doc/draft-ietf-webtrans-http3/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WebTransport IETF specification</a> is still a draft, it changed a lot since our first blog post about it. But WebTransport object is already part of Chrome (since v97) and things seem to be very close to the release.</p>\n<p>So we added experimental WebTransport support to Centrifugo v4. This is made possible with the help of the <a href=\"https://github.com/marten-seemann/webtransport-go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">marten-seemann/webtransport-go</a> library.</p>\n<p>To use WebTransport you need to run HTTP/3 experimental server and enable WebTransport endpoint with <code>\"webtransport\": true</code> option in the configuration. Then you can connect to that endpoint using <code>centrifuge-js</code>. For example, let's enable WebTransport and use WebSocket as a fallback option:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> transports </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'webtransport'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'https://your_centrifugo.com/connection/webtransport'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">transport</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'websocket'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">endpoint</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'wss://your_centrifugo.com/connection/websocket'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">transports</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>Note, that we are using secure schemes here – <code>https://</code> and <code>wss://</code>. While in WebSocket case you could opt for non-TLS communication, in HTTP/3 and specifically WebTransport non-TLS communication is simply not supported by the specification.</p>\n<p>In Centrifugo case, we utilize the bidirectional reliable stream of WebTransport to pass our protocol between client and server. Both JSON and Protobuf communication formats are supported. There are some issues with the proper passing of the disconnect advice in some cases, otherwise it's fully functional.</p>\n<p>Obviously, due to the limited WebTransport support in browsers at the moment, possible breaking changes in the WebTransport specification we can not recommended it for production usage for now. At some point in the future, it may become a reasonable alternative to WebSocket, now we are more confident that Centrifugo will be able to provide a proper support of it.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"migration-guide\">Migration guide<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#migration-guide\" class=\"hash-link\" aria-label=\"Direct link to Migration guide\" title=\"Direct link to Migration guide\" translate=\"no\">​</a></h2>\n<p>The <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/migration_v4\">migration guide</a> contains steps to upgrade your Centrifugo from version 3 to version 4. While there are many changes in the v4 release, it should be possible to migrate to Centrifugo v4 without changing the code on the client side at all. And then, after updating the server, gradually update the client-side to the latest version of the stack.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://centrifugal.dev/assets/images/bg_cat-975c20ed8cf4a65568bc628c638050f9.jpg\" width=\"3780\" height=\"1988\" class=\"img_ev3q\"></p>\n<p>To sum it up, here are some benefits of Centrifugo v4:</p>\n<ul>\n<li class=\"\">unified experience thoughout application frontend environments</li>\n<li class=\"\">an optimized protocol which is generally faster, more compact and human-readable in JSON case, provides more resilient behavior for subscriptions</li>\n<li class=\"\">revised channel namespace security model, more granular permission control</li>\n<li class=\"\">more efficient and flexible use of subscription tokens</li>\n<li class=\"\">better initial latency – thanks to optimistic subscriptions and the ability to pre-create subscription tokens (as the <code>client</code> claim not needed anymore)</li>\n<li class=\"\">the ability to use more efficient WebSocket bidirectional emulation in the browser without having to worry about sticky sessions, unless you want to optimize the real-time infrastructure</li>\n</ul>\n<p>That's it. We now begin the era of v4 and it is going to be awesome, no doubt.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"join-community\">Join community<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#join-community\" class=\"hash-link\" aria-label=\"Direct link to Join community\" title=\"Direct link to Join community\" translate=\"no\">​</a></h2>\n<p>The release contains many changes that strongly affect developing with Centrifugo. And of course you may have some questions or issues regarding new or changed concepts. Join our communities in Telegram (the most active) and Discord:</p>\n<p><a href=\"https://t.me/joinchat/ABFVWBE0AhkyyhREoaboXQ\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><img decoding=\"async\" loading=\"lazy\" src=\"https://img.shields.io/badge/Telegram-Group-orange?style=flat&amp;logo=telegram\" alt=\"Join the chat at https://t.me/joinchat/ABFVWBE0AhkyyhREoaboXQ\" class=\"img_ev3q\"></a> &nbsp;<a href=\"https://discord.gg/tYgADKx\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><img decoding=\"async\" loading=\"lazy\" src=\"https://img.shields.io/discord/719186998686122046?style=flat&amp;label=Discord&amp;logo=discord\" alt=\"Join the chat at https://discord.gg/tYgADKx\" class=\"img_ev3q\"></a></p>\n<p>Enjoy Centrifugo v4, and let the Centrifugal force be with you.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"special-thanks\">Special thanks<a href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released#special-thanks\" class=\"hash-link\" aria-label=\"Direct link to Special thanks\" title=\"Direct link to Special thanks\" translate=\"no\">​</a></h2>\n<p>The refactoring of client SDKs and introducing unified behavior based on the common spec was the hardest part of Centrifugo v4 release. Many thanks to <a href=\"https://github.com/puzrin\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Vitaly Puzrin</a> (who is the author of several popular open-source libraries such as <a href=\"https://github.com/markdown-it/markdown-it\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">markdown-it</a>, <a href=\"https://github.com/fontello/fontello\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">fontello</a>, and others). We had a series of super productive sessions with him on client SDK API design. Some great ideas emerged from these sessions and the result seems like a huge step forward for Centrifugal projects.</p>\n<p>Also, thanks to <a href=\"https://github.com/silischev\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Anton Silischev</a> who helped a lot with WebTransport prototypes earlier this year, so we could quickly adopt WebTransport for v4.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>As some of you know, Centrifugo server is built on top of the <a href=\"https://github.com/centrifugal/centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifuge</a> library for Go. Most of the optimizations and improvements described here are now also part of Centrifuge library.</p><p>With its new unified SDK behavior and bidirectional emulation layer, it seems a solid alternative to Socket.IO in the Go language ecosystem.</p><p>In some cases, Centrifuge library can be a more flexible solution than Centrifugo, since Centrifugo (as a standalone server) dictates some mechanics and rules that must be followed. In the case of Centrifugo, the business logic must live on the application backend side, with Centrifuge library it can be kept closer to the real-time transport layer.</p></div></div>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Attributions</div><div class=\"admonitionContent_BuS1\"><p>This post used images from freepik.com: <a href=\"https://www.freepik.com/free-vector/abstract-background-consisting-colorful-arcs-illustration_14803794.htm#&amp;position=5&amp;from_view=author\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">background</a> by <a href=\"https://www.freepik.com/author/liuzishan\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">liuzishan</a>. Also <a href=\"https://www.freepik.com/free-vector/abstract-black-circles-layers-dark-background-paper-cut_17303270.htm\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">image</a> by <a href=\"https://www.freepik.com/author/kenshinstock\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">kenshinstock</a>.</p></div></div>",
            "url": "https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released",
            "title": "Centrifugo v4 released – a little revolution",
            "summary": "Centrifugo v4 provides an optimized client protocol, modern WebSocket emulation, improved channel security, redesigned client SDK behavior, experimental HTTP/3 and WebTransport support.",
            "date_modified": "2022-07-19T00:00:00.000Z",
            "author": {
                "name": "Centrifugal team"
            },
            "tags": [
                "centrifugo",
                "release"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial",
            "content_html": "<p>In this tutorial, we will create a multi-room chat server using <a href=\"https://laravel.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Laravel framework</a> and <a href=\"https://centrifugal.dev/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo</a> real-time messaging server.</p>\n<p>Authenticated users of our chat app will be able to create new chat rooms, join existing rooms and instantly communicate inside rooms with the help of Centrifugo WebSocket real-time transport.</p>\n<div class=\"theme-admonition theme-admonition-caution admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>caution</div><div class=\"admonitionContent_BuS1\"><p>This tutorial was written for Centrifugo v3. We recently released <a class=\"\" href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released\">Centrifugo v4</a> which makes some parts of this tutorial obsolete. The core concepts are similar though – so this can still be used as a Centrifugo learning step.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"application-overview\">Application overview<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#application-overview\" class=\"hash-link\" aria-label=\"Direct link to Application overview\" title=\"Direct link to Application overview\" translate=\"no\">​</a></h2>\n<p>The result will look like this:</p>\n<video width=\"100%\" controls=\"\"><source src=\"/img/laravel_chat_demo.mp4\" type=\"video/mp4\"><p>Sorry, your browser doesn't support embedded video.</p></video>\n<p>For the backend, we are using Laravel (version 8.65) as one of the most popular PHP frameworks. Centrifugo v3 will accept WebSocket client connections. And we will implement an integration layer between Laravel and Centrifugo.</p>\n<p>For CSS styles we are using recently released Bootstrap 5. Also, some vanilla JS instead of frameworks like React/Vue/whatever to make frontend Javascript code simple – so most developers out there could understand the mechanics.</p>\n<p>We are also using a bit old-fashioned server rendering here where server renders templates for different room routes (URLs) – i.e. our app is not a SPA app – mostly for the same reasons: to keep example short and let reader focus on Centrifugo and Laravel integration parts.</p>\n<p>To generate fake user avatars we are requesting images from <a href=\"https://robohash.org/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://robohash.org/</a> which can generate unique robot puctures based on some input string (username in our case). Robots like to chat with each other!</p>\n<img src=\"https://robohash.org/1.png\" width=\"30%\">\n<img src=\"https://robohash.org/2.png\" width=\"30%\">\n<img src=\"https://robohash.org/4.png\" width=\"30%\">\n<br>\n<br>\n<br>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>We also have some ideas on further possible app improvements at the end of this post.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"why-integrate-laravel-with-centrifugo\">Why integrate Laravel with Centrifugo?<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#why-integrate-laravel-with-centrifugo\" class=\"hash-link\" aria-label=\"Direct link to Why integrate Laravel with Centrifugo?\" title=\"Direct link to Why integrate Laravel with Centrifugo?\" translate=\"no\">​</a></h2>\n<p>Why would Laravel developers want to integrate a project with Centrifugo for real-time messaging functionality? That's a good question. There are several points which could be a good motivation:</p>\n<ul>\n<li class=\"\">Centrifugo is <a href=\"https://github.com/centrifugal/centrifugo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">open-source</a> and <strong>self-hosted</strong>. So you can run it on your own infrastructure. Popular Laravel real-time broadcasting intergrations (Pusher and Ably) are paid cloud solutions. At scale Centrifugo will cost you less than cloud solutions. Of course cloud solutions do not require additional server setup – but everything is a trade-off right? So you should decide for youself.</li>\n<li class=\"\">Centrifugo is fast and scales well. It has an optimized Redis Engine with client-side sharding and Redis Cluster support. Centrifugo can also scale with KeyDB, Nats, or Tarantool. So it's possible to handle millions of connections distributed over different Centrifugo nodes.</li>\n<li class=\"\">Centrifugo provides a variety of features out-of-the-box – some of them are unique, especially for self-hosted real-time servers that scale to many nodes (like fast message history cache, or maintaining single user connection, both client-side and server-side subscriptions, etc).</li>\n<li class=\"\">Centrifugo is lightweight, single binary server which works as a separate service – it can be a universal tool in the developer's pocket, can migrate with you from one project to another, no matter what programming language or framework is used for business logic.</li>\n</ul>\n<p>Hope this makes sense as a good motivation to give Centrifugo a try in your Laravel project. Let's get started!</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"setup-and-start-a-project\">Setup and start a project<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#setup-and-start-a-project\" class=\"hash-link\" aria-label=\"Direct link to Setup and start a project\" title=\"Direct link to Setup and start a project\" translate=\"no\">​</a></h2>\n<p>For the convenience of working with the example, we <a href=\"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/docker-compose.yml\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">wrapped the end result into docker compose</a>.</p>\n<p>To start the app clone <a href=\"https://github.com/centrifugal/examples\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">examples repo</a>, cd into <code>v3/php_laravel_chat_tutorial</code> directory and run:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker compose up</span><br></div></code></pre></div></div>\n<p>At the first launch, the necessary images will be downloaded (will take some time and network bytes). When the main service is started, you should see something like this in container logs:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">...</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app           | Database seeding completed successfully.</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app           | [10-Dec-2021 12:25:05] NOTICE: fpm is running, pid 112</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app           | [10-Dec-2021 12:25:05] NOTICE: ready to handle connections</span><br></div></code></pre></div></div>\n<p>Then go to <a href=\"http://localhost/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost/</a> – you should see:</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"Image\" src=\"https://centrifugal.dev/assets/images/laravel_main_page-c3e70ae9857eefd3ca4fcd9999a7962c.jpg\" width=\"884\" height=\"453\" class=\"img_ev3q\"></p>\n<p>Register (using some fake credentials) or sign up – and proceed to the chat rooms.</p>\n<p>Pay attention to the <a href=\"https://github.com/centrifugal/examples/tree/master/v3/php_laravel_chat_tutorial/docker/conf\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">configuration</a> of Centrifugo and Nginx. Also, on <a href=\"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/docker/entrypoints/app.sh\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">entrypoint</a> which does some things:</p>\n<ul>\n<li class=\"\">dependencies are installed via composer</li>\n<li class=\"\">copying settings from .env.example</li>\n<li class=\"\">db migrations are performed and the necessary npm packages are installed</li>\n<li class=\"\">php-fpm starts</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"application-structure\">Application structure<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#application-structure\" class=\"hash-link\" aria-label=\"Direct link to Application structure\" title=\"Direct link to Application structure\" translate=\"no\">​</a></h2>\n<p>We assume you already familar with Laravel concepts, so we will just point you to some core aspects of the Laravel application structure and will pay more attention to Centrifugo integration parts.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"environment-settings\">Environment settings<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#environment-settings\" class=\"hash-link\" aria-label=\"Direct link to Environment settings\" title=\"Direct link to Environment settings\" translate=\"no\">​</a></h3>\n<p>After the first launch of the application, all settings will be copied from the file <a href=\"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/.env.example\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>.env.example</code></a> to <code>.env</code>. Next, we will take a closer look at some settings.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"database-migrations-and-models\">Database migrations and models<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#database-migrations-and-models\" class=\"hash-link\" aria-label=\"Direct link to Database migrations and models\" title=\"Direct link to Database migrations and models\" translate=\"no\">​</a></h3>\n<p>You can view the database structure <a href=\"https://github.com/centrifugal/examples/tree/master/v3/php_laravel_chat_tutorial/app/database/migrations\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">here</a>.</p>\n<p>We will use the following tables which will be then translated to the application models:</p>\n<ul>\n<li class=\"\">Laravel standard user authentication tables. See <a href=\"https://laravel.com/docs/8.x/authentication\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://laravel.com/docs/8.x/authentication</a>. In the service we are using Laravel Breeze. For more information <a href=\"https://laravel.com/docs/8.x/starter-kits#laravel-breeze\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">see official docs</a>.</li>\n<li class=\"\"><a href=\"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/database/migrations/2021_11_21_000001_create_rooms_table.php\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">rooms</a> table. Basically - describes different rooms in the app every user can create.</li>\n<li class=\"\">rooms <a href=\"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/database/migrations/2021_11_21_000002_create_users_rooms_table.php\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">many-to-many relation</a> to users. Allows to add users into rooms when <code>join</code> button clicked or automatically upon room creation.</li>\n<li class=\"\"><a href=\"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/database/migrations/2021_11_21_000003_create_messages_table.php\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">messages</a>. Keeps message history in rooms.</li>\n</ul>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"broadcasting\">Broadcasting<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#broadcasting\" class=\"hash-link\" aria-label=\"Direct link to Broadcasting\" title=\"Direct link to Broadcasting\" translate=\"no\">​</a></h3>\n<p>For broadcasting we are using <a href=\"https://github.com/denis660/laravel-centrifugo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">laravel-centrifugo</a> library. It helps to simplify interaction between Laravel and Centrifugo by providing some convenient wrappers.</p>\n<p>Step-by-step configuration can be viewed in the <a href=\"https://github.com/denis660/laravel-centrifugo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">readme</a> file of this library.</p>\n<p>Pay attention to the <code>CENTRIFUGO_API_KEY</code> setting. It is used to send API requests from Laravel to Centrifugo and must match in <code>.env</code> and <code>centrifugo.json</code> files. And we also telling <code>laravel-centrifugo</code> the URL of Centrifugo. That's all we need to configure for this example app.</p>\n<p>See more information about Laravel broadcasting <a href=\"https://laravel.com/docs/8.x/broadcasting\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">here</a>.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>As an alternative to <code>laravel-centrifugo</code>, you can use <a href=\"https://github.com/centrifugal/phpcent\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">phpcent</a> – it's an official generic API client which allows publishing to Centrifugo HTTP API. But it does know nothing about Laravel broadcasting specifics.</p></div></div>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"interaction-with-centrifugo\">Interaction with Centrifugo<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#interaction-with-centrifugo\" class=\"hash-link\" aria-label=\"Direct link to Interaction with Centrifugo\" title=\"Direct link to Interaction with Centrifugo\" translate=\"no\">​</a></h3>\n<p>When user opens a chat app it connects to Centrifugo over WebSocket transport.</p>\n<p>Let's take a closer look at Centrifugo server configuration file we use for this example app:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"port\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8000</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"engine\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"memory\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"api_key\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"some-long-api-key-which-you-should-keep-secret\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"allowed_origins\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"proxy_connect_endpoint\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://nginx/centrifugo/connect/\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"proxy_http_headers\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Cookie\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"namespaces\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"personal\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>This configuration defines a connect proxy endpoint which is targeting Nginx and then proxied to Laravel. Centrifugo will proxy <code>Cookie</code> header of WebSocket HTTP Upgrade requests to Laravel – this allows using native Laravel authentication.</p>\n<p>We also defined a <code>\"personal\"</code> namespace – we will subscribe each user to a personal channel in this namespace inside connect proxy handler. Using namespaces for different real-time features is one of Centrifugo best-practices.</p>\n<p>Allowed origins must be properly set to prevent <a href=\"https://christian-schneider.net/CrossSiteWebSocketHijacking.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">cross-site WebSocket connection hijacking</a>.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"connect-proxy-controller\">Connect proxy controller<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#connect-proxy-controller\" class=\"hash-link\" aria-label=\"Direct link to Connect proxy controller\" title=\"Direct link to Connect proxy controller\" translate=\"no\">​</a></h3>\n<p>To use native Laravel user authentication middlewares, we will use <a href=\"https://centrifugal.dev/docs/server/proxy\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo proxy feature</a>.</p>\n<p>When user connects to Centrifugo it's connection attempt will be transformed into HTTP request from Centrifugo to Laravel and will hit the <a href=\"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/app/Http/Controllers/CentrifugoProxyController.php\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">connect proxy controller</a>:</p>\n<div class=\"language-php codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-php codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">class</span><span class=\"token plain\"> </span><span class=\"token class-name-definition class-name\" style=\"color:rgb(255, 203, 107)\">CentrifugoProxyController</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">extends</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Controller</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">public</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function-definition function\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">JsonResponse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token string single-quoted-string\" style=\"color:rgb(195, 232, 141)\">'result'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token string single-quoted-string\" style=\"color:rgb(195, 232, 141)\">'user'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword type-casting\" style=\"font-style:italic\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token class-name static-context\" style=\"color:rgb(255, 203, 107)\">Auth</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">::</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">user</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token property\">id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token string single-quoted-string\" style=\"color:rgb(195, 232, 141)\">'channels'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string double-quoted-string\" style=\"color:rgb(195, 232, 141)\">\"personal:#\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">.</span><span class=\"token class-name static-context\" style=\"color:rgb(255, 203, 107)\">Auth</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">::</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">user</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token property\">id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>This controller <a href=\"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/routes/api.php\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">protected by auth middleware</a>.</p>\n<p>Since Centrifugo proxies <code>Cookie</code> header of initial WebSocket HTTP Upgrade request Laravel auth layer will work just fine. So in a controller you already has access to the current authenticated user.</p>\n<p>In the response from controller we tell Centrifugo the ID of connecting user and subscribe user to its personal channel (using <a href=\"https://centrifugal.dev/docs/server/channels#user-channel-boundary-\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">user-limited channel</a> feature of Centrifugo). Returning a channel in such way will subscribe user to it using <a href=\"https://centrifugal.dev/docs/server/server_subs\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">server-side subscriptions</a> mechanism.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>Note, that in our chat app we are using a single personal channel for each user to receive real-time updates from all rooms. We are not creating separate subscriptions for each room user joined too. This will allow us to scale more easily in the future, and basically the only viable solution in case of room list pagination in chat application like this. It does not mean you can not combine personal user channels and separate room channels for different tasks though.</p><p>Some additional tips can be found in <a href=\"https://centrifugal.dev/docs/faq/index#what-about-best-practices-with-the-number-of-channels\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo FAQ</a>.</p></div></div>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"room-controller\">Room controller<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#room-controller\" class=\"hash-link\" aria-label=\"Direct link to Room controller\" title=\"Direct link to Room controller\" translate=\"no\">​</a></h3>\n<p>In <a href=\"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/app/Http/Controllers/RoomController.php\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RoomController</a> we perform various actions with rooms:</p>\n<ul>\n<li class=\"\">displaying rooms</li>\n<li class=\"\">create rooms</li>\n<li class=\"\">join users to rooms</li>\n<li class=\"\">publish messages</li>\n</ul>\n<p>When we publish a message in a room, we send a message to the personal channel of all users joined to the room using the <a href=\"https://centrifugal.dev/docs/server/server_api#broadcast\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><code>broadcast</code> method of Centrifugo API</a>. It allows publishing the same message into many channels.</p>\n<div class=\"language-php codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-php codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$message</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token class-name static-context\" style=\"color:rgb(255, 203, 107)\">Message</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">::</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">create</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string single-quoted-string\" style=\"color:rgb(195, 232, 141)\">'sender_id'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token class-name static-context\" style=\"color:rgb(255, 203, 107)\">Auth</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">::</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">user</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token property\">id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string single-quoted-string\" style=\"color:rgb(195, 232, 141)\">'message'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$requestData</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string double-quoted-string\" style=\"color:rgb(195, 232, 141)\">\"message\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string single-quoted-string\" style=\"color:rgb(195, 232, 141)\">'room_id'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$room</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token class-name static-context\" style=\"color:rgb(255, 203, 107)\">Room</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">::</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">with</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string single-quoted-string\" style=\"color:rgb(195, 232, 141)\">'users'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">find</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$channels</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">foreach</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$room</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token property\">users</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">as</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$user</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$channels</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string double-quoted-string\" style=\"color:rgb(195, 232, 141)\">\"personal:#\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">.</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$user</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token property\">id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$this</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token property\">centrifugo</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">broadcast</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$channels</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string double-quoted-string\" style=\"color:rgb(195, 232, 141)\">\"text\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$message</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token property\">message</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string double-quoted-string\" style=\"color:rgb(195, 232, 141)\">\"createdAt\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$message</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token property\">created_at</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">toDateTimeString</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string double-quoted-string\" style=\"color:rgb(195, 232, 141)\">\"roomId\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token variable\" style=\"color:rgb(191, 199, 213)\">$id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string double-quoted-string\" style=\"color:rgb(195, 232, 141)\">\"senderId\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token class-name static-context\" style=\"color:rgb(255, 203, 107)\">Auth</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">::</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">user</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token property\">id</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string double-quoted-string\" style=\"color:rgb(195, 232, 141)\">\"senderName\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token class-name static-context\" style=\"color:rgb(255, 203, 107)\">Auth</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">::</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">user</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">-&gt;</span><span class=\"token property\">name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>We also add some fields to the published message which will be used when dynamically displaying a message coming from a WebSocket connection (see <a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#client-side\" class=\"\">Client side</a> below).</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"client-side\">Client side<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#client-side\" class=\"hash-link\" aria-label=\"Direct link to Client side\" title=\"Direct link to Client side\" translate=\"no\">​</a></h3>\n<p>Our chat is basically a one page with some variations dependng on the current route. So we use <a href=\"https://github.com/centrifugal/examples/blob/master/v3/php_laravel_chat_tutorial/app/resources/views/rooms/index.blade.php\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">a single view</a> for the entire chat app.</p>\n<p>On the page we have a form for creating rooms. The user who created the room automatically joins it upon creation. Other users need to join manually (using <code>join</code> button in the room).</p>\n<p>When sending a message (using the chat room message input), we make an AJAX request that hits <code>RoomController</code> shown above. A message saved into the database and then broadcasted to all users who joined this room. Here is a code that processes sending on ENTER:</p>\n<div class=\"language-js codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-js codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">messageInput</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method-variable function-variable method function property-access\" style=\"color:rgb(130, 170, 255)\">onkeyup</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">keyCode</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">===</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">13</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">preventDefault</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> message </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> messageInput</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">value</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token plain\">message</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> xhttp </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">XMLHttpRequest</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        xhttp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">open</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"POST\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"/rooms/\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> roomId </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"/publish\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        xhttp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">setRequestHeader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"X-CSRF-TOKEN\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> csrfToken</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        xhttp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">send</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token known-class-name class-name\" style=\"color:rgb(255, 203, 107)\">JSON</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">stringify</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token literal-property property\">message</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> message</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        messageInput</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">value</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">''</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>After the message is processed on the server and broadcasted to Centrifugo it instantly comes to client-side. To receive the message we are connecting to Centrifugo WebSocket endpoint and wait for a message in the <code>publish</code> event handler:</p>\n<div class=\"language-js codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-js codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> url </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"ws://\"</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">window</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">location</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">host</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"/connection/websocket\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">url</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'connect'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"connected to Centrifugo\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'disconnect'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"disconnected from Centrifugo\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publish'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">roomId</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">toString</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">===</span><span class=\"token plain\"> currentRoomId</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">addMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">scrollToLastMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">addRoomLastMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>We are using <a href=\"https://github.com/centrifugal/centrifuge-js\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifuge-js</a> client connector library to communicate with Centrifugo. This client abstracts away bidirectional asynchronous protocol complexity for us providing a simple way to listen connect, disconnect events and communicate with a server in various ways.</p>\n<p>In publish event handler we check whether the message belongs to the room the user is currently in. If yes, then we add it to the message history of the room. We also add this message to the room in the list on the left as the last chat message in room. If necessary, we crop the text for normal display.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>In our example we only subscribe each user to a single channel, but user can be subscribed to several server-side channels. To distinguish between them use <code>ctx.channel</code> inside publish event handler.</p></div></div>\n<p>And that's it! We went through all the main parts of the integration.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"possible-improvements\">Possible improvements<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#possible-improvements\" class=\"hash-link\" aria-label=\"Direct link to Possible improvements\" title=\"Direct link to Possible improvements\" translate=\"no\">​</a></h2>\n<p>As promised, here is a list with several possible app improvements:</p>\n<ul>\n<li class=\"\">Transform to a single page app, use productive Javascript frameworks like React or VueJS instead of vanilla JS.</li>\n<li class=\"\">Add message read statuses - as soon as one of the chat participants read the message mark it read in the database.</li>\n<li class=\"\">Introduce user-to-user chats.</li>\n<li class=\"\">Support pagination for the message history, maybe for chat room list also.</li>\n<li class=\"\">Don't show all rooms in the system – add functionality to search room by name.</li>\n<li class=\"\">Horizontal scaling (using multiple nodes of Centrifugo, for example with <a href=\"https://centrifugal.dev/docs/server/engines#redis-engine\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Redis Engine</a>) – mostly one line in Centrifugo config if you have Redis running.</li>\n<li class=\"\">Gracefully handle temporary disconnects by loading missed messages from the database or Centrifugo channel history cache.</li>\n<li class=\"\">Optionally replace connect proxy with <a href=\"https://centrifugal.dev/docs/server/authentication\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">JWT authentication</a> to reduce HTTP calls from Centrifugo to Laravel. This may drastically reduce resources for Laravel backend at scale.</li>\n<li class=\"\">Try using <a href=\"https://centrifugal.dev/docs/server/proxy#rpc-proxy\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo RPC proxy</a> feature to use WebSocket connection for message publish instead of issuing AJAX request.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>We built a chat app with Laravel and Centrifugo. While there is still an area for improvements, this example is not really the basic. It's already valuable in the current form and may be transformed into part of your production system with minimal tweaks.</p>\n<p>Hope you enjoyed this tutorial. If you have any questions after reading – join our <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/introduction#join-community\">community channels</a>. We touched only part of Centrifugo concepts here – take a look at detailed Centrifugo docs nearby. And let the Centrifugal force be with you!</p>",
            "url": "https://centrifugal.dev/blog/2021/12/14/laravel-multi-room-chat-tutorial",
            "title": "Building a multi-room chat application with Laravel and Centrifugo",
            "summary": "In this tutorial, we are integrating Laravel framework with Centrifugo real-time messaging server to make a multi-room chat application.",
            "date_modified": "2021-12-14T00:00:00.000Z",
            "author": {
                "name": "Anton Silischev"
            },
            "tags": [
                "centrifugo",
                "tutorial",
                "laravel",
                "php"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application",
            "content_html": "<p>In this tutorial, we will create a basic chat server using the <a href=\"https://www.djangoproject.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Django framework</a> and <a href=\"https://centrifugal.dev/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo</a>. Our chat application will have two pages:</p>\n<ol>\n<li class=\"\">A page that lets you type the name of a chat room to join.</li>\n<li class=\"\">A room view that lets you see messages posted in a chat room you joined.</li>\n</ol>\n<p>The room view will use a WebSocket to communicate with the Django server (with help from Centrifugo) and listen for any messages that are published to the room channel.</p>\n<div class=\"theme-admonition theme-admonition-caution admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>caution</div><div class=\"admonitionContent_BuS1\"><p>This tutorial was written for Centrifugo v3. We recently released <a class=\"\" href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released\">Centrifugo v4</a> which makes some parts of this tutorial obsolete. The core concepts are similar though – so this can still be used as a Centrifugo learning step.</p></div></div>\n<p>The result will look like this:</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"demo\" src=\"https://centrifugal.dev/assets/images/django_chat-9972126f452363132e76a62f00213fbf.gif\" width=\"1280\" height=\"844\" class=\"img_ev3q\"></p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>Some of you will notice that this tutorial looks very similar to <a href=\"https://channels.readthedocs.io/en/stable/tutorial/index.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Chat app tutorial of Django Channels</a>. This is intentional to let Pythonistas already familiar with Django Channels feel how Centrifugo compares to Channels in terms of the integration process.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"why-integrate-django-with-centrifugo\">Why integrate Django with Centrifugo<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#why-integrate-django-with-centrifugo\" class=\"hash-link\" aria-label=\"Direct link to Why integrate Django with Centrifugo\" title=\"Direct link to Why integrate Django with Centrifugo\" translate=\"no\">​</a></h2>\n<p>Why would Django developers want to integrate a project with Centrifugo for real-time messaging functionality? This is a good question especially since there is a popular Django Channels project which solves the same task.</p>\n<p>I found several points which could be a good motivation:</p>\n<ul>\n<li class=\"\">Centrifugo is fast and scales well. We have an optimized Redis Engine with client-side sharding and Redis Cluster support. Centrifugo can also scale with KeyDB, Nats, or Tarantool. So it's possible to handle millions of connections distributed over different server nodes.</li>\n<li class=\"\">Centrifugo provides a variety of features out-of-the-box – some of them are unique, especially for real-time servers that scale to many nodes. Check out our doc!</li>\n<li class=\"\">With Centrifugo you don't need to rewrite the existing application to introduce real-time messaging features to your users.</li>\n<li class=\"\">Centrifugo works as a separate service – so can be a universal tool in the developer's pocket, can migrate from one project to another, no matter what programming language or framework is used for business logic.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"prerequisites\">Prerequisites<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#prerequisites\" class=\"hash-link\" aria-label=\"Direct link to Prerequisites\" title=\"Direct link to Prerequisites\" translate=\"no\">​</a></h2>\n<p>We assume that you are already familiar with basic Django concepts. If not take a look at the official <a href=\"https://docs.djangoproject.com/en/stable/intro/tutorial01/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Django tutorial</a> first and then come back to this tutorial.</p>\n<p>Also, make sure you read a bit about Centrifugo – <a href=\"https://centrifugal.dev/docs/getting-started/introduction\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">introduction</a> and <a href=\"https://centrifugal.dev/docs/getting-started/quickstart\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">quickstart tutorial</a>.</p>\n<p>We also assume that you have <a href=\"https://docs.djangoproject.com/en/stable/intro/install/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Django installed</a> already.</p>\n<p>One possible way to quickly install Django locally is to create virtualenv, activate it, and install Django:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">python3 -m venv env</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">. env/bin/activate</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">pip install django</span><br></div></code></pre></div></div>\n<p>Alos, make sure you have Centrifugo v3 <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/installation\">installed</a> already.</p>\n<p>This tutorial also uses Docker to run Redis. We use Redis as a Centrifugo engine – this allows us to have a scalable solution in the end. Using Redis is optional actually, Centrifugo uses a Memory engine by default (but it does not allow scaling Centrifugo nodes). We will also run Nginx with Docker to serve the entire app. <a href=\"https://www.docker.com/get-started\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Install Docker</a> from its official website but I am sure you already have one.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"creating-a-project\">Creating a project<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#creating-a-project\" class=\"hash-link\" aria-label=\"Direct link to Creating a project\" title=\"Direct link to Creating a project\" translate=\"no\">​</a></h2>\n<p>First, let's create a Django project.</p>\n<p>From the command line, <code>cd</code> into a directory where you’d like to store your code, then run the following command:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">django-admin startproject mysite</span><br></div></code></pre></div></div>\n<p>This will create a mysite directory in your current directory with the following contents:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">❯ tree mysite</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">mysite</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── manage.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">└── mysite</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    ├── __init__.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    ├── asgi.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    ├── settings.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    ├── urls.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    └── wsgi.py</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"creating-the-chat-app\">Creating the chat app<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#creating-the-chat-app\" class=\"hash-link\" aria-label=\"Direct link to Creating the chat app\" title=\"Direct link to Creating the chat app\" translate=\"no\">​</a></h2>\n<p>We will put the code for the chat server inside <code>chat</code> app.</p>\n<p>Make sure you’re in the same directory as <code>manage.py</code> and type this command:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">python3 manage.py startapp chat</span><br></div></code></pre></div></div>\n<p>That’ll create a directory chat, which is laid out like this:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">❯ tree chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── __init__.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── admin.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── apps.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── migrations</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   └── __init__.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── models.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── tests.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">└── views.py</span><br></div></code></pre></div></div>\n<p>For this tutorial, we will only be working with <code>chat/views.py</code> and <code>chat/__init__.py</code>. Feel free to remove all other files from the chat directory.</p>\n<p>After removing unnecessary files, the chat directory should look like this:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">❯ tree chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── __init__.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">└── views.py</span><br></div></code></pre></div></div>\n<p>We need to tell our project that the chat app is installed. Edit the <code>mysite/settings.py</code> file and add 'chat' to the <code>INSTALLED_APPS</code> setting. It’ll look like this:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># mysite/settings.py</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">INSTALLED_APPS </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'chat'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'django.contrib.admin'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'django.contrib.auth'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'django.contrib.contenttypes'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'django.contrib.sessions'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'django.contrib.messages'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'django.contrib.staticfiles'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"add-the-index-view\">Add the index view<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#add-the-index-view\" class=\"hash-link\" aria-label=\"Direct link to Add the index view\" title=\"Direct link to Add the index view\" translate=\"no\">​</a></h2>\n<p>We will now create the first view, an index view that lets you type the name of a chat room to join.</p>\n<p>Create a templates directory in your chat directory. Within the templates directory, you have just created, create another directory called <code>chat</code>, and within that create a file called <code>index.html</code> to hold the template for the index view.</p>\n<p>Your chat directory should now look like this:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">❯ tree chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── __init__.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── templates</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   └── chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│       └── index.html</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">└── views.py</span><br></div></code></pre></div></div>\n<p>Put the following code in chat/templates/chat/index.html:</p>\n<div class=\"language-html codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">chat/templates/chat/index.html</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-html codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token doctype punctuation\" style=\"color:rgb(199, 146, 234);font-style:italic\">&lt;!</span><span class=\"token doctype doctype-tag\" style=\"color:rgb(199, 146, 234);font-style:italic\">DOCTYPE</span><span class=\"token doctype\" style=\"color:rgb(199, 146, 234);font-style:italic\"> </span><span class=\"token doctype name\" style=\"color:rgb(199, 146, 234);font-style:italic\">html</span><span class=\"token doctype punctuation\" style=\"color:rgb(199, 146, 234);font-style:italic\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">head</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">meta</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">charset</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">utf-8</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">/&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">title</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\">Select a chat room</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">title</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">head</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">center</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">input-wrapper</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">input</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">type</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">text</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">room-name-input</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">/&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">input-help</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            Type a room name to </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">a</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">room-name-submit</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">href</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">#</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\">JOIN</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">a</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> nameInput </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">querySelector</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'#room-name-input'</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> nameSubmit </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">querySelector</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'#room-name-submit'</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        nameInput</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">focus</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        nameInput</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method-variable function-variable method function property-access\" style=\"color:rgb(130, 170, 255)\">onkeyup</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">function</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript parameter\">e</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">e</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">keyCode</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">===</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript number\" style=\"color:rgb(247, 140, 108)\">13</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\">  </span><span class=\"token script language-javascript comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// enter, return</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                nameSubmit</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">click</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        nameSubmit</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method-variable function-variable method function property-access\" style=\"color:rgb(130, 170, 255)\">onclick</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">function</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript parameter\">e</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            e</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">preventDefault</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">var</span><span class=\"token script language-javascript\"> roomName </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> nameInput</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">value</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token script language-javascript\">roomName</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">window</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">location</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">pathname</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'/chat/room/'</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token script language-javascript\"> roomName </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'/'</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><br></div></code></pre></div></div>\n<p>Create the view function for the room view. Put the following code in <code>chat/views.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">chat/views.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> django</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">shortcuts </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> render</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> render</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'chat/index.html'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>To call the view, we need to map it to a URL - and for this, we need a URLconf.</p>\n<p>To create a URLconf in the chat directory, create a file called <code>urls.py</code>. Your app directory should now look like this:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">❯ tree chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── __init__.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── templates</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   └── chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│       └── index.html</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">└── views.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">└── urls.py</span><br></div></code></pre></div></div>\n<p>In the <code>chat/urls.py</code> file include the following code:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">chat/urls.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> django</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">urls </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> path</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> views</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">urlpatterns </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">''</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> views</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> name</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'index'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><br></div></code></pre></div></div>\n<p>The next step is to point the root URLconf at the <code>chat.urls</code> module. In <code>mysite/urls.py</code>, add an import for <code>django.conf.urls.include</code> and insert an include() in the urlpatterns list, so you have:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">mysite/urls.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> django</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">conf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">urls </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> include</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> django</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">urls </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> path</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> django</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">contrib </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> admin</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">urlpatterns </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'chat/'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> include</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'chat.urls'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'admin/'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> admin</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">site</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">urls</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><br></div></code></pre></div></div>\n<p>Let’s verify that the index view works. Run the following command:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">python3 manage.py runserver</span><br></div></code></pre></div></div>\n<p>You’ll see the following output on the command line:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Watching for file changes with StatReloader</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Performing system checks...</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">System check identified no issues (0 silenced).</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Run 'python manage.py migrate' to apply them.</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">October 21, 2020 - 18:49:39</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Django version 3.1.2, using settings 'mysite.settings'</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Starting development server at http://localhost:8000/</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Quit the server with CONTROL-C.</span><br></div></code></pre></div></div>\n<p>Go to <a href=\"http://localhost:8000/chat/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:8000/chat/</a> in your browser and you should see the a text input to provide a room name.</p>\n<p>Type in \"lobby\" as the room name and press Enter. You should be redirected to the room view at <a href=\"http://localhost:8000/chat/room/lobby/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:8000/chat/room/lobby/</a> but we haven’t written the room view yet, so you’ll get a \"Page not found\" error page.</p>\n<p>Go to the terminal where you ran the runserver command and press Control-C to stop the server.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"add-the-room-view\">Add the room view<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#add-the-room-view\" class=\"hash-link\" aria-label=\"Direct link to Add the room view\" title=\"Direct link to Add the room view\" translate=\"no\">​</a></h2>\n<p>We will now create the second view, a room view that lets you see messages posted in a particular chat room.</p>\n<p>Create a new file <code>chat/templates/chat/room.html</code>. Your app directory should now look like this:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── __init__.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── templates</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│   └── chat</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│       ├── index.html</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">│       └── room.html</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">├── urls.py</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">└── views.py</span><br></div></code></pre></div></div>\n<p>Create the view template for the room view in <code>chat/templates/chat/room.html</code>:</p>\n<div class=\"language-html codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">chat/templates/chat/room.html</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-html codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token doctype punctuation\" style=\"color:rgb(199, 146, 234);font-style:italic\">&lt;!</span><span class=\"token doctype doctype-tag\" style=\"color:rgb(199, 146, 234);font-style:italic\">DOCTYPE</span><span class=\"token doctype\" style=\"color:rgb(199, 146, 234);font-style:italic\"> </span><span class=\"token doctype name\" style=\"color:rgb(199, 146, 234);font-style:italic\">html</span><span class=\"token doctype punctuation\" style=\"color:rgb(199, 146, 234);font-style:italic\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">head</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">meta</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">charset</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">utf-8</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">/&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">title</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\">Chat Room</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">title</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">src</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">https://cdn.jsdelivr.net/gh/centrifugal/centrifuge-js@2.8.3/dist/centrifuge.min.js</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token script\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">head</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">ul</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">chat-thread</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">chat-thread</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">ul</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">chat-message</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">input</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">chat-message-input</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">chat-message-input</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">type</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">text</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">autocomplete</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">off</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">autofocus</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">/&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    {{ room_name|json_script:\"room-name\" }}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> roomName </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript known-class-name class-name\" style=\"color:rgb(255, 203, 107)\">JSON</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">parse</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'room-name'</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">textContent</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> chatThread </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">querySelector</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'#chat-thread'</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> messageInput </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">querySelector</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'#chat-message-input'</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> centrifuge </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">new</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"ws://\"</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">window</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">location</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">host</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"/connection/websocket\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        centrifuge</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'connect'</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">function</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript parameter\">ctx</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"connected\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> ctx</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        centrifuge</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'disconnect'</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">function</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript parameter\">ctx</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">\"disconnected\"</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> ctx</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> sub </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> centrifuge</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'rooms:'</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token script language-javascript\"> roomName</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">function</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript parameter\">ctx</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> chatNewThread </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">createElement</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">'li'</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> chatNewMessage </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">createTextNode</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">ctx</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">data</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">message</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            chatNewThread</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">appendChild</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">chatNewMessage</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            chatThread</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">appendChild</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">chatNewThread</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            chatThread</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">scrollTop</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> chatThread</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">scrollHeight</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        centrifuge</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        messageInput</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">focus</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        messageInput</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method-variable function-variable method function property-access\" style=\"color:rgb(130, 170, 255)\">onkeyup</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">function</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript parameter\">e</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript\">e</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">keyCode</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">===</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript number\" style=\"color:rgb(247, 140, 108)\">13</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\">  </span><span class=\"token script language-javascript comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// enter, return</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                e</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">preventDefault</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                </span><span class=\"token script language-javascript keyword\" style=\"font-style:italic\">const</span><span class=\"token script language-javascript\"> message </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> messageInput</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">value</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token script language-javascript\">message</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                    </span><span class=\"token script language-javascript keyword control-flow\" style=\"font-style:italic\">return</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                sub</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript method function property-access\" style=\"color:rgb(130, 170, 255)\">publish</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string-property property\">'message'</span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token script language-javascript\"> message </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">                messageInput</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token script language-javascript property-access\">value</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token script language-javascript\"> </span><span class=\"token script language-javascript string\" style=\"color:rgb(195, 232, 141)\">''</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">            </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">        </span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token script language-javascript punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token script language-javascript\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token script language-javascript\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><br></div></code></pre></div></div>\n<p>Create the view function for the room view in <code>chat/views.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">chat/views.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> django</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">shortcuts </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> render</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> render</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'chat/index.html'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">room</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> room_name</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> render</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'chat/room.html'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'room_name'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> room_name</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>Create the route for the room view in <code>chat/urls.py</code>:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># chat/urls.py</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> django</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">urls </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> re_path</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> views</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">urlpatterns </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">''</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> views</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> name</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'index'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    re_path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'room/(?P&lt;room_name&gt;[A-z0-9_-]+)/'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> views</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">room</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> name</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'room'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><br></div></code></pre></div></div>\n<p>Start the development server:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">python3 manage.py runserver</span><br></div></code></pre></div></div>\n<p>Go to <a href=\"http://localhost:8000/chat/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:8000/chat/</a> in your browser and to see the index page.</p>\n<p>Type in \"lobby\" as the room name and press enter. You should be redirected to the room page at <a href=\"http://localhost:8000/chat/lobby/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:8000/chat/lobby/</a> which now displays an empty chat log.</p>\n<p>Type the message \"hello\" and press Enter. Nothing happens! In particular, the message does not appear in the chat log. Why?</p>\n<p>The room view is trying to open a WebSocket connection with Centrifugo using the URL <code>ws://localhost:8000/connection/websocket</code> but we haven’t started Centrifugo to accept WebSocket connections yet. If you open your browser’s JavaScript console, you should see an error that looks like this:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">WebSocket connection to 'ws://localhost:8000/connection/websocket' failed</span><br></div></code></pre></div></div>\n<p>And since port 8000 has already been allocated we will start Centrifugo at a different port actually.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"starting-centrifugo-server\">Starting Centrifugo server<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#starting-centrifugo-server\" class=\"hash-link\" aria-label=\"Direct link to Starting Centrifugo server\" title=\"Direct link to Starting Centrifugo server\" translate=\"no\">​</a></h2>\n<p>As promised we will use Centrifugo with Redis engine. So first thing to do before running Centrifugo is to start Redis:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker run -it --rm -p 6379:6379 redis:6</span><br></div></code></pre></div></div>\n<p>Then create a configuration file for Centrifugo:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"port\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">8001</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"engine\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redis\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"redis_address\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"redis://localhost:6379\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"allowed_origins\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:9000\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"proxy_connect_endpoint\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:8000/chat/centrifugo/connect/\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"proxy_publish_endpoint\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:8000/chat/centrifugo/publish/\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"proxy_subscribe_endpoint\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:8000/chat/centrifugo/subscribe/\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"proxy_http_headers\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Cookie\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"namespaces\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"name\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"rooms\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"publish\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"proxy_publish\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token property\">\"proxy_subscribe\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>And run Centrifugo with it like this:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifugo -c config.json</span><br></div></code></pre></div></div>\n<p>Let's describe some options we used here:</p>\n<ul>\n<li class=\"\"><code>port</code> - sets the port Centrifugo runs on since we are running everything on localhost we make it different (8001) from the port allocated for the Django server (8000).</li>\n<li class=\"\"><code>engine</code> - as promised we are using Redis engine so we can easily scale Centrifigo nodes to handle lots of WebSocket connections</li>\n<li class=\"\"><code>redis_address</code> allows setting Redis address</li>\n<li class=\"\"><code>allowed_origins</code> - we will connect from <code>http://localhost:9000</code> so we need to allow it</li>\n<li class=\"\"><code>namespaces</code> – we are using <code>rooms:</code> prefix when subscribing to a channel, i.e. using Centrifugo <code>rooms</code> namespace. Here we define this namespace and tell Centrifigo to proxy subscribe and publish events for channels in the namespace.</li>\n</ul>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>It's a good practice to use different namespaces in Centrifugo for different real-time features as this allows enabling only required options for a specific task.</p></div></div>\n<p>Also, config has some options related to <a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy\">Centrifugo proxy feature</a>. This feature allows proxying WebSocket events to the configured endpoints. We will proxy three types of events:</p>\n<ol>\n<li class=\"\">Connect (called when a user establishes WebSocket connection with Centrifugo)</li>\n<li class=\"\">Subscribe (called when a user wants to subscribe on a channel)</li>\n<li class=\"\">Publish (called when a user tries to publish data to a channel)</li>\n</ol>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"adding-nginx\">Adding Nginx<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#adding-nginx\" class=\"hash-link\" aria-label=\"Direct link to Adding Nginx\" title=\"Direct link to Adding Nginx\" translate=\"no\">​</a></h2>\n<p>In Centrifugo config we set endpoints which we will soon implement inside our Django app. You may notice that the allowed origin has a URL with port <code>9000</code>. That's because we want to proxy Cookie headers from a persistent connection established with Centrifugo to the Django app and need Centrifugo and Django to share the same origin (so browsers can send Django session cookies to Centrifugo).</p>\n<p>While not used in this tutorial (we will use fake <code>tutorial-user</code> as user ID here) – this can be useful if you decide to authenticate connections using Django native sessions framework later. To achieve this we should also add Nginx with a configuration like this:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">nginx.conf</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">events {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    worker_connections 1024;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">error_log /dev/stdout info;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">http {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    access_log /dev/stdout;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    server {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        listen 9000;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        server_name localhost;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        location / {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_pass http://host.docker.internal:8000;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_http_version 1.1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_set_header Host $host;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_set_header X-Real-IP $remote_addr;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_set_header X-Forwarded-Proto $scheme;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        location /connection/websocket {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_pass http://host.docker.internal:8001;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_http_version 1.1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_buffering off;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            keepalive_timeout 65;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_read_timeout 60s;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_set_header Upgrade $http_upgrade;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_set_header Connection 'upgrade';</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_set_header Host $host;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_set_header X-Real-IP $remote_addr;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_set_header X-Forwarded-Proto $scheme;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            proxy_cache_bypass $http_upgrade;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div></code></pre></div></div>\n<p>Start Nginx (replace the path to <code>nginx.conf</code> to yours):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker run -it --rm -v /path/to/nginx.conf:/etc/nginx/nginx.conf:ro -p 9000:9000 --add-host=host.docker.internal:host-gateway nginx</span><br></div></code></pre></div></div>\n<p>Note that we are exposing port 9000 to localhost and use a possibility to use <code>host.docker.internal</code> host to communicate from inside Docker network with services which are running on localhost (on the host machine). See <a href=\"https://stackoverflow.com/questions/31324981/how-to-access-host-port-from-docker-container\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">this answer on SO</a>.</p>\n<p>Open <a href=\"http://localhost:9000/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:9000</a>. Nginx should now properly proxy requests to Django server and to Centrifugo, but we still need to do some things.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"implementing-proxy-handlers\">Implementing proxy handlers<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#implementing-proxy-handlers\" class=\"hash-link\" aria-label=\"Direct link to Implementing proxy handlers\" title=\"Direct link to Implementing proxy handlers\" translate=\"no\">​</a></h2>\n<p>Well, now if you try to open a chat page with Nginx, Centrifugo, Django, and Redis running you will notice some errors in Centrifugo logs. That's because Centrifugo tries to proxy WebSocket connect events to Django to authenticate them but we have not created event handlers in Django yet. Let's fix this.</p>\n<p>Extend chat/urls.py:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">chat/urls.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> django</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">urls </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> re_path</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> views</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">urlpatterns </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">''</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> views</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">index</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> name</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'index'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    re_path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'room/(?P&lt;room_name&gt;[A-z0-9_-]+)/'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> views</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">room</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> name</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'room'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'centrifugo/connect/'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> views</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> name</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'connect'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'centrifugo/subscribe/'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> views</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> name</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'subscribe'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    path</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'centrifugo/publish/'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> views</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">publish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> name</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publish'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><br></div></code></pre></div></div>\n<p>Extend chat/views.py:</p>\n<div class=\"language-python codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">chat/views.py</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-python codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> django</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">http </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> JsonResponse</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">from</span><span class=\"token plain\"> django</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">views</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">decorators</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">csrf </span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> csrf_exempt</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:rgb(199, 146, 234)\">@csrf_exempt</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># In connect handler we must authenticate connection.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># Here we return a fake user ID to Centrifugo to keep tutorial short.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># More details about connect result format can be found in proxy docs:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># https://centrifugal.dev/docs/server/proxy#connect-proxy</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    logger</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">debug</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">body</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    response </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'result'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'user'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'tutorial-user'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> JsonResponse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">response</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:rgb(199, 146, 234)\">@csrf_exempt</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">publish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># In publish handler we can validate publication request initialted by a user.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># Here we return an empty object – thus allowing publication.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># More details about publish result format can be found in proxy docs:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># https://centrifugal.dev/docs/server/proxy#publish-proxy</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    response </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'result'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> JsonResponse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">response</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token decorator annotation punctuation\" style=\"color:rgb(199, 146, 234)\">@csrf_exempt</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">def</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># In subscribe handler we can validate user subscription request to a channel.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># Here we return an empty object – thus allowing subscription.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># More details about subscribe result format can be found in proxy docs:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\"># https://centrifugal.dev/docs/server/proxy#subscribe-proxy</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    response </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'result'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> JsonResponse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">response</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\">        </span><br></div></code></pre></div></div>\n<p><code>connect</code> view will accept all connections and return user ID as <code>tutorial-user</code>. In real app you most probably want to use Django sessions and return real authenticated user ID instead of <code>tutorial-user</code>. Since we told Centrifugo to proxy connection <code>Cookie</code> headers native Django user authentication will work just fine.</p>\n<p>Restart Django and try the chat app again. You should now successfully connect. Open a browser tab to the room page at <a href=\"http://localhost:9000/chat/room/lobby/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:9000/chat/room/lobby/</a>. Open a second browser tab to the same room page.</p>\n<p>In the second browser tab, type the message \"hello\" and press Enter. You should now see \"hello\" echoed in the chat log in both the second browser tab and in the first browser tab.</p>\n<p>You now have a basic fully-functional chat server!</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"what-could-be-improved\">What could be improved<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#what-could-be-improved\" class=\"hash-link\" aria-label=\"Direct link to What could be improved\" title=\"Direct link to What could be improved\" translate=\"no\">​</a></h2>\n<p>The list is large, but it's fun to do. To name some possible improvements:</p>\n<ul>\n<li class=\"\">Replace <code>tutorial-user</code> used here with native Django session framework. We already proxying the <code>Cookie</code> header to Django from Centrifugo, so you can reuse native Django authentication. Only allow authenticated users to join rooms.</li>\n<li class=\"\">Create <code>Room</code> model and add users to it – thus you will be able to check permissions inside subscribe and publish handlers.</li>\n<li class=\"\">Create <code>Message</code> model to display chat history in <code>Room</code>.</li>\n<li class=\"\">Replace Django devserver with something more suitable for production like <a href=\"https://gunicorn.org/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Gunicorn</a>.</li>\n<li class=\"\">Check out Centrifugo possibilities like presence to display online users.</li>\n<li class=\"\">Use <a href=\"https://github.com/centrifugal/cent\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">cent</a> Centrifugo HTTP API library to publish something to a user on behalf of a server. In this case you can avoid using publish proxy, publish messages to Django over convinient AJAX call - and then call Centrifugo HTTP API to publish message into a channel.</li>\n<li class=\"\">You can replace connect proxy (which is an HTTP call from Centrifugo to Django on each connect) with JWT authentication. JWT authentication may result in a better application performance (since no additional proxy requests will be issued on connect). It can allow your Django app to handle millions of users on a reasonably small hardware and survive mass reconnects from all those users. More details can be found in <a href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Scaling WebSocket in Go and beyond</a> blog post.</li>\n<li class=\"\">Instead of using subscribe proxy you can put channel into connect proxy result or into JWT – thus using <a class=\"\" href=\"https://centrifugal.dev/docs/server/server_subs\">server-side subscriptions</a> and avoid subscribe proxy HTTP call.</li>\n</ul>\n<p>One more thing I'd like to note is that if you aim to build a chat application like WhatsApp or Telegram where you have a screen with list of chats (which can be pretty long!) you should not create a separate channel for each room. In this case using separate channel per room does not scale well and you better use personal channel for each user to receive all user-related messages. And as soon as message published to a chat you can send message to each participant's channel. In this case, take a look at Centrifugo <a class=\"\" href=\"https://centrifugal.dev/docs/server/server_api#broadcast\">broadcast API</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"tutorial-source-code-with-docker-compose\">Tutorial source code with docker-compose<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#tutorial-source-code-with-docker-compose\" class=\"hash-link\" aria-label=\"Direct link to Tutorial source code with docker-compose\" title=\"Direct link to Tutorial source code with docker-compose\" translate=\"no\">​</a></h2>\n<p>The full example which can run by issuing a single <code>docker compose up</code> <a href=\"https://github.com/centrifugal/examples/tree/master/v3/python_django_chat_tutorial\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">can be found on Github</a>. It also has some CSS styles so that the chat looks like shown in the beginning.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>Here we implemented a basic chat app with Django and Centrifugo.</p>\n<p>While a chat still requires work to be suitable for production this example can help understand core concepts of Centrifugo - specifically channel namespaces and proxy features.</p>\n<p>It's possible to use unidirectional Centrifugo transports instead of bidirectional WebSocket used here – in this case, you can go without using <code>centrifuge-js</code> at all.</p>\n<p>Centrifugo scales perfectly if you need to handle more connections – thanks to Centrifugo built-in PUB/SUB engines.</p>\n<p>It's also possible to use server-side subscriptions, keep channel history cache, use JWT authentication instead of connect proxy, enable channel presence, and more. All the power of Centrifugo is in your hands.</p>\n<p>Hope you enjoyed this tutorial. And let the Centrifugal force be with you!</p>\n<p>Join our <a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/introduction#join-community\">community channels</a> in case of any questions left after reading this.</p>",
            "url": "https://centrifugal.dev/blog/2021/11/04/integrating-with-django-building-chat-application",
            "title": "Centrifugo integration with Django – building a basic chat application",
            "summary": "In this tutorial, we are integrating Django with Centrifugo to make a basic chat application. We are using Centrifugo proxy feature to proxy WebSocket connection events to a Django backend.",
            "date_modified": "2021-11-04T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "tutorial",
                "django"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2021/10/18/integrating-with-nodejs",
            "content_html": "<p>Centrifugo is a scalable real-time messaging server in a language-agnostic way. In this tutorial we will integrate Centrifugo with NodeJS backend using a connect proxy feature of Centrifugo for user authentication and native session middleware of ExpressJS framework.</p>\n<p>Why would NodeJS developers want to integrate a project with Centrifugo? This is a good question especially since there are lots of various tools for real-time messaging available in NodeJS ecosystem.</p>\n<div class=\"theme-admonition theme-admonition-caution admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>caution</div><div class=\"admonitionContent_BuS1\"><p>This tutorial was written for Centrifugo v3. We recently released <a class=\"\" href=\"https://centrifugal.dev/blog/2022/07/19/centrifugo-v4-released\">Centrifugo v4</a> which makes some parts of this tutorial obsolete. The core concepts are similar though – so this can still be used as a Centrifugo learning step.</p></div></div>\n<p>I found several points which could be a good motivation:</p>\n<ul>\n<li class=\"\">Centrifugo scales well – we have a very optimized Redis Engine with client-side sharding and Redis Cluster support. We can also scale with KeyDB, Nats, or Tarantool. Centrifugo can scale to millions connections distributed over different server nodes.</li>\n<li class=\"\">Centrifugo is pretty fast (written in Go) and can handle thousands of clients per node. Client protocol is optimized for thousands of messages per second.</li>\n<li class=\"\">Centrifugo provides a variety of features out-of-the-box – some of them are unique, especially for real-time servers that scale to many nodes.</li>\n<li class=\"\">Centrifugo works as a separate service – so can be a universal tool in developer's pocket, can migrate from one project to another, no matter what programming language or framework is used for a business logic.</li>\n</ul>\n<p>Having said this all – let's move to a tutorial itself.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"what-we-are-building\">What we are building<a href=\"https://centrifugal.dev/blog/2021/10/18/integrating-with-nodejs#what-we-are-building\" class=\"hash-link\" aria-label=\"Direct link to What we are building\" title=\"Direct link to What we are building\" translate=\"no\">​</a></h2>\n<p>Not a super-cool app to be honest. Our goal here is to give a reader an idea how integration with Centrifugo could look like. There are many possible apps which could be built on top of this knowledge.</p>\n<p>The end result here will allow application user to authenticate and once authenticated – connect to Centrifugo. Centrifugo will proxy connection requests to NodeJS backend and native ExpressJS session middleware will be used for connection authentication. We will also send some periodical real-time messages to a user personal channel.</p>\n<p>The <a href=\"https://github.com/centrifugal/examples/tree/master/v3/nodejs_proxy\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">full source code of this tutorial</a> located on Github. You can clone examples repo and run this demo by simply writing:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">docker compose up</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"creating-expressjs-app\">Creating Express.js app<a href=\"https://centrifugal.dev/blog/2021/10/18/integrating-with-nodejs#creating-expressjs-app\" class=\"hash-link\" aria-label=\"Direct link to Creating Express.js app\" title=\"Direct link to Creating Express.js app\" translate=\"no\">​</a></h2>\n<p>Start new NodeJS app:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">npm init</span><br></div></code></pre></div></div>\n<p>Install dependencies:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">npm install express express-session cookie-parser axios morgan</span><br></div></code></pre></div></div>\n<p>Create <code>index.js</code> file.</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">index.js</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> express </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">require</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'express'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> cookieParser </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">require</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"cookie-parser\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> sessions </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">require</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'express-session'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> morgan </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">require</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'morgan'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> axios </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">require</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'axios'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> app </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">express</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> port </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">3000</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">use</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">express</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> oneDay </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1000</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">60</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">60</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">24</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">use</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">sessions</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">secret</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"this_is_my_secret_key\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">saveUninitialized</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">cookie</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">maxAge</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> oneDay </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">resave</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">false</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">use</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">cookieParser</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">use</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">express</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">urlencoded</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">extended</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">use</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">express</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">use</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">express</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">static</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'static'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">use</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">morgan</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'dev'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">get</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'/'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">req</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">req</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">session</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">userid</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">sendFile</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'views/app.html'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">root</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> __dirname </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">else</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">sendFile</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'views/login.html'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">root</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> __dirname </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">listen</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">port</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token template-string string\" style=\"color:rgb(195, 232, 141)\">Example app listening at http://localhost:</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">${</span><span class=\"token template-string interpolation\">port</span><span class=\"token template-string interpolation interpolation-punctuation punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Create <code>login.html</code> file in <code>views</code> folder:</p>\n<div class=\"language-html codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">views/login.html</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-html codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">form</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">action</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">/login</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">method</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">post</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">h2</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\">Login (username: demo-user, password: demo-pass)</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">h2</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">input-field</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">input</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">type</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">text</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">name</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">username</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">username</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">placeholder</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">Enter Username</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">class</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">input-field</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">input</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">type</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">password</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">name</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">password</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">password</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">placeholder</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">Enter Password</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">input</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">type</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">submit</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">value</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">Log in</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">form</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><br></div></code></pre></div></div>\n<p>Also create <code>app.html</code> file in <code>views</code> folder:</p>\n<div class=\"language-html codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">views/app.html</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-html codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">head</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">link</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">rel</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">stylesheet</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">href</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">app.css</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">src</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">https://cdn.jsdelivr.net/gh/centrifugal/centrifuge-js@2.8.3/dist/centrifuge.min.js</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token script\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">script</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">head</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">a</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">href</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">'</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">/logout</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">'</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\">Click to logout</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">a</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\"> </span><span class=\"token tag attr-name\" style=\"color:rgb(255, 203, 107)\">id</span><span class=\"token tag attr-value punctuation attr-equals\" style=\"color:rgb(199, 146, 234)\">=</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag attr-value\" style=\"color:rgb(255, 85, 114)\">log</span><span class=\"token tag attr-value punctuation\" style=\"color:rgb(199, 146, 234)\">\"</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">div</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">body</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&lt;/</span><span class=\"token tag\" style=\"color:rgb(255, 85, 114)\">html</span><span class=\"token tag punctuation\" style=\"color:rgb(199, 146, 234)\">&gt;</span><br></div></code></pre></div></div>\n<p>Make attention that we import <code>centrifuge-js</code> client here which abstracts away Centrifugo bidirectional WebSocket protocol.</p>\n<p>Let's write an HTTP handler for login form:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">index.js</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> myusername </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'demo-user'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> mypassword </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'demo-pass'</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">post</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'/login'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">req</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">req</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">body</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">username</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">==</span><span class=\"token plain\"> myusername </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;&amp;</span><span class=\"token plain\"> req</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">body</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">password</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">==</span><span class=\"token plain\"> mypassword</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    req</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">session</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">userid</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> req</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">body</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">username</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">redirect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'/'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">else</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">send</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'Invalid username or password'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>In this example we use hardcoded username and password for out single user. Of course in real app you will have a database with user credentials. But since our goal is only show integration with Centrifugo – we are skipping these hard parts here.</p>\n<p>Also create a handler for a logout request:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">index.js</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">get</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'/logout'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">req</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  req</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">session</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">destroy</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">redirect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'/'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Now if you run an app with <code>node index.js</code> you will see a login form using which you can authenticate. At this point this is a mostly convenient NodeJS application, let's add Centrifugo integration.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"starting-centrifugo\">Starting Centrifugo<a href=\"https://centrifugal.dev/blog/2021/10/18/integrating-with-nodejs#starting-centrifugo\" class=\"hash-link\" aria-label=\"Direct link to Starting Centrifugo\" title=\"Direct link to Starting Centrifugo\" translate=\"no\">​</a></h2>\n<p>Run Centrifugo with <code>config.json</code> like this:</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">config.json</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"token_hmac_secret_key\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"secret\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"admin\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"admin_password\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"password\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"admin_secret\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"my_admin_secret\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"api_key\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"my_api_key\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"allowed_origins\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:9000\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"user_subscribe_to_personal\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"proxy_connect_endpoint\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"http://localhost:3000/centrifugo/connect\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"proxy_http_headers\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Cookie\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>I.e.:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">./centrifugo -c config.json</span><br></div></code></pre></div></div>\n<p>Create <code>app.js</code> file in <code>static</code> folder:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">static/app.js</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">drawText</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">text</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> div </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">createElement</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'div'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    div</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">innerHTML</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> text</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token dom variable\" style=\"color:rgb(191, 199, 213)\">document</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">getElementById</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'log'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">appendChild</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">div</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'ws://localhost:9000/connection/websocket'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'connect'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">drawText</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'Connected to Centrifugo'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'disconnect'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">drawText</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'Disconnected from Centrifugo'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publish'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">drawText</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'Publication, time = '</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"adding-nginx\">Adding Nginx<a href=\"https://centrifugal.dev/blog/2021/10/18/integrating-with-nodejs#adding-nginx\" class=\"hash-link\" aria-label=\"Direct link to Adding Nginx\" title=\"Direct link to Adding Nginx\" translate=\"no\">​</a></h2>\n<p>Since we are going to use native session auth of ExpressJS we can't just connect from localhost:3000 (where our NodeJS app is served) to Centrifugo running on localhost:8000 – browser won't send a <code>Cookie</code> header to Centrifugo in this case. Due to this reason we need a reverse proxy which will terminate a traffic from frontend and proxy requests to NodeJS process or to Centrifugo depending on URL path. In this case both browser and NodeJS app will share the same origin – so Cookie will be sent to Centrifugo in WebSocket Upgrade request.</p>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>Alternatively, we could also use <a class=\"\" href=\"https://centrifugal.dev/docs/server/authentication\">JWT authentication</a> of Centrifugo but that's a topic for another tutorial. Here we are using <a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy#connect-proxy\">connect proxy feature</a> for auth.</p></div></div>\n<p>Nginx config will look like this:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">server {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  listen 9000;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  server_name localhost;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  location / {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_pass http://localhost:3000;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_http_version 1.1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header Host $host;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header X-Real-IP $remote_addr;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header X-Forwarded-Proto $scheme;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  location /connection {</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_pass http://localhost:8000;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_http_version 1.1;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header Upgrade $http_upgrade;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header Connection 'upgrade';</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header Host $host;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header X-Real-IP $remote_addr;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_set_header X-Forwarded-Proto $scheme;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    proxy_cache_bypass $http_upgrade;</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  }</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">}</span><br></div></code></pre></div></div>\n<p>Run Nginx and open <a href=\"http://localhost:9000/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:9000</a>. After authenticating in app you should see an attempt to connect to a WebSocket endpoint. But connection will fail since we need to implement connect proxy handler in NodeJS app.</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">index.js</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">app</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">post</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'/centrifugo/connect'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">req</span><span class=\"token parameter punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token parameter\"> res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">req</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">session</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">userid</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token literal-property property\">result</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">user</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> req</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">session</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">userid</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">else</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    res</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">json</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token literal-property property\">disconnect</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">code</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1000</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">reason</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"unauthorized\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">reconnect</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">false</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Restart NodeJS process and try opening an app again. Application should now successfully connect to Centrifugo.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"send-real-time-messages\">Send real-time messages<a href=\"https://centrifugal.dev/blog/2021/10/18/integrating-with-nodejs#send-real-time-messages\" class=\"hash-link\" aria-label=\"Direct link to Send real-time messages\" title=\"Direct link to Send real-time messages\" translate=\"no\">​</a></h2>\n<p>Let's also periodically publish current server time to a client's personal channel. In Centrifugo configuration we set a <code>user_subscribe_to_personal</code> option which turns on <a class=\"\" href=\"https://centrifugal.dev/docs/server/server_subs#automatic-personal-channel-subscription\">automatic subscription to a personal channel</a> for each connected user. We can use <code>axios</code> library and send publish API requests to Centrifugo periodically (according to <a class=\"\" href=\"https://centrifugal.dev/docs/server/server_api#http-api\">API docs</a>):</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">index.js</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifugoApiClient </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> axios</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">create</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">baseURL</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token template-string string\" style=\"color:rgb(195, 232, 141)\">http://centrifugo:8000/api</span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token literal-property property\">headers</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token literal-property property\">Authorization</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token template-string string\" style=\"color:rgb(195, 232, 141)\">apikey my_api_key</span><span class=\"token template-string template-punctuation string\" style=\"color:rgb(195, 232, 141)\">`</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string-property property\">'Content-Type'</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'application/json'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">setInterval</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">async</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token arrow operator\" style=\"color:rgb(137, 221, 255)\">=&gt;</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">try</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> centrifugoApiClient</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">post</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">''</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token literal-property property\">method</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publish'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token literal-property property\">params</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">channel</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'#'</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">+</span><span class=\"token plain\"> myusername</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// construct personal channel name.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token literal-property property\">data</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">          </span><span class=\"token literal-property property\">time</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token known-class-name class-name\" style=\"color:rgb(255, 203, 107)\">Math</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">floor</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Date</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">getTime</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">/</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1000</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">      </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">catch</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token property-access\">message</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">5000</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>After restarting NodeJS you should see periodical updates on application web page.</p>\n<p>You can also log in into Centrifugo admin web UI <a href=\"http://localhost:8000/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">http://localhost:8000</a> using password <code>password</code> - and play with other available server API from within web interface.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2021/10/18/integrating-with-nodejs#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>While not being super useful this example can help understanding core concepts of Centrifugo - specifically connect proxy feature and server API.</p>\n<p>It's possible to use unidirectional Centrifugo transports instead of bidrectional WebSocket used here – in this case you can go without using <code>centrifuge-js</code> at all.</p>\n<p>This application scales perfectly if you need to handle more connections – thanks to Centrifugo builtin PUB/SUB engines.</p>\n<p>It's also possible to use client-side subscriptions, keep channel history cache, enable channel presence and more. All the power of Centrifugo is in your hands.</p>",
            "url": "https://centrifugal.dev/blog/2021/10/18/integrating-with-nodejs",
            "title": "Centrifugo integration with NodeJS tutorial",
            "summary": "In this tutorial we are integrating Centrifugo with NodeJS. We are using Centrifugo connect proxy feature to authenticate connections over standard Express.js session middleware.",
            "date_modified": "2021-10-18T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifugo",
                "tutorial",
                "proxy"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3",
            "content_html": "<p>After almost three years of Centrifugo v2 life cycle we are happy to announce the next major release of Centrifugo. During the last several months deep in our Centrifugal laboratory we had been synthesizing an improved version of the server.</p>\n<p>New Centrifugo v3 is targeting to improve Centrifugo adoption for basic real-time application cases, improves server performance and extends existing features with new functionality. It comes with unidirectional real-time transports, protocol speedups, super-fast engine implementation based on Tarantool, new documentation site, GRPC proxy, API extensions and PRO version which provides unique possibilities for business adopters.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"centrifugo-v2-flashbacks\">Centrifugo v2 flashbacks<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#centrifugo-v2-flashbacks\" class=\"hash-link\" aria-label=\"Direct link to Centrifugo v2 flashbacks\" title=\"Direct link to Centrifugo v2 flashbacks\" translate=\"no\">​</a></h3>\n<p>Centrifugo v2 life cycle has come to an end. Before discussing v3 let's look back at what has been done during the last three years.</p>\n<p>Centrifugo v2 was a pretty huge refactoring of v1. Since the v2 release, Centrifugo is built on top of  new <a href=\"https://github.com/centrifugal/centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifuge library</a> for Go language. Centrifuge library evolved significantly since its initial release and now powers Grafana v8 real-time streaming among other things.</p>\n<p>Here is an awesome demo made by my colleague <a href=\"https://github.com/alexanderzobnin\">Alexander Zobnin</a> that demonstrates real-time telemetry of Assetto Corsa sports car streamed in real-time to Grafana dashboard:</p>\n<div class=\"vimeo-full-width\"><iframe src=\"https://player.vimeo.com/video/570333329?title=0&amp;byline=0&amp;portrait=0\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen=\"\"></iframe></div>\n<p></p>\n<p>Centrifugo integrated with Redis Streams, got Redis Cluster support, can now work with Nats server as a PUB/SUB broker. Notable additions of Centrifugo v2 were <a class=\"\" href=\"https://centrifugal.dev/docs/server/server_subs\">server-side subscriptions</a> with some interesting features on top – like maintaining a single global connection from one user and automatic personal channel subscription upon user connect.</p>\n<p>A very good addition which increased Centrifugo adoption a lot was introduction of <a class=\"\" href=\"https://centrifugal.dev/docs/server/proxy\">proxy to backend</a>. This made Centrifugo fit many setups where JWT authentication and existing subscription permission model did not suit well before.</p>\n<p>Client ecosystem improved significantly. The fact that client protocol migrated to a strict Protobuf schema allowed to introduce binary protocol format (in addition to JSON) and simplify building client connectors. We now have much better and complete client libraries (compared to v1 situation).</p>\n<p>We also have an <a href=\"https://github.com/centrifugal/helm-charts\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">official Helm chart</a>, <a href=\"https://grafana.com/grafana/dashboards/13039\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Grafana dashboard</a> for Prometheus datasource, and so on.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://grafana.com/api/dashboards/13039/images/8950/image\" alt=\"\" class=\"img_ev3q\"></p>\n<p>Centrifugo is becoming more noticeable in a wider real-time technology community. For example, it was included in a <a href=\"https://ably.com/periodic-table-of-realtime\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">periodic table of real-time</a> created by Ably.com (one of the most powerful real-time messaging cloud services at the moment):</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://ik.imagekit.io/ably/ghost/prod/2021/08/periodic-table-screenshots-combined-without-banner-no-legend.jpg?tr=w-1520\" alt=\"\" class=\"img_ev3q\"></p>\n<p>Of course, there are many aspects where Centrifugo can be improved. And v3 addresses some of them. Below we will look at the most notable features and changes of the new major Centrifugo version.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"backwards-compatibility\">Backwards compatibility<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#backwards-compatibility\" class=\"hash-link\" aria-label=\"Direct link to Backwards compatibility\" title=\"Direct link to Backwards compatibility\" translate=\"no\">​</a></h3>\n<p>Let's start with the most important thing – backwards compatibility concerns.</p>\n<p>In Centrifugo v3 client protocol mostly stayed the same. We expect that most applications will be able to update without any change on a client-side. This was an important concern for v3 given how painful the update cycle can be on mobile devices and lessons learned from v1 to v2 migration. There is one breaking change though which can affect users who use history API manually from a client-side (we provide a temporary workaround to give apps a chance to migrate smoothly).</p>\n<p>On a server-side, much more changes happened, especially in the configuration: some options were renamed, some were removed. We provide a <a class=\"\" href=\"https://centrifugal.dev/docs/3/getting-started/migration_v3#v2-to-v3-config-converter\">v2 to v3 configuration converter</a> which can help dealing with changes. In most cases, all you should do is adapt Centrifugo configuration to match v3 changes and redeploy Centrifugo using v3 build instead of v2. All features are still there (or a replacement exists, like for <code>channels</code> API).</p>\n<p>For more details, refer to the <a class=\"\" href=\"https://centrifugal.dev/docs/3/getting-started/migration_v3\">v3 migration guide</a>.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"license-change\">License change<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#license-change\" class=\"hash-link\" aria-label=\"Direct link to License change\" title=\"Direct link to License change\" translate=\"no\">​</a></h3>\n<p>As some of you know we considered changing Centrifugo license to AGPL v3 for a new release. After thinking a lot about this we decided to not step into this area.</p>\n<p>But the license has been changed: the license of OSS Centrifugo is now Apache 2.0 instead of MIT. Apache 2.0 is also a permissive OSS license, it's just a bit more concrete in some aspects.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://user-images.githubusercontent.com/2097922/91162089-8570e100-e6c3-11ea-8c41-cd8fcfe049d0.png\" alt=\"\" class=\"img_ev3q\"></p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"unidirectional-real-time-transports\">Unidirectional real-time transports<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#unidirectional-real-time-transports\" class=\"hash-link\" aria-label=\"Direct link to Unidirectional real-time transports\" title=\"Direct link to Unidirectional real-time transports\" translate=\"no\">​</a></h3>\n<p>Server-side subscriptions introduced in Centrifugo v2 and recent improvements in the underlying Centrifuge library opened a road for a unidirectional approach.</p>\n<p>This means that Centrifugo v3 provides a set of unidirectional real-time transports where messages flow only in one direction – from a server to a client. Why is this change important?</p>\n<p>Centrifugo originally concentrated on using bidirectional transports for client-server communication. Like WebSocket and SockJS. Bidirectional transports allow implementing some great protocol features since a client can communicate with a server in various ways after establishing a persistent connection. While this is a great opportunity this also leads to an increased complexity.</p>\n<p>Centrifugo users had to use special client connector libraries which abstracted underlying work into a simple public API. But internally connectors do many things: matching requests to responses, handling timeouts, handling an ordering, queuing operations, error handling. So the client connector is a pretty complex piece of software.</p>\n<p>But what if a user just needs to receive real-time updates from a stable set of channels known in connection time? Can we simplify everything and avoid using custom software on a client-side?</p>\n<p>With unidirectional transports, the answer is yes. Clients can now connect to Centrifugo using a bunch of unidirectional transports. And the greatest thing is that in this case, developers should not depend on Centrifugo client connectors at all – just use native browser APIs or GRPC-generated code. It's finally possible to consume events from Centrifugo using CURL (see <a class=\"\" href=\"https://centrifugal.dev/docs/transports/uni_http_stream#connecting-using-curl\">an example</a>).</p>\n<p>Using unidirectional transports you can still benefit from Centrifugo built-in scalability with various engines, utilize built-in authentication over JWT or the connect proxy feature.</p>\n<p>With subscribe server API (see below) it's even possible to subscribe unidirectional client to server-side channels dynamically. With refresh server API or the refresh proxy feature it's possible to manage a connection expiration.</p>\n<p>Centrifugo supports the following unidirectional transports:</p>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/transports/uni_sse\">EventSource (SSE)</a></li>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/transports/uni_http_stream\">HTTP streaming</a></li>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/transports/uni_websocket\">Unidirectional WebSocket</a></li>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/transports/uni_grpc\">Unidirectional GRPC stream</a></li>\n</ul>\n<p>We expect that introducing unidirectional transports will significantly increase Centrifugo adoption.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"history-iteration-api\">History iteration API<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#history-iteration-api\" class=\"hash-link\" aria-label=\"Direct link to History iteration API\" title=\"Direct link to History iteration API\" translate=\"no\">​</a></h3>\n<img src=\"https://centrifugal.dev/img/centrifuge.svg\" align=\"right\" width=\"25%\">\n<p>There was a rather important limitation of Centrifugo history API – it was not very suitable for keeping large streams because a call to a history could only return the entire channel history.</p>\n<p>Centrifugo v3 introduces an API to iterate over a stream. It's possible to do from the current stream beginning or end, in both directions – forward and backward, with configured limit. Also with certain starting stream position if it's known.</p>\n<p>This, among other things, can help to implement manual missed message recovery on a client-side to reduce the load on the application backend.</p>\n<p>Here is an example program in Go which endlessly iterates over stream both ends (using <a href=\"https://github.com/centrifugal/gocent\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">gocent</a> API library), upon reaching the end of stream the iteration goes in reversed direction (not really useful in real world but fun):</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Iterate by 10.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">limit </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">10</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Paginate in reversed order first, then invert it.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">reverse </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Start with nil StreamPosition, then fill it with value while paginating.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> sp </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">gocent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">StreamPosition</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\thistoryResult</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> c</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">History</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        ctx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        channel</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tgocent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">WithLimit</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">limit</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tgocent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">WithReverse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">reverse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        gocent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">WithSince</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">sp</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatalf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"Error calling history: %v\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> pub </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">range</span><span class=\"token plain\"> historyResult</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Publications </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">pub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Offset</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"=&gt;\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">pub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tsp </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">gocent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">StreamPosition</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tOffset</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> pub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Offset</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tEpoch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">  historyResult</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Epoch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">len</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">historyResult</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Publications</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\"> limit </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Got all pubs, invert pagination direction.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\treverse </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token plain\">reverse</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"end of stream reached, change iteration direction\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-caution admonition_xJq3 alert alert--warning\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z\"></path></svg></span>caution</div><div class=\"admonitionContent_BuS1\"><p>This new API does not remove the need in having the main application database – that's still mandatory for idiomatic Centrifugo usage.</p></div></div>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"redis-streams-by-default\">Redis Streams by default<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#redis-streams-by-default\" class=\"hash-link\" aria-label=\"Direct link to Redis Streams by default\" title=\"Direct link to Redis Streams by default\" translate=\"no\">​</a></h3>\n<p>In Centrifugo v3 Redis engine uses Redis Stream data structure by default for keeping channel history. Before v3 Redis Streams were supported by not enabled by default so almost nobody used them. This change is important in terms of introducing history iteration API described above – since Redis Streams allow doing iteration effectively.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"tarantool-engine\">Tarantool engine<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#tarantool-engine\" class=\"hash-link\" aria-label=\"Direct link to Tarantool engine\" title=\"Direct link to Tarantool engine\" translate=\"no\">​</a></h3>\n<p>As you may know, Centrifugo has several built-in engines that allow scaling Centrifugo nodes (using PUB/SUB) and keep shared history and presence state. Before v3 Centrifugo had in-memory and Redis (or KeyDB) engines available.</p>\n<p>Introducing a new engine to Centrifugo is pretty hard since the engine should provide a very robust PUB/SUB performance, fast history and presence operations, possibility to publish a message to PUB/SUB and save to history atomically. It also should allow dealing with ephemeral frequently changing subscriptions. It's typical for Centrifugo use case to have millions of users each subscribed to a  unique channel and constantly connecting/disconnecting (thus subscribing/unsubscribing).</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://www.tadviser.ru/images/thumb/1/1a/Tarantool_%D0%A1%D0%A3%D0%91%D0%94_logo_2020.png/840px-Tarantool_%D0%A1%D0%A3%D0%91%D0%94_logo_2020.png\" alt=\"\" class=\"img_ev3q\"></p>\n<p>In v3 we added <strong>experimental</strong> support for the <a href=\"https://www.tarantool.io/en/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Tarantool</a> engine. It fits nicely all the requirements above and provides a huge performance speedup for history and presence operations compared to Redis. According to our benchmarks, the speedup can be up to 4-10x depending on operation. The PUB/SUB performance of Tarantool is comparable with Redis (10-20% worse according to our internal benchmarks to be exact, but that's pretty much the same).</p>\n<p>For example, let's look at Centrifugo benchmark where we recover zero messages (i.e. emulate a situations when many connections disconnected for a very short time interval due to load balancer reload).</p>\n<p>For Redis engine:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">Redis engine, single Redis instance</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkRedisRecover       26883 ns/op\t    1204 B/op\t   28 allocs/op</span><br></div></code></pre></div></div>\n<p>Compare it with the same operation measured with Tarantool engine:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">Tarantool engine, single Tarantool instance</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkTarantoolRecover    6292 ns/op\t     563 B/op\t   10 allocs/op</span><br></div></code></pre></div></div>\n<p>Tarantool can provide new storage properties (like synchronous replication), new adoption. We are pretty excited about adding it as an option.</p>\n<p>The reason why Tarantool support is experimental is because Tarantool integration involves one more moving piece – the <a href=\"https://github.com/centrifugal/tarantool-centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifuge Lua module</a> which should be run by a Tarantool server.</p>\n<p>This increases deployment complexity and given the fact that many users have their own best practices in Tarantool deployment we are still evaluating a sufficient way to distribute Lua part. For now, we are targeting standalone (see examples in <a href=\"https://github.com/centrifugal/tarantool-centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifugal/tarantool-centrifuge</a>) and Cartridge Tarantool setups (with <a href=\"https://github.com/centrifugal/rotor\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifugal/rotor</a>).</p>\n<p>Refer to the <a class=\"\" href=\"https://centrifugal.dev/docs/server/engines#tarantool-engine\">Tarantool Engine documentation</a> for more details.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"grpc-proxy\">GRPC proxy<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#grpc-proxy\" class=\"hash-link\" aria-label=\"Direct link to GRPC proxy\" title=\"Direct link to GRPC proxy\" translate=\"no\">​</a></h3>\n<p>Centrifugo can now transform events received over persistent connections from users into GRPC calls to the application backend (in addition to the HTTP proxy available in v2).</p>\n<p>GRPC support should make Centrifugo ready for today's microservice architecture where GRPC is a huge player for inter-service communication.</p>\n<p>So we mostly just provide more choices for Centrifugo users here. GRPC has some good advantages – for example an application backend RPC layer which is responsible for communication with Centrifugo can now be generated from Protobuf definitions for all popular programming languages.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"server-api-improvements\">Server API improvements<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#server-api-improvements\" class=\"hash-link\" aria-label=\"Direct link to Server API improvements\" title=\"Direct link to Server API improvements\" translate=\"no\">​</a></h3>\n<img src=\"https://centrifugal.dev/img/test-tube.svg\" align=\"right\" width=\"25%\">\n<p>Centrifugo v3 has some valuable server API improvements.</p>\n<p>The new <code>subscribe</code> API method allows subscribing connection to a channel at any point in time. This works by utilizing server-side subscriptions. So it's not only possible to subscribe connection to a list of server-side channels during the connection establishment phase – but also later during the connection lifetime. This may be very useful for the unidirectional approach - by emulating client-side subscribe call over request to application backend which in turn calls subscribe Centrifugo server API.</p>\n<p>Publish API now returns the current top stream position (offset and epoch) for channels with history enabled.</p>\n<p>Server history API inherited iteration possibilities described above.</p>\n<p>Channels command now returns a number of clients in a channel, also supports channel filtering by a pattern. Since we changed how channels call implemented internally there is no limitation anymore to call it when using Redis cluster.</p>\n<p>Admin web UI has been updated too to support new API methods, so you can play with new API from its <code>actions</code> tab.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"better-clustering\">Better clustering<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#better-clustering\" class=\"hash-link\" aria-label=\"Direct link to Better clustering\" title=\"Direct link to Better clustering\" translate=\"no\">​</a></h3>\n<p>Centrifugo behaves a bit better in cluster mode: as soon as a node leaves a cluster gracefully (upon graceful termination) it sends a shutdown signal to the control channel thus giving other nodes a chance to immediately delete that node from the local registry.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"client-improvements\">Client improvements<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#client-improvements\" class=\"hash-link\" aria-label=\"Direct link to Client improvements\" title=\"Direct link to Client improvements\" translate=\"no\">​</a></h3>\n<p>While preparing the v3 release we improved client connectors too. All existing client connectors now actualized to the latest protocol, support server-side subscriptions, history API.</p>\n<p>One important detail is that it's not required to set <code>?format=protobuf</code> URL param now when connecting to Centrifugo from mobile devices - this is now managed internally by using the WebSocket subprotocol mechanism (requires using the latest client connector version and Centrifugo v3).</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"new-documentation-site\">New documentation site<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#new-documentation-site\" class=\"hash-link\" aria-label=\"Direct link to New documentation site\" title=\"Direct link to New documentation site\" translate=\"no\">​</a></h3>\n<p>You are reading this post on a new project site. It's built with amazing <a href=\"https://docusaurus.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Docusaurus</a>.</p>\n<p>A lot of documents were actualized, extended, and rewritten. We also now have new chapters like:</p>\n<ul>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/highlights\">Main highlights</a></li>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/getting-started/design\">Design overview</a></li>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/server/history_and_recovery\">History and recovery</a></li>\n<li class=\"\"><a class=\"\" href=\"https://centrifugal.dev/docs/server/codes\">Error and disconnect codes</a>.</li>\n</ul>\n<p>Server API and proxy documentation have been improved significantly.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"performance-improvements\">Performance improvements<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#performance-improvements\" class=\"hash-link\" aria-label=\"Direct link to Performance improvements\" title=\"Direct link to Performance improvements\" translate=\"no\">​</a></h3>\n<img src=\"https://centrifugal.dev/img/stopwatch.svg\" align=\"right\" width=\"25%\">\n<p>Centrifugo v3 has some notable performance improvements.</p>\n<p>JSON client protocol now utilizes a couple of libraries (<code>easyjson</code> for encoding and <code>segmentio/encoding</code> for unmarshaling). Actually we use a slightly customized version of <code>easyjson</code> library to achieve even faster performance than it provides out-of-the-box. Changes allowed to speed up JSON encoding and decoding up to 4-5x for small messages. For large payloads speed up can be even more noticeable – we observed up to 30x performance boost when serializing 5kb messages.</p>\n<p>For example, let's look at a JSON serialization benchmark result for 256 byte payload. Here is what we had before:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">Centrifugo v2 JSON encoding/decoding</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkMarshal-12              \t 5883 ns/op\t    1121 B/op\t    6 allocs/op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkMarshalParallel-12      \t 1009 ns/op\t    1121 B/op\t    6 allocs/op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkUnmarshal-12            \t 1717 ns/op\t    1328 B/op\t   16 allocs/op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkUnmarshalParallel-12    \t492.2 ns/op\t    1328 B/op\t   16 allocs/op</span><br></div></code></pre></div></div>\n<p>And what we have now with mentioned JSON optimizations:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockTitle_OeMC\">Centrifugo v3 JSON encoding/decoding</div><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkMarshal-12              \t 461.3 ns/op\t 928 B/op\t    3 allocs/op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkMarshalParallel-12      \t 250.6 ns/op\t 928 B/op\t    3 allocs/op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkUnmarshal-12            \t 476.5 ns/op\t 136 B/op\t    3 allocs/op</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">BenchmarkUnmarshalParallel-12    \t 107.2 ns/op\t 136 B/op\t    3 allocs/op</span><br></div></code></pre></div></div>\n<div class=\"theme-admonition theme-admonition-tip admonition_xJq3 alert alert--success\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z\"></path></svg></span>tip</div><div class=\"admonitionContent_BuS1\"><p>Centrifugo Protobuf protocol is still faster than JSON for encoding/decoding on a server-side.</p></div></div>\n<p>Of course, JSON encoding is only one part of Centrifugo – so you should not expect overall 4x performance improvement. But loaded setups should notice the difference and this should also be a good thing for reducing garbage collection pauses.</p>\n<p>Centrifugo inherited a couple of other improvements from the Centrifuge library.</p>\n<p>In-memory connection hub is now sharded – this should reduce lock contention between operations in different channels. In <a href=\"https://github.com/centrifugal/centrifuge/pull/184\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">our artificial benchmarks</a> we noticed a 3x better hub throughput, but in reality the benefit is heavily depends on the usage pattern.</p>\n<p>Centrifugo now allocates less during message broadcasting to a large number of subscribers.</p>\n<p>Also, an upgrade to Go 1.17 for builds results in ~5% performance boost overall, thanks to a new way of passing function arguments and results using registers instead of the stack introduced in Go 1.17.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"centrifugo-pro\">Centrifugo PRO<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#centrifugo-pro\" class=\"hash-link\" aria-label=\"Direct link to Centrifugo PRO\" title=\"Direct link to Centrifugo PRO\" translate=\"no\">​</a></h3>\n<p>The final notable thing is an introduction of Centrifugo PRO. This is an extended version of Centrifugo built on top of the OSS version. It provides some unique features targeting business adopters.</p>\n<p>Those who followed Centrifugo for a long time know that there were some attempts to make project development sustainable. Buy me a coffee and Opencollective approaches were not successful, during a year we got ~300$ of total contributions. While we appreciate these contributions a lot - this does not fairly justify a time spent on Centrifugo maintenance these days and does not allow bringing it to the next level. So here is an another attempt to monetize Centrifugo.</p>\n<p>Centrifugo PRO details and features described <a class=\"\" href=\"https://centrifugal.dev/docs/pro/overview\">here in docs</a>. Let's see how it goes. We believe that a set of additional functionality can provide great advantages for both small and large-scale Centrifugo setups. PRO features can give useful insights on a system, protect from client API misusing, reduce server resource usage, and more.</p>\n<p>PRO version will be released soon after Centrifugo v3 OSS.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h3>\n<p>There are some other changes introduced in v3 but not mentioned here. The full list can be found in the release notes and the migration guide.</p>\n<p>Hope we stepped into an exciting time of the v3 life cycle and many improvements will follow. Join our communities in Telegram and Discord if you have questions or want to follow Centrifugo development:</p>\n<p><a href=\"https://t.me/joinchat/ABFVWBE0AhkyyhREoaboXQ\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><img decoding=\"async\" loading=\"lazy\" src=\"https://img.shields.io/badge/Telegram-Group-orange?style=flat&amp;logo=telegram\" alt=\"Join the chat at https://t.me/joinchat/ABFVWBE0AhkyyhREoaboXQ\" class=\"img_ev3q\"></a> &nbsp;<a href=\"https://discord.gg/tYgADKx\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\"><img decoding=\"async\" loading=\"lazy\" src=\"https://img.shields.io/discord/719186998686122046?style=flat&amp;label=Discord&amp;logo=discord\" alt=\"Join the chat at https://discord.gg/tYgADKx\" class=\"img_ev3q\"></a></p>\n<p>Enjoy Centrifugo v3, and let the Centrifugal force be with you.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>Special thanks</div><div class=\"admonitionContent_BuS1\"><p>Special thanks to <a href=\"https://github.com/silischev\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Anton Silischev</a> for the help with v3 tests, examples and CI. To <a href=\"https://github.com/leeoniya\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Leon Sorokin</a> for the spinning CSS Centrifugo logo. To <a href=\"https://github.com/filonenko-mikhail\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Michael Filonenko</a> for the help with Tarantool. To <a href=\"https://github.com/mogol\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">German Saprykin</a> for Dart magic.</p><p>Thanks to the community members who tested out Centrifugo v3 beta, found bugs and sent improvements.</p><div>Icons used here made by <a href=\"https://www.flaticon.com/authors/wanicon\" title=\"wanicon\">wanicon</a> from <a href=\"https://www.flaticon.com/\" title=\"Flaticon\"></a><a href=\"http://www.flaticon.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">www.flaticon.com</a></div></div></div>",
            "url": "https://centrifugal.dev/blog/2021/08/31/hello-centrifugo-v3",
            "title": "Centrifugo v3 released",
            "summary": "Centrifugo v3 released with lots of exciting improvements",
            "date_modified": "2021-08-31T00:00:00.000Z",
            "author": {
                "name": "Centrifugal team"
            },
            "tags": [
                "centrifugo",
                "release"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2021/01/15/centrifuge-intro",
            "content_html": "<p>In this post I'll try to introduce <a href=\"https://github.com/centrifugal/centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifuge</a> - the heart of Centrifugo.</p>\n<p>Centrifuge is a real-time messaging library for the Go language.</p>\n<p>This post is going to be pretty long (looks like I am a huge fan of long reads) – so make sure you also have a drink (probably two) and let's go!</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"how-its-all-started\">How it's all started<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#how-its-all-started\" class=\"hash-link\" aria-label=\"Direct link to How it's all started\" title=\"Direct link to How it's all started\" translate=\"no\">​</a></h2>\n<p>I wrote several blog posts before (<a href=\"https://medium.com/@fzambia/four-years-in-centrifuge-ce7a94e8b1a8\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">for example this one</a> – yep, it's on Medium...) about an original motivation of <a href=\"https://github.com/centrifugal/centrifugo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo</a> server.</p>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span>danger</div><div class=\"admonitionContent_BuS1\"><p>Centrifugo server is not the same as Centrifuge library for Go. It's a full-featured project built on top of Centrifuge library. Naming can be confusing, but it's not too hard once you spend some time with ecosystem.</p></div></div>\n<p>In short – Centrifugo was implemented to help traditional web frameworks dealing with many persistent connections (like WebSocket or SockJS HTTP transports). So frameworks like Django or Ruby on Rails, or frameworks from the PHP world could be used on a backend but still provide real-time messaging features like chats, multiplayer browser games, etc for users. With a little help from Centrifugo.</p>\n<p>Now there are cases when Centrifugo server used in conjunction even with a backend written in Go. While Go mostly has no problems dealing with many concurrent connections – Centrifugo provides some features beyond simple message passing between a client and a server. That makes it useful, especially since design is pretty non-obtrusive and fits well microservices world. Centrifugo is used in some well-known projects (like ManyChat, Yoola.io, Spot.im, Badoo etc).</p>\n<p>At the end of 2018, I released Centrifugo v2 based on a real-time messaging library for Go language – Centrifuge – the subject of this post.</p>\n<p>It was a pretty hard experience to decouple Centrifuge out of the monolithic Centrifugo server – I was unable to make all the things right immediately, so Centrifuge library API went through several iterations where I introduced backward-incompatible changes. All those changes targeted to make Centrifuge a more generic tool and remove opinionated or limiting parts.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"so-what-is-centrifuge\">So what is Centrifuge?<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#so-what-is-centrifuge\" class=\"hash-link\" aria-label=\"Direct link to So what is Centrifuge?\" title=\"Direct link to So what is Centrifuge?\" translate=\"no\">​</a></h2>\n<p>This is ... well, a framework to build real-time messaging applications with Go language. If you ever heard about <a href=\"https://socket.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">socket.io</a> – then you can think about Centrifuge as an analogue. I think the most popular applications these days are chats of different forms, but I want to emphasize that Centrifuge is not a framework to build chats – it's a generic instrument that can be used to create different sorts of real-time applications – real-time charts, multiplayer games.</p>\n<p>The obvious choice for real-time messaging transport to achieve fast and cross-platform bidirectional communication these days is WebSocket. Especially if you are targeting a browser environment. You mostly don't need to use WebSocket HTTP polyfills in 2021 (though there are still corner cases so Centrifuge supports <a href=\"https://github.com/sockjs/sockjs-client\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">SockJS</a> polyfill).</p>\n<p>Centrifuge has its own custom protocol on top of plain WebSocket or SockJS frames.</p>\n<p>The reason why Centrifuge has its own protocol on top of underlying transport is that it provides several useful primitives to build real-time applications. The protocol <a href=\"https://github.com/centrifugal/protocol/blob/master/definitions/client.proto\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">described as strict Protobuf schema</a>. It's possible to pass JSON or binary Protobuf-encoded data over the wire with Centrifuge.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>GRPC is very handy these days too (and can be used in a browser with a help of additional proxies), some developers prefer using it for real-time messaging apps – especially when one-way communication needed. It can be a bit better from integration perspective but more resource-consuming on server side and a bit trickier to deploy.</p></div></div>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>Take a look at <a href=\"https://w3c.github.io/webtransport/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WebTransport</a> – a brand-new spec for web browsers to allow fast communication between a client and a server on top of QUIC – it may be a good alternative to WebSocket in the future. This in a draft status at the moment, but it's <a href=\"https://centrifugal.github.io/centrifugo/blog/quic_web_transport/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">already possible to play with in Chrome</a>.</p></div></div>\n<p>Own protocol is one of the things that prove the framework status of Centrifuge. This dictates certain limits (for example, you can't just use an alternative message encoding) and makes developers use custom client connectors on a front-end side to communicate with a Centrifuge-based server (see more about connectors in ecosystem part).</p>\n<p>But protocol solves many practical tasks – and here we are going to look at real-time features it provides for a developer.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"centrifuge-node\">Centrifuge Node<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#centrifuge-node\" class=\"hash-link\" aria-label=\"Direct link to Centrifuge Node\" title=\"Direct link to Centrifuge Node\" translate=\"no\">​</a></h2>\n<p>To start working with Centrifuge you need to start Centrifuge server Node. Node is a core of Centrifuge – it has many useful methods – set event handlers, publish messages to channels, etc. We will look at some events and channels concept very soon.</p>\n<p>Also, Node abstracts away scalability aspects, so you don't need to think about how to scale WebSocket connections over different server instances and still have a way to deliver published messages to interested clients.</p>\n<p>For now, let's start a single instance of Node that will serve connections for us:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">node</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">DefaultConfig</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> node</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Run</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>It's also required to serve a WebSocket handler – this is possible just by registering <code>centrifuge.WebsocketHandler</code> in HTTP mux:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">wsHandler </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewWebsocketHandler</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">node</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">WebsocketConfig</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Handle</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"/connection/websocket\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> wsHandler</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>Now it's possible to connect to a server (using Centrifuge connector for a browser called <code>centrifuge-js</code>):</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> centrifuge </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">Centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'ws://localhost:8000/connection/websocket'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">connect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Though connection will be rejected by the server since we also need to provide authentication details – Centrifuge expects explicitly provided connection <code>Credentials</code> to accept connection.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"authentication\">Authentication<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#authentication\" class=\"hash-link\" aria-label=\"Direct link to Authentication\" title=\"Direct link to Authentication\" translate=\"no\">​</a></h2>\n<p>Let's look at how we can tell Centrifuge details about connected user identity, so it could accept an incoming connection.</p>\n<p>There are two main ways to authenticate client connection in Centrifuge.</p>\n<p>The first one is over the native middleware mechanism. It's possible to wrap <code>centrifuge.WebsocketHandler</code> or <code>centrifuge.SockjsHandler</code> with middleware that checks user authentication and tells Centrifuge current user ID over <code>context.Context</code>:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">auth</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">h http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Handler</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Handler </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">HandlerFunc</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">w http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">ResponseWriter</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> r </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Request</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tcred </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Credentials</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tUserID</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"42\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tnewCtx </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">SetCredentials</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">r</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cred</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tr </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">WithContext</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">newCtx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\th</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">ServeHTTP</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">w</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> r</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>So WebsocketHandler can be registered this way (note that a handler now wrapped by auth middleware):</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">wsHandler </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewWebsocketHandler</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">node</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">WebsocketConfig</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">http</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Handle</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"/connection/websocket\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">auth</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">wsHandler</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>Another authentication way is a bit more generic – developers can authenticate connection based on custom token sent from a client inside first WebSocket/SockJS frame. This is called <code>connect</code> frame in terms of Centrifuge protocol. Any string token can be set – this opens a way to use JWT, Paceto, and any other kind of authentication tokens. For example <a href=\"https://github.com/centrifugal/centrifuge/tree/master/_examples/jwt_token\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">see an authenticaton with JWT</a>.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>BTW it's also possible to pass any information from client side with a first connect message from client to server and return custom information about server state to a client. This is out of post scope though.</p></div></div>\n<p>Nothing prevents you to <a href=\"https://github.com/centrifugal/centrifuge/tree/master/_examples/chat_oauth2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">integrate Centrifuge with OAuth2</a> or another framework session mechanism – <a href=\"https://github.com/centrifugal/centrifuge/tree/master/_examples/chat_oauth2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">like Gin for example</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"channel-subscriptions\">Channel subscriptions<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#channel-subscriptions\" class=\"hash-link\" aria-label=\"Direct link to Channel subscriptions\" title=\"Direct link to Channel subscriptions\" translate=\"no\">​</a></h2>\n<p>As soon as a client connected and successfully authenticated it can subscribe to channels. Channel (room or topic in other systems) is a lightweight and ephemeral entity in Centrifuge. Channel can have different features (we will look at some channel features below). Channels created automatically as soon as the first subscriber joins and destroyed as soon as the last subscriber left.</p>\n<p>The application can have many real-time features – even on one app screen. So sometimes client subscribes to several channels – each related to a specific real-time feature (for example one channel for chat updates, one channel likes notification stream, etc).</p>\n<p>Channel is just an ASCII string. A developer is responsible to find the best channel naming convention suitable for an application. Channel naming convention is an important aspect since in many cases developers want to authorize subscription to a channel on the server side – so only authorized users could listen to specific channel updates.</p>\n<p>Let's look at a basic subscription example on the client-side:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'example'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">msgCtx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">msgCtx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>On the server-side, you need to define subscribe event handler. If subscribe event handler not set then the connection won't be able to subscribe to channels at all. Subscribe event handler is where a developer may check permissions of the current connection to read channel updates. Here is a basic example of subscribe event handler that simply allows subscriptions to channel <code>example</code> for all authenticated connections and reject subscriptions to all other channels:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">node</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">OnConnect</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">client </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">OnSubscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">e centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeEvent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cb centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeCallback</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Channel </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"example\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">cb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeReply</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">ErrorPermissionDenied</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">cb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeReply</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>You may notice a callback style of reacting to connection related things. While not being very idiomatic for Go it's very practical actually. The reason why we use callback style inside client event handlers is that it gives a developer possibility to control operation concurrency (i.e. process sth in separate goroutines or goroutine pool) and still control the order of events. See <a href=\"https://github.com/centrifugal/centrifuge/tree/master/_examples/concurrency\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">an example</a> that demonstrates concurrency control in action.</p>\n<p>Now if some event published to a channel:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Here is how we can publish data to a channel.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">node</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Publish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"example\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`{\"input\": \"hello\"}`</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>– data will be delivered to a subscribed client, and message will be printed to Javascript console. PUB/SUB in its usual form.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>Though Centrifuge protocol based on Protobuf schema in example above we published a JSON message into a channel. By default, we can only send JSON to connections since default protocol format is JSON. But we can switch to Protobuf-based binary protocol by connecting to <code>ws://localhost:8000/connection/websocket?format=protobuf</code> endpoint – then it's possible to send binary data to clients.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"async-message-passing\">Async message passing<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#async-message-passing\" class=\"hash-link\" aria-label=\"Direct link to Async message passing\" title=\"Direct link to Async message passing\" translate=\"no\">​</a></h2>\n<p>While Centrifuge mostly shines when you need channel semantics it's also possible to send any data to connection directly – to achieve bidirectional asynchronous communication, just what a native WebSocket provides.</p>\n<p>To send a message to a server one can use the <code>send</code> method on the client-side:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">send</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string-property property\">\"input\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"hello\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>On the server-side data will be available inside a message handler:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">OnMessage</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">e centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">MessageEvent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Printf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"message from client: %s\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>And vice-versa, to send data to a client use <code>Send</code> method of <code>centrifuge.Client</code>:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Send</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`{\"input\": \"hello\"}`</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>To listen to it on the client-side:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'message'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"rpc\">RPC<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#rpc\" class=\"hash-link\" aria-label=\"Direct link to RPC\" title=\"Direct link to RPC\" translate=\"no\">​</a></h2>\n<p>RPC is a primitive for sending a request from a client to a server and waiting for a response (in this case all communication still happens via asynchronous message passing internally, but Centrifuge takes care of matching response data to request previously sent).</p>\n<p>On client side it's as simple as:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> resp </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">rpc</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'my_method'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>On server side RPC event handler should be set to make calls available:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">OnRPC</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">e centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">RPCEvent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cb centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">RPCCallback</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> e</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Method </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">==</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"my_method\"</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">cb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">RPCReply</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">Data</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`{\"result\": \"42\"}`</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">cb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">RPCReply</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">ErrorMethodNotFound</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>Note, that it's possible to pass the name of RPC and depending on it and custom request params return different results to a client – just like a regular HTTP request but over asynchronous WebSocket (or SockJS) connection.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"server-side-subscriptions\">Server-side subscriptions<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#server-side-subscriptions\" class=\"hash-link\" aria-label=\"Direct link to Server-side subscriptions\" title=\"Direct link to Server-side subscriptions\" translate=\"no\">​</a></h2>\n<p>In many cases, a client is a source of knowledge which channels it wants to subscribe to on a specific application screen. But sometimes you want to control subscriptions to channels on a server-side. This is also possible in Centrifuge.</p>\n<p>It's possible to provide a slice of channels to subscribe connection to at the moment of connection establishment phase:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">node</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">OnConnecting</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ctx context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> e centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">ConnectEvent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">ConnectReply</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">ConnectReply</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        Subscriptions</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeOptions</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"example\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>Note, that <code>OnConnecting</code> does not follow callback-style – this is because it can only happen once at the start of each connection – so there is no need to control operation concurrency.</p>\n<p>In this case on the client-side you will have access to messages published to channels by listening to <code>on('publish')</code> event:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">on</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'publish'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">function</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token parameter\">msgCtx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token console class-name\" style=\"color:rgb(255, 203, 107)\">console</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">msgCtx</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Also, <code>centrifuge.Client</code> has <code>Subscribe</code> and <code>Unsubscribe</code> methods so it's possible to subscribe/unsubscribe client to/from channel somewhere in the middle of its long WebSocket session.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"windowed-history-in-channel\">Windowed history in channel<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#windowed-history-in-channel\" class=\"hash-link\" aria-label=\"Direct link to Windowed history in channel\" title=\"Direct link to Windowed history in channel\" translate=\"no\">​</a></h2>\n<p>Every time a message published to a channel it's possible to provide custom history options. For example:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">node</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Publish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"example\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">`{\"input\": \"hello\"}`</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">WithHistory</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">300</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Minute</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>In this case, Centrifuge will maintain a windowed Publication cache for a channel - or in other words, maintain a publication stream. This stream will have time retention (one minute in the example above) and the maximum size will be limited to the value provided during Publish (300 in the example above).</p>\n<p>Every message inside a history stream has an incremental <code>offset</code> field. Also, a stream has a field called <code>epoch</code> – this is a unique identifier of stream generation - thus client will have a possibility to distinguish situations where a stream is completely removed and there is no guarantee that no messages have been lost in between even if offset looks fine.</p>\n<p>Client protocol provides a possibility to paginate over a stream from a certain position with a limit:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> streamPosition </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string-property property\">'offset'</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">epoch</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'xyz'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">resp </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword control-flow\" style=\"font-style:italic\">await</span><span class=\"token plain\"> sub</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token method function property-access\" style=\"color:rgb(130, 170, 255)\">history</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token literal-property property\">since</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> streamPosition</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token literal-property property\">limit</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">10</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>Iteration over history stream is a new feature which is just merged into Centrifuge master branch and can only be used from Javascript client at the moment.</p>\n<p>Also, Centrifuge has an automatic message recovery feature. Automatic recovery is very useful in scenarios when tons of persistent connections start reconnecting at once. I already described why this is useful in one of my previous posts about Websocket scalability. In short – since WebSocket connections are stateful then at the moment of mass reconnect they can create a very big spike in load on your main application database. Such mass reconnects are a usual thing in practice - for example when you reload your load balancers or re-deploying the Websocket server (new code version).</p>\n<p>Of course, recovery can also be useful for regular short network disconnects - when a user travels in the subway for example. But you always need a way to load an actual state from the main application database in case of an unsuccessful recovery.</p>\n<p>To enable automatic recovery you can provide the <code>Recover</code> flag in subscribe options:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">OnSubscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">e centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeEvent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cb centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeCallback</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">cb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeReply</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        Options</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeOptions</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            Recover</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">   </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>Obviously, recovery will work only for channels where history stream maintained. The limitation in recovery is that all missed publications sent to client in one protocol frame – pagination is not supported during recovery process. This means that recovery is mostly effective for not too long offline time without tons of missed messages.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"online-presence-and-presence-stats\">Online presence and presence stats<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#online-presence-and-presence-stats\" class=\"hash-link\" aria-label=\"Direct link to Online presence and presence stats\" title=\"Direct link to Online presence and presence stats\" translate=\"no\">​</a></h2>\n<p>Another cool thing Centrifuge exposes to developers is online presence information for channels. Presence information contains a list of active channel subscribers. This is useful to show the online status of players in a game for example.</p>\n<p>Also, it's possible to turn on Join/Leave message feature inside channels: so each time connection subscribes to a channel all channel subscribers receive a Join message with client information (client ID, user ID). As soon as the client unsubscribes Leave message is sent to remaining channel subscribers with information who left a channel.</p>\n<p>Here is how to enable both online presence and join/leave features for a subscription to channel:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">client</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">OnSubscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">e centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeEvent</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> cb centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeCallback</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">cb</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeReply</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        Options</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> centrifuge</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">SubscribeOptions</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            Presence</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">   </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            JoinLeave</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">  </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>On a client-side then it's possible to call for the presence and setting event handler for join/leave messages.</p>\n<p>The important thing to be aware of when using Join/Leave messages is that this feature can dramatically increase CPU utilization and overall traffic in channels with a big number of active subscribers – since on every client connect/disconnect event such Join or Leave message must be sent to all subscribers. The advice here – avoid using Join/Leave messages or be ready to scale (Join/Leave messages scale well when adding more Centrifuge Nodes – more about scalability below).</p>\n<p>One more thing to remember is that online presence information can also be pretty expensive to request in channels with many active subscribers – since it returns information about all connections – thus payload in response can be large. To help a bit with this situation Centrifuge has a presence stats client API method. Presence stats only contain two counters: the number of active connections in the channel and amount of unique users in the channel.</p>\n<p>If you still need to somehow process online presence in rooms with a massive number of active subscribers – then I think you better do it in near real-time - for example with fast OLAP like <a href=\"https://clickhouse.tech/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">ClickHouse</a>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"scalability-aspects\">Scalability aspects<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#scalability-aspects\" class=\"hash-link\" aria-label=\"Direct link to Scalability aspects\" title=\"Direct link to Scalability aspects\" translate=\"no\">​</a></h2>\n<p>To be fair it's not too hard to implement most of the features above inside one in-memory process. Yes, it takes time, but the code is mostly straightforward. When it comes to scalability things tend to be a bit harder.</p>\n<p>Centrifuge designed with the idea in mind that one machine is not enough to handle all application WebSocket connections. Connections should scale over application backend instances, and it should be simple to add more application nodes when the amount of users (connections) grows.</p>\n<p>Centrifuge abstracts scalability over the <code>Node</code> instance and two interfaces: <code>Broker</code> interface and <code>PresenceManager</code> interface.</p>\n<p>A broker is responsible for PUB/SUB and streaming semantics:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> Broker </span><span class=\"token keyword\" style=\"font-style:italic\">interface</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Run</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">BrokerEventHandler</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Subscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Unsubscribe</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Publish</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> data </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> opts PublishOptions</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">StreamPosition</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">PublishJoin</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> info </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">ClientInfo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">PublishLeave</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> info </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">ClientInfo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">PublishControl</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">data </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> nodeID </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">History</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> filter HistoryFilter</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">Publication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> StreamPosition</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">RemoveHistory</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>See <a href=\"https://github.com/centrifugal/centrifuge/blob/v0.14.2/engine.go#L98\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">full version with comments</a> in source code.</p>\n<p>Every Centrifuge Node subscribes to channels via a broker. This provides a possibility to scale connections over many node instances – published messages will flow only to nodes with active channel subscribers.</p>\n<p>It's and important thing to combine PUB/SUB with history inside a Broker implementation to achieve an atomicity of saving message into history stream and publishing it to PUB/SUB with generated offset.</p>\n<p>PresenceManager is responsible for online presence information management:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> PresenceManager </span><span class=\"token keyword\" style=\"font-style:italic\">interface</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Presence</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token keyword\" style=\"font-style:italic\">map</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">ClientInfo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">PresenceStats</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">PresenceStats</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">AddPresence</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> clientID </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> info </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">ClientInfo</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> expire time</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Duration</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">RemovePresence</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ch </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> clientID </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p><a href=\"https://github.com/centrifugal/centrifuge/blob/v0.14.2/engine.go#L150\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Full code with comments</a>.</p>\n<p><code>Broker</code> and <code>PresenceManager</code> together form an <code>Engine</code> interface:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> Engine </span><span class=\"token keyword\" style=\"font-style:italic\">interface</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tBroker</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tPresenceManager</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>By default, Centrifuge uses <code>MemoryEngine</code> that does not use any external services but limits developers to using only one Centrifuge Node (i.e. one server instance). Memory Engine is fast and can be suitable for some scenarios - even in production (with configured backup instance) – but as soon as the number of connections grows – you may need to load balance connections to different server instances. Here comes the Redis Engine.</p>\n<p>Redis Engine utilizes Redis for Broker and PresenceManager parts.</p>\n<p>History cache saved to Redis STREAM or Redis LIST data structures. For presence, Centrifuge uses a combination of HASH and ZSET structures.</p>\n<p>Centrifuge tries to fully utilize the connection between Node and Redis by using pipelining where possible and smart batching technique. All operations done in a single RTT with the help of Lua scripts loaded automatically to Redis on engine start.</p>\n<p>Redis is pretty fast and will allow your app to scale to some limits. When Redis starts being a bottleneck it's possible to shard data over different Redis instances. Client-side consistent sharding is built-in in Centrifuge and allows scaling further.</p>\n<p>It's also possible to achieve Redis's high availability with built-in Sentinel support. Redis Cluster supported too. So Redis Engine covers many options to communicate with Redis deployed in different ways.</p>\n<p>At Avito we served about 800k active connections in the messenger app with ease using a slightly adapted Centrifuge Redis Engine, so an approach proved to be working for rather big applications. We will look at some more concrete numbers below in the performance section.</p>\n<p>Both <code>Broker</code> and <code>PresenceManager</code> are pluggable, so it's possible to replace them with alternative implementations. Examples show <a href=\"https://github.com/centrifugal/centrifuge/tree/master/_examples/custom_broker_nats\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">how to use Nats server</a> for at most once only PUB/SUB together with Centrifuge. Also, we have <a href=\"https://github.com/centrifugal/centrifuge/tree/master/_examples/custom_engine_tarantool\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">an example of full-featured Engine for Tarantool database</a> – Tarantool Engine shows even better throughput for history and presence operations than Redis-based Engine (up to 10x for some ops).</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"order-and-delivery-properties\">Order and delivery properties<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#order-and-delivery-properties\" class=\"hash-link\" aria-label=\"Direct link to Order and delivery properties\" title=\"Direct link to Order and delivery properties\" translate=\"no\">​</a></h2>\n<p>Since Centrifuge is a messaging system I also want to describe its order and message delivery guarantees.</p>\n<p>Message ordering in channels supported. As soon as you publish messages into channels one after another of course.</p>\n<p>Message delivery model is at most once by default. This is mostly comes from PUB/SUB model – message can be dropped on Centrifuge level if subscriber is offline or simply on broker level – since Redis PUB/SUB also works with at most once guarantee.</p>\n<p>Though if you maintain history stream inside a channel then things become a bit different. In this case you can tell Centrifuge to check client position inside stream. Since every publication has a unique incremental offset Centrifuge can track that client has correct offset inside a channel stream. If Centrifuge detects any missed messages it disconnects a client with special code – thus make it reconnect and recover messages from history stream. Since a message first saved to history stream and then published to PUB/SUB inside broker these mechanisms allow achieving at least once message delivery guarantee.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/PLb9xS5.jpg\" alt=\"What happens on publish\" class=\"img_ev3q\"></p>\n<p>Even if stream completely expired or dropped from broker memory Centrifuge will give a client a tip that messages could be lost – so client has a chance to restore state from a main application database.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"ecosystem\">Ecosystem<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#ecosystem\" class=\"hash-link\" aria-label=\"Direct link to Ecosystem\" title=\"Direct link to Ecosystem\" translate=\"no\">​</a></h2>\n<p>Here I want to be fair with my readers – Centrifuge is not ideal. This is a project maintained mostly by one person at the moment with all consequences. This hits an ecosystem a lot, can make some design choices opinionated or non-optimal.</p>\n<p>I mentioned in the first post that Centrifuge built on top of the custom protocol. The protocol is based on a strict Protobuf schema, works with JSON and binary data transfer, supports many features. But – this means that to connect to the Centrifuge-based server developers have to use custom connectors that can speak with Centrifuge over its custom protocol.</p>\n<p>The difficulty here is that protocol is asynchronous. Asynchronous protocols are harder to implement than synchronous ones. Multiplexing frames allows achieving good performance and fully utilize a single connection – but it hurts simplicity.</p>\n<p>At this moment Centrifuge has client connectors for:</p>\n<ul>\n<li class=\"\"><a href=\"https://github.com/centrifugal/centrifuge-js\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifuge-js</a> - Javascript client for a browser, NodeJS and React Native</li>\n<li class=\"\"><a href=\"https://github.com/centrifugal/centrifuge-go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifuge-go</a> - for Go language</li>\n<li class=\"\"><a href=\"https://github.com/centrifugal/centrifuge-mobile\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifuge-mobile</a> - for mobile development based on centrifuge-go and <a href=\"https://github.com/golang/mobile\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">gomobile</a> project</li>\n<li class=\"\"><a href=\"https://github.com/centrifugal/centrifuge-swift\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifuge-swift</a> - for iOS native development</li>\n<li class=\"\"><a href=\"https://github.com/centrifugal/centrifuge-java\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifuge-java</a> - for Android native development and general Java</li>\n<li class=\"\"><a href=\"https://github.com/centrifugal/centrifuge-dart\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">centrifuge-dart</a> - for Dart and Flutter</li>\n</ul>\n<p>Not all clients support all protocol features. Another drawback is that all clients do not have a persistent maintainer – I mostly maintain everything myself. Connectors can have non-idiomatic and pretty dumb code since I had no previous experience with mobile development, they lack proper tests and documentation. This is unfortunate.</p>\n<p>The good thing is that all connectors feel very similar, I am quickly releasing new versions when someone sends a pull request with improvements or bug fixes. So all connectors are alive.</p>\n<p>I maintain a feature matrix in connectors to let users understand what's supported. Actually feature support is pretty nice throughout all these connectors - there are only several things missing and not so much work required to make all connectors full-featured. But I really need help here.</p>\n<p>It will be a big mistake to not mention Centrifugo as a big plus for Centrifuge library ecosystem. Centrifugo is a server deployed in many projects throughout the world. Many features of Centrifuge library and its connectors have already been tested by Centrifugo users.</p>\n<p>One more thing to mention is that Centrifuge does not have v1 release. It still evolves – I believe that the most dramatic changes have already been made and backward compatibility issues will be minimal in the next releases – but can't say for sure.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"performance\">Performance<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#performance\" class=\"hash-link\" aria-label=\"Direct link to Performance\" title=\"Direct link to Performance\" translate=\"no\">​</a></h2>\n<p>I made a test stand in Kubernetes with one million connections.</p>\n<p>I can't call this a proper benchmark – since in a benchmark your main goal is to destroy a system, in my test I just achieved some reasonable numbers on limited hardware. These numbers should give a good insight into a possible throughput, latency, and estimate hardware requirements (at least approximately).</p>\n<p>Connections landed on different server pods, 5 Redis instances have been used to scale connections between pods.</p>\n<p>The detailed test stand description <a href=\"https://centrifugal.github.io/centrifugo/misc/benchmark/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">can be found in Centrifugo documentation</a>.</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"Benchmark\" src=\"https://centrifugal.dev/assets/images/benchmark-b670972866abdc8936d2aef84333dd0c.gif\" width=\"1860\" height=\"916\" class=\"img_ev3q\"></p>\n<p>Some quick conclusions are:</p>\n<ul>\n<li class=\"\">One connection costs about 30kb of RAM</li>\n<li class=\"\">Redis broker CPU utilization increases linearly with more messages traveling around</li>\n<li class=\"\">1 million connections with 500k <strong>delivered</strong> messages per second with 200ms delivery latency in 99 percentile can be served with hardware amount equal to one modern physical server machine. The possible amount of messages can vary a lot depending on the number of channel subscribers though.</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"limitations\">Limitations<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#limitations\" class=\"hash-link\" aria-label=\"Direct link to Limitations\" title=\"Direct link to Limitations\" translate=\"no\">​</a></h2>\n<p>Centrifuge does not allow subscribing on the same channel twice inside a single connection. It's not simple to add due to design decisions made – though there was no single user report about this in seven years of Centrifugo/Centrifuge history.</p>\n<p>Centrifuge does not support wildcard subscriptions. Not only because I never needed this myself but also due to some design choices made – so be aware of this.</p>\n<p>SockJS fallback does not support binary data - only JSON. If you want to use binary in your application then you can only use WebSocket with Centrifuge - there is no built-in fallback transport in this case.</p>\n<p>SockJS also requires sticky session support from your load balancer to emulate a stateful bidirectional connection with its HTTP fallback transports. Ideally, Centrifuge will go away from SockJS at some point, maybe when WebTransport becomes mature so users will have a choice between WebTransport or WebSocket.</p>\n<p>Websocket <code>permessage-deflate</code> compression supported (thanks to Gorilla WebSocket), but it can be pretty expensive in terms of CPU utilization and memory usage – the overhead depends on usage pattern, it's pretty hard to estimate in numbers.</p>\n<p>As said above you cannot only rely on Centrifuge for state recovery – it's still required to have a way to fully load application state from the main database.</p>\n<p>Also, I am not very happy with current error and disconnect handling throughout the connector ecosystem – this can be improved though, and I have some ideas for the future.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"examples\">Examples<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#examples\" class=\"hash-link\" aria-label=\"Direct link to Examples\" title=\"Direct link to Examples\" translate=\"no\">​</a></h2>\n<p>I am adding examples to <a href=\"https://github.com/centrifugal/centrifuge/tree/master/_examples\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">_examples</a> folder of Centrifuge repo. These examples completely cover Centrifuge API - including things not mentioned here.</p>\n<p>Check out the <a href=\"https://github.com/centrifugal/centrifuge#tips-and-tricks\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">tips &amp; tricks</a> section of README – it contains some additional insights about an implementation.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2021/01/15/centrifuge-intro#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>I think <a href=\"https://github.com/centrifugal/centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifuge</a> could be a nice alternative to <a href=\"https://socket.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">socket.io</a> - with a better performance, main server implementation in Go language, and even more builtin features to build real-time apps.</p>\n<p>Centrifuge ecosystem definitely needs more work, especially in client connectors area, tutorials, community, stabilizing API, etc.</p>\n<p>Centrifuge fits pretty well proprietary application development where time matters and deadlines are close, so developers tend to choose a ready solution instead of writing their own. I believe Centrifuge can be a great time saver here.</p>\n<p>For Centrifugo server users Centrifuge package provides a way to write a more flexible server code adapted for business requirements but still use the same real-time core and have the same protocol features.</p>",
            "url": "https://centrifugal.dev/blog/2021/01/15/centrifuge-intro",
            "title": "Centrifuge – real-time messaging with Go",
            "summary": "An introduction to Centrifuge – real-time messaging with Go",
            "date_modified": "2021-01-15T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "centrifuge",
                "go"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2020/11/12/scaling-websocket",
            "content_html": "<p>I believe that in 2020 WebSocket is still an entertaining technology which is not so well-known and understood like HTTP. In this blog post I'd like to tell about state of WebSocket in Go language ecosystem, and a way we could write scalable WebSocket servers with Go and beyond Go.</p>\n<p>We won't talk a lot about WebSocket transport pros and cons – I'll provide links to other resources on this topic. Most advices here are generic enough and can be easily approximated to other programming languages. Also in this post we won't talk about ready to use solutions (if you are looking for it – check out <a href=\"https://www.leggetter.co.uk/real-time-web-technologies-guide/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Real-time Web Technologies guide</a> by Phil Leggetter), just general considerations. There is not so much information about scaling WebSocket on the internet so if you are interested in WebSocket and real-time messaging technologies - keep on reading.</p>\n<p>If you don't know what WebSocket is – check out the following curious links:</p>\n<ul>\n<li class=\"\"><a href=\"https://hpbn.co/websocket/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://hpbn.co/websocket/</a> – a wonderful chapter of great book by Ilya Grigorik</li>\n<li class=\"\"><a href=\"https://lucumr.pocoo.org/2012/9/24/websockets-101/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://lucumr.pocoo.org/2012/9/24/websockets-101/</a> – valuable thoughts about WebSocket from Armin Ronacher</li>\n</ul>\n<p>As soon as you know WebSocket basics – we can proceed.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"websocket-server-tasks\">WebSocket server tasks<a href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#websocket-server-tasks\" class=\"hash-link\" aria-label=\"Direct link to WebSocket server tasks\" title=\"Direct link to WebSocket server tasks\" translate=\"no\">​</a></h2>\n<p>Speaking about scalable servers that work with many persistent WebSocket connections – I found several important tasks such a server should be able to do:</p>\n<ul>\n<li class=\"\">Maintain many active connections</li>\n<li class=\"\">Send many messages to clients</li>\n<li class=\"\">Support WebSocket fallback to scale to every client</li>\n<li class=\"\">Authenticate incoming connections and invalidate connections</li>\n<li class=\"\">Survive massive reconnect of all clients without loosing messages</li>\n</ul>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>Of course not all of these points equally important in various situations.</p></div></div>\n<p>Below we will look at some tips which relate to these points.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/4lYjJSP.png\" alt=\"one_hour_scale\" class=\"img_ev3q\"></p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"websocket-libraries\">WebSocket libraries<a href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#websocket-libraries\" class=\"hash-link\" aria-label=\"Direct link to WebSocket libraries\" title=\"Direct link to WebSocket libraries\" translate=\"no\">​</a></h2>\n<p>In Go language ecosystem we have several libraries which can be used as a building block for a WebSocket server.</p>\n<p>Package <a href=\"https://godoc.org/golang.org/x/net/websocket\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">golang.org/x/net/websocket</a> is considered <strong>deprecated</strong>.</p>\n<p>The default choice in the community is <a href=\"https://github.com/gorilla/websocket\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">gorilla/websocket</a> library. Made by Gary Burd (who also gifted us an awesome <a href=\"https://github.com/gomodule/redigo\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Redigo</a> package to communicate with Redis) – it's widely used, performs well, has a very good API – so in most cases you should go with it. Some people think that library not actively maintained at moment – but this is not quite true, it implements full WebSocket RFC, so actually it can be considered done.</p>\n<p>In 2018 my ex-colleague Sergey Kamardin open-sourced <a href=\"https://github.com/gobwas/ws\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">gobwas/ws</a> library. It provides a bit lower-level API than <code>gorilla/websocket</code> thus allows reducing RAM usage per connection and has nice optimizations for WebSocket upgrade process. It does not support WebSocket <code>permessage-deflate</code> compression but otherwise a good alternative you can consider using. If you have not read Sergey's famous post <a href=\"https://www.freecodecamp.org/news/million-websockets-and-go-cc58418460bb/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">A Million WebSockets and Go</a> – make a bookmark!</p>\n<p>One more library is <a href=\"https://github.com/nhooyr/websocket\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">nhooyr/websocket</a>. It's the youngest one and actively maintained. It compiles to WASM which can be a cool thing for someone. The API is a bit different from what <code>gorilla/websocket</code> offers, and one of the big advantages I see is that it solves a problem with a proper WebSocket closing handshake which is <a href=\"https://github.com/gorilla/websocket/issues/448\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">a bit hard to do right with Gorilla WebSocket</a>.</p>\n<p>You can consider all listed libraries except one from <code>x/net</code> for your project. Take a library, follow its examples (make attention to goroutine-safety of various API operations). Personally I prefer Gorilla WebSocket at moment since it's feature-complete and battle tested by tons of projects around Go world.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"os-tuning\">OS tuning<a href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#os-tuning\" class=\"hash-link\" aria-label=\"Direct link to OS tuning\" title=\"Direct link to OS tuning\" translate=\"no\">​</a></h2>\n<p>OK, so you have chosen a library and built a server on top of it. As soon as you put it in production the interesting things start happening.</p>\n<p>Let's start with several OS specific key things you should do to prepare for many connections from WebSocket clients.</p>\n<p>Every connection will cost you an open file descriptor, so you should tune a maximum number of open file descriptors your process can use. An errors like <code>too many open files</code> raise due to OS limit on file descriptors which is usually 256-1024 by default (see with <code>ulimit -n</code> on Unix). A nice overview on how to do this on different systems can be found <a href=\"https://docs.riak.com/riak/kv/2.2.3/using/performance/open-files-limit.1.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">in Riak docs</a>. Wanna more connections? Make this limit higher.</p>\n<p>Nice tip here is to limit a maximum number of connections your process can serve – making it less than known file descriptor limit:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">//&nbsp;ulimit&nbsp;-n&nbsp;==&nbsp;65535</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> conns</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Len</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&gt;=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">65500</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> errors</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"connection&nbsp;limit&nbsp;reached\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">conns</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Add</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">conn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><br></div></code></pre></div></div>\n<p>– otherwise you have a risk to not even able to look at <code>pprof</code> when things go bad. And you always need monitoring of open file descriptors.</p>\n<p>You can also consider using <a href=\"https://godoc.org/golang.org/x/net/netutil#LimitListener\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">netutil.LimitListener</a> for this task, but don't forget to put pprof on another port with another HTTP server instance in this case.</p>\n<p>Keep attention on <em>Ephemeral ports</em> problem which is often happens between your load balancer and your WebSocket server. The problem arises due to the fact that each TCP connection uniquely identified in the OS by the 4-part-tuple:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">source ip | source port | destination ip | destination port</span><br></div></code></pre></div></div>\n<p>On balancer/server boundary you are limited in 65536 possible variants by default. But actually due to some OS limits and sockets in TIME_WAIT state the number is even less. A very good explanation and how to deal with it can be found <a href=\"https://making.pusher.com/ephemeral-port-exhaustion-and-how-to-avoid-it/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">in Pusher blog</a>.</p>\n<p>Your possible number of connections also limited by conntrack table. Netfilter framework which is part of iptables keeps information about all connections and has limited size for this information. See how to see its limits and instructions to increase <a href=\"https://morganwu277.github.io/2018/05/26/Solve-production-issue-of-nf-conntrack-table-full-dropping-packet/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">in this article</a>.</p>\n<p>One more thing you can do is tune your network stack for performance. Do this only if you understand that you need it. Maybe start <a href=\"https://gist.github.com/mustafaturan/47268d8ad6d56cadda357e4c438f51ca\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">with this gist</a>, but don't optimize without full understanding why you are doing this.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"sending-many-messages\">Sending many messages<a href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#sending-many-messages\" class=\"hash-link\" aria-label=\"Direct link to Sending many messages\" title=\"Direct link to Sending many messages\" translate=\"no\">​</a></h2>\n<p>Now let's speak about sending many messages. The general tips follows.</p>\n<p><strong>Make payload smaller</strong>. This is obvious – fewer data means more effective work on all layers. BTW WebSocket framing overhead is minimal and adds only 2-8 bytes to your payload. You can read detailed dedicated research in <a href=\"https://crossbario.com/blog/Dissecting-Websocket-Overhead/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Dissecting WebSocket's Overhead</a> article. You can reduce an amount of data traveling over network with <code>permessage-deflate</code> WebSocket extension, so your data will be compressed. Though using <code>permessage-deflate</code> is not always a good thing for server due to <a href=\"https://github.com/gorilla/websocket/issues/203\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">poor performance of flate</a>, so you should be prepared for a CPU and RAM resource usage on server side. While Gorilla WebSocket has a lot of optimizations internally by reusing flate writers, overhead is still noticeable. The increase value heavily depends on your load profile.</p>\n<p><strong>Make less system calls</strong>. Every syscall will have a constant overhead, and actually in WebSocket server under load you will mostly see read and write system calls in your CPU profiles. An advice here – try to use client-server protocol that supports message batching, so you can join individual messages together.</p>\n<p><strong>Use effective message serialization protocol</strong>. Maybe use code generation for JSON to avoid extensive usage of reflect package done by Go std lib. Maybe use sth like <a href=\"https://github.com/gogo/protobuf\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">gogo/protobuf</a> package which allows to speedup Protobuf marshalling and unmarshalling. Unfortunately Gogo Protobuf <a href=\"https://github.com/gogo/protobuf/issues/691\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">is going through hard times\n</a> at this moment. Try to serialize a message only once when sending to many subscribers.</p>\n<p><strong>Have a way to scale to several machines</strong> - more power, more possible messages. We will talk about this very soon.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"websocket-fallback-transport\">WebSocket fallback transport<a href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#websocket-fallback-transport\" class=\"hash-link\" aria-label=\"Direct link to WebSocket fallback transport\" title=\"Direct link to WebSocket fallback transport\" translate=\"no\">​</a></h2>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/IAOyvmg.png\" alt=\"ie\" class=\"img_ev3q\"></p>\n<p>Even in 2020 there are still users which cannot establish connection with WebSocket server. Actually the problem mostly appears with browsers. Some users still use old browsers. But they have a choice – install a newer browser. Still, there could also be users behind corporate proxies. Employees can have a trusted certificate installed on their machine so company proxy can re-encrypt even TLS traffic. Also, some browser extensions can block WebSocket traffic.</p>\n<p>One ready solution to this is <a href=\"https://github.com/igm/sockjs-go/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Sockjs-Go</a> library. This is a mature library that provides fallback transport for WebSocket. If client does not succeed with WebSocket connection establishment then client can use some of HTTP transports for client-server communication: <a href=\"https://hpbn.co/server-sent-events-sse/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">EventSource aka Server-Sent Events</a>, XHR-streaming, Long-Polling etc. The downside with those transports is that to achieve bidirectional communication you should use sticky sessions on your load balancer since SockJS keeps connection session state in process memory. We will talk about many instances of your WebSocket server very soon.</p>\n<p>You can implement WebSocket fallback yourself, this should be simple if you have a sliding window message stream on your backend which we will discuss very soon.</p>\n<p>Maybe look at <a href=\"https://grpc.io/docs/what-is-grpc/introduction/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">GRPC</a>, depending on application it could be better or worse than WebSocket – in general you can expect a better performance and less resource consumption from WebSocket for bidirectional communication case. My measurements for a <strong>bidirectional</strong> scenario showed 3x win for WebSocket (binary + GOGO protobuf) in terms of server CPU consumption and 4 times less RAM per connection. Though if you only need RPC then GRPC can be a better choice. But you need additional proxy to work with GRPC from a browser.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"performance-is-not-scalability\">Performance is not scalability<a href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#performance-is-not-scalability\" class=\"hash-link\" aria-label=\"Direct link to Performance is not scalability\" title=\"Direct link to Performance is not scalability\" translate=\"no\">​</a></h2>\n<p>You can optimize client-server protocol, tune your OS, but at some point you won't be able to use only one process on one server machine. You need to scale connections and work your server does over different server machines. Horizontal scaling is also good for a server high availability. Actually there are some sort of real-time applications where a single isolated process makes sense - for example multiplayer games where limited number of players play independent game rounds.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/8ElqpjI.png\" alt=\"many_instances\" class=\"img_ev3q\"></p>\n<p>As soon as you distribute connections over several machines you have to find a way to deliver a message to a certain user. The basic approach here is to publish messages to all server instances. This can work but this does not scale well. You need a sort of instance discovery to make this less painful.</p>\n<p>Here comes PUB/SUB, where you can connect WebSocket server instances over central PUB/SUB broker. Clients that establish connections with your WebSocket server subscribe to topics (channels) in a broker, and as soon as you publish a message to that topic it will be delivered to all active subscribers on WebSocket server instances. If server node does not have interested subscriber then it won't get a message from a broker thus you are getting effective network communication.</p>\n<p>Actually the main picture of this post illustrates exactly this architecture:</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/QOJ1M9a.png\" alt=\"gopher-broker\" class=\"img_ev3q\"></p>\n<p>Let's think about requirements for a broker for real-time messaging application. We want a broker:</p>\n<ul>\n<li class=\"\">with reasonable performance and possibility to scale</li>\n<li class=\"\">which maintains message order in topics</li>\n<li class=\"\">can support millions of topics, where each topic should be ephemeral and lightweight – topics can be created when user comes to application and removed after user goes away</li>\n<li class=\"\">possibility to keep a sliding window of messages inside channel to help us survive massive reconnect scenario (will talk about this later below, can be a separate part from broker actually)</li>\n</ul>\n<p>Personally when we talk about such brokers here are some options that come into my mind:</p>\n<ul>\n<li class=\"\"><a href=\"https://www.rabbitmq.com/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">RabbitMQ</a></li>\n<li class=\"\"><a href=\"https://kafka.apache.org/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Kafka</a> or <a href=\"https://pulsar.apache.org/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Pulsar</a></li>\n<li class=\"\"><a href=\"https://nats.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Nats or Nats-Streaming</a></li>\n<li class=\"\"><a href=\"https://www.tarantool.io/en/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Tarantool</a></li>\n<li class=\"\"><a href=\"https://redis.io/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Redis</a></li>\n</ul>\n<p><strong>Sure there are more exist</strong> including libraries like <a href=\"https://zeromq.org/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">ZeroMQ</a> or <a href=\"https://nanomsg.org/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">nanomsg</a>.</p>\n<p>Below I'll try to consider these solutions for the task of making scalable WebSocket server facing many user connections from Internet.</p>\n<p>If you are looking for unreliable at most once PUB/SUB then any of solutions mentioned above should be sufficient. Many real-time messaging apps are ok with at most once guarantee delivery.</p>\n<p>If you don't want to miss messages then things are a bit harder. Let's try to evaluate these options for a task where application has lots of different topics from which it wants to receive messages with at least once guarantee (having a personal topic per client is common thing in applications). A short analysis below can be a bit biased, but I believe thoughts are reasonable enough. I did not found enough information on the internet about scaling WebSocket beyond a single server process, so I'll try to fill the gap a little based on my personal knowledge without pretending to be absolutely objective in these considerations.</p>\n<p>In some posts on the internet about scaling WebSocket I saw advices to use RabbitMQ for PUB/SUB stuff in real-time messaging server. While this is a great messaging server, it does not like a high rate of queue bind and unbind type of load. It will work, but you will need to use a lot of server resources for not so big number of clients (imagine having millions of queues inside RabbitMQ). I have an example from my practice where RabbitMQ consumed about 70 CPU cores to serve real-time messages for 100k online connections. After replacing it with Redis keeping the same message delivery semantics we got only 0.3 CPU consumption on broker side.</p>\n<p>Kafka and Pulsar are great solutions, but not for this task I believe. The problem is again in dynamic ephemeral nature of our topics. Kafka also likes a more stable configuration of its topics. Keeping messages on disk can be an overkill for real-time messaging task. Also your consumers on Kafka server should pull from millions of different topics, not sure how well it performs, but my thoughts at moment - this should not perform very well. Kafka itself scales perfectly, you will definitely be able to achieve a goal but resource usage will be significant. Here is <a href=\"https://tech.trello.com/why-we-chose-kafka/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">a post from Trello</a> where they moved from RabbitMQ to Kafka for similar real-time messaging task and got about 5x resource usage improvements. Note also that the more partitions you have the more heavy failover process you get.</p>\n<p>Nats and Nats-Streaming. Raw Nats can only provide at most once guarantee. BTW recently Nats developers <a href=\"https://github.com/nats-io/nats-server/issues/315\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">released native WebSocket support</a>, so you can consider it for your application. Nats-Streaming server as broker will allow you to not lose messages. To be fair I don't have enough information about how well Nats-Streaming scales to millions of topics. An upcoming <a href=\"https://github.com/nats-io/jetstream\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Jetstream</a> which will be a part of Nats server can also be an interesting option – like Kafka it provides a persistent stream of messages for at least once delivery semantics. But again, it involves disk storage, a nice thing for backend microservices communication but can be an overkill for real-time messaging task.</p>\n<p>Sure Tarantool can fit to this task well too. It's fast, im-memory and flexible. Some possible problems with Tarantool are not so healthy state of its client libraries, complexity and the fact that it's heavily enterprise-oriented. You should invest enough time to benefit from it, but this can worth it actually. See <a href=\"https://hackernoon.com/tarantool-when-it-takes-500-lines-of-code-to-notify-a-million-users-11d340523493\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">an article</a> on how to do a performant broker for WebSocket applications with Tarantool.</p>\n<p>Building PUB/SUB system on top of ZeroMQ will require you to build separate broker yourself. This could be an unnecessary complexity for your system. It's possible to implement PUB/SUB pattern with ZeroMQ and nanomsg without a central broker, but in this case messages without active subscribers on a server will be dropped on a consumer side thus all publications will travel to all server nodes.</p>\n<p>My personal choice at moment is Redis. While <strong>Redis PUB/SUB itself provides at most once guarantee</strong>, you can build at least once delivery on top of PUB/SUB and Redis data structures (though this can be challenging enough). Redis is very fast (especially when using pipelining protocol feature), and what is more important – <strong>very predictable</strong>. It gives you a good understanding of operation time complexity. You can shard topics over different Redis instances running in HA setup - with Sentinel or with Redis Cluster. It allows writing LUA procedures with some advanced logic which can be uploaded over client protocol thus feels like ordinary commands. You can use Redis to keep sliding window event stream which gives you access to missed messages from a certain position. We will talk about this later.</p>\n<p>OK, the end of opinionated thoughts here :)</p>\n<p>Depending on your choice the implementation of your system will vary and will have different properties – so try to evaluate possible solutions based on your application requirements. Anyway, whatever broker will be your choice, try to follow this rules to build effective PUB/SUB system:</p>\n<ul>\n<li class=\"\">take into account message delivery guarantees of your system: at most once or at least once, ideally you should have an option to have both for different real-time features in your app</li>\n<li class=\"\">make sure to use one or pool of connections between your server and a broker, don't create new connection per each client or topic that comes to your WebSocket server</li>\n<li class=\"\">use effective serialization format between your WebSocket server and broker</li>\n</ul>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"massive-reconnect\">Massive reconnect<a href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#massive-reconnect\" class=\"hash-link\" aria-label=\"Direct link to Massive reconnect\" title=\"Direct link to Massive reconnect\" translate=\"no\">​</a></h2>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/S9koKYg.png\" alt=\"mass_reconnect\" class=\"img_ev3q\"></p>\n<p>Let's talk about one more problem that is unique for Websocket servers compared to HTTP. Your app can have thousands or millions of active WebSocket connections. In contract to stateless HTTP APIs your application is stateful. It uses push model. As soon as you deploying your WebSocket server or reload your load balancer (Nginx maybe) – connections got dropped and all that army of users start reconnecting. And this can be like an avalanche actually. How to survive?</p>\n<p>First of all - use exponential backoff strategies on client side. I.e. reconnect with intervals like 1, 2, 4, 8, 16 seconds with some random jitter.</p>\n<p>Turn on various rate limiting strategies on your WebSocket server, some of them should be turned on your backend load balancer level (like controlling TCP connection establishment rate), some are application specific (maybe limit an amount of requests from certain user).</p>\n<p>One more interesting technique to survive massive reconnect is using JWT (JSON Web Token) for authentication. I'll try to explain why this can be useful.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/aaTEhXo.png\" alt=\"jwt\" class=\"img_ev3q\"></p>\n<p>As soon as your client start reconnecting you will have to authenticate each connection. In massive setups with many persistent connection this can be a very significant load on your Session backend. Since you need an extra request to your session storage for every client coming back. This can be a no problem for some infrastructures but can be really disastrous for others. JWT allows to reduce this spike in load on session storage since it can have all required authentication information inside its payload. When using JWT make sure you have chosen a reasonable JWT expiration time – expiration interval depends on your application nature and just one of trade-offs you should deal with as developer.</p>\n<p>Don't forget about making an effective connection between your WebSocket server and broker – as soon as all clients start reconnecting you should resubscribe your server nodes to all topics as fast as possible. Use techniques like smart batching at this moment.</p>\n<p>Let's look at a small piece of code that demonstrates this technique. Imagine we have a source channel from which we get items to process. We don’t want to process items individually but in batch. For this we wait for first item coming from channel, then try to collect as many items from channel buffer as we want without blocking and timeouts involved. And then process slice of items we collected at once. For example build Redis pipeline from them and send to Redis in one connection write call.</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">maxBatchSize </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">50</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">select</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> item </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\">sourceCh</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        batch </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">item</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    loop</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">len</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">batch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;</span><span class=\"token plain\"> maxBatchSize </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"font-style:italic\">select</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> item </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&lt;-</span><span class=\"token plain\">sourceCh</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                batch </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">append</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">batch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> item</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token keyword\" style=\"font-style:italic\">default</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token keyword\" style=\"font-style:italic\">break</span><span class=\"token plain\"> loop</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">            </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Do sth with collected batch of items.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">len</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">batch</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Look at a complete example in a Go playground: <a href=\"https://play.golang.org/p/u7SAGOLmDke\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://play.golang.org/p/u7SAGOLmDke</a>.</p>\n<p>I also made a repo where I demonstrate how this technique together with Redis pipelining feature allows to fully utilize connection for a good performance <a href=\"https://github.com/FZambia/redigo-smart-batching\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://github.com/FZambia/redigo-smart-batching</a>.</p>\n<p>Another advice for those who run WebSocket services in Kubernetes. Learn how your ingress behaves – for example Nginx ingress can reload its configuration on every change inside Kubernetes services map resulting into closing all active WebSocket connections. Proxies like Envoy don't have this behaviour, so you can reduce number of mass disconnections in your system. You can also proxy WebSocket without using ingress at all over configured WebSocket service NodePort.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"message-event-stream-benefits\">Message event stream benefits<a href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#message-event-stream-benefits\" class=\"hash-link\" aria-label=\"Direct link to Message event stream benefits\" title=\"Direct link to Message event stream benefits\" translate=\"no\">​</a></h2>\n<p>Here comes a final part of this post. Maybe the most important one.</p>\n<p>Not only mass client re-connections could create a significant load on a session backend but also a huge load on your main application database. Why? Because WebSocket applications are stateful. Clients rely on a stream of messages coming from a backend to maintain its state actual. As soon as connection dropped client tries to reconnect. In some scenarios it also wants to restore its actual state. What if client reconnected after 3 seconds? How many state updates it could miss? Nobody knows. So to make sure state is actual client tries to get it from application database. This is again <strong>a significant spike in load on your main database</strong> in massive reconnect scenario. In can be really painful with many active connections.</p>\n<p>So what I think is nice to have for scenarios where we can't afford to miss messages (like in chat-like apps for example) is having effective and performant stream of messages inside each channel. Keep this stream in fast in-memory storage. This stream can have time retention and be limited in size (think about it as a sliding window of messages). I already mentioned that Redis can do this – it's possible to keep messages in Redis List or Redis Stream data structures. Other broker solutions could give you access to such a stream inside each channel out of the box.</p>\n<p>So as soon as client reconnects it can restore its state from fast in-memory event stream without even querying your database. Actually to survive mass reconnect scenario you don't need to keep such a stream for a long time – several minutes should be enough. You can <strong>even create your own Websocket fallback implementation (like Long-Polling) utilizing event stream with limited retention</strong>.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2020/11/12/scaling-websocket#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>Hope advices given here will be useful for a reader and will help writing a more robust and more scalable real-time application backends.</p>\n<p><a href=\"https://github.com/centrifugal/centrifugo/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifugo server</a> and <a href=\"https://github.com/centrifugal/centrifuge\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Centrifuge library for Go language</a> have most of the mechanics described here including the last one – message stream for topics limited by size and retention period. Both also have techniques to prevent message loss due to at most once nature of Redis PUB/SUB giving at least once delivery guarantee inside message history window size and retention period.</p>",
            "url": "https://centrifugal.dev/blog/2020/11/12/scaling-websocket",
            "title": "Scaling WebSocket in Go and beyond",
            "summary": "The post describes techniques to write scalable WebSocket servers within Go ecosystem and beyond it",
            "date_modified": "2020-11-12T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "websocket",
                "go"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport",
            "content_html": "<p><strong>UPDATE: WebTransport spec is still evolving. Most information here is not actual anymore. For example the working group has no plan to implement both QuicTransport and HTTP3-based transports – only HTTP3 based WebTransport is going to be implemented. Maybe we will publish a follow-up of this post at some point.</strong></p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"overview\">Overview<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#overview\" class=\"hash-link\" aria-label=\"Direct link to Overview\" title=\"Direct link to Overview\" translate=\"no\">​</a></h2>\n<p>WebTransport is a new browser API offering low-latency, bidirectional, client-server messaging. If you have not heard about it before I suggest to first read a post called <a href=\"https://web.dev/quictransport/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Experimenting with QuicTransport</a> published recently on web.dev – it gives a nice overview to WebTransport and shows client-side code examples. Here we will concentrate on implementing server side.</p>\n<p>Some key points about WebTransport spec:</p>\n<ul>\n<li class=\"\">WebTransport standard will provide a possibility to use streaming client-server communication using modern transports such as <a href=\"https://en.wikipedia.org/wiki/QUIC\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">QUIC</a> and <a href=\"https://en.wikipedia.org/wiki/HTTP/3\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">HTTP/3</a></li>\n<li class=\"\">It can be a good alternative to <a href=\"https://en.wikipedia.org/wiki/WebSocket\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WebSocket</a> messaging, standard provides some capabilities that are not possible with current WebSocket spec: possibility to get rid of head-of-line blocking problems using individual streams for different data, the possibility to reuse a single connection to a server in different browser tabs</li>\n<li class=\"\">WebTransport also defines an unreliable stream API using UDP datagrams (which is possible since QUIC is UDP-based) – which is what browsers did not have before without a rather complex <a href=\"https://en.wikipedia.org/wiki/WebRTC\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WebRTC</a> setup involving ICE, STUN, etc. This is sweet for in-browser real-time games.</li>\n</ul>\n<p>To help you figure out things here are links to current WebTransport specs:</p>\n<ul>\n<li class=\"\"><a href=\"https://tools.ietf.org/html/draft-vvv-webtransport-overview-01\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WebTransport overview</a> – this spec gives an overview of WebTransport and provides requirements to transport layer</li>\n<li class=\"\"><a href=\"https://tools.ietf.org/html/draft-vvv-webtransport-quic\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WebTransport over QUIC</a> – this spec describes QUIC-based transport for WebTransport</li>\n<li class=\"\"><a href=\"https://tools.ietf.org/html/draft-vvv-webtransport-http3\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">WebTransport over HTTP/3</a> – this spec describes HTTP/3-based transport for WebTransport (actually HTTP/3 is a protocol defined on top of QUIC)</li>\n</ul>\n<p>At moment Chrome only implements <a href=\"https://web.dev/quictransport/#register-for-ot\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">trial possibility</a> to try out WebTransport standard and only implements WebTransport over QUIC. Developers can initialize transport with code like this:</p>\n<div class=\"language-javascript codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-javascript codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> transport </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">new</span><span class=\"token plain\"> </span><span class=\"token class-name\" style=\"color:rgb(255, 203, 107)\">QuicTransport</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">'quic-transport://localhost:4433/path'</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><br></div></code></pre></div></div>\n<p>In case of HTTP/3 transport one will use URL like <code>'https://localhost:4433/path'</code> in transport constructor. All WebTransport underlying transports should support instantiation over URL – that's one of the spec requirements.</p>\n<p>I decided that this is a cool possibility to finally play with QUIC protocol and its Go implementation <a href=\"https://github.com/lucas-clemente/quic-go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">github.com/lucas-clemente/quic-go</a>.</p>\n<div class=\"theme-admonition theme-admonition-danger admonition_xJq3 alert alert--danger\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 12 16\"><path fill-rule=\"evenodd\" d=\"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z\"></path></svg></span>danger</div><div class=\"admonitionContent_BuS1\"><p>Please keep in mind that all things described in this post are work in progress. WebTransport drafts, Quic-Go library, even QUIC protocol itself are subjects to change. You should not use it in production yet.</p></div></div>\n<p><a href=\"https://web.dev/quictransport/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Experimenting with QuicTransport</a> post contains links to a <a href=\"https://googlechrome.github.io/samples/quictransport/client.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">client example</a> and companion <a href=\"https://github.com/GoogleChrome/samples/blob/gh-pages/quictransport/quic_transport_server.py\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Python server implementation</a>.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/Hty00aG.png\" alt=\"client example\" class=\"img_ev3q\"></p>\n<p>We will use a linked client example to connect to a server that runs on localhost and uses <a href=\"https://github.com/lucas-clemente/quic-go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">github.com/lucas-clemente/quic-go</a> library. To make our example work we need to open client example in Chrome, and actually, at this moment we need to install Chrome Canary. The reason behind this is that the  <code>quic-go</code> library supports QUIC draft-29 while Chrome &lt; 85 implements QuicTransport over draft-27. If you read this post at a time when Chrome stable 85 already released then most probably you don't need to install Canary release and just use your stable Chrome.</p>\n<p>We also need to generate self-signed certificates since WebTransport only works with a TLS layer, and we should make Chrome trust our certificates. Let's prepare our client environment before writing a server and first install Chrome Canary.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"install-chrome-canary\">Install Chrome Canary<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#install-chrome-canary\" class=\"hash-link\" aria-label=\"Direct link to Install Chrome Canary\" title=\"Direct link to Install Chrome Canary\" translate=\"no\">​</a></h2>\n<p>Go to <a href=\"https://www.google.com/intl/en/chrome/canary/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://www.google.com/intl/en/chrome/canary/</a>, download and install Chrome Canary. We will use it to open <a href=\"https://googlechrome.github.io/samples/quictransport/client.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">client example</a>.</p>\n<div class=\"theme-admonition theme-admonition-note admonition_xJq3 alert alert--secondary\"><div class=\"admonitionHeading_Gvgb\"><span class=\"admonitionIcon_Rf37\"><svg viewBox=\"0 0 14 16\"><path fill-rule=\"evenodd\" d=\"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z\"></path></svg></span>note</div><div class=\"admonitionContent_BuS1\"><p>If you have Chrome &gt;= 85 then most probably you can skip this step.</p></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"generate-self-signed-tls-certificates\">Generate self-signed TLS certificates<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#generate-self-signed-tls-certificates\" class=\"hash-link\" aria-label=\"Direct link to Generate self-signed TLS certificates\" title=\"Direct link to Generate self-signed TLS certificates\" translate=\"no\">​</a></h2>\n<p>Since WebTransport based on modern network transports like QUIC and HTTP/3 security is a keystone. For our experiment we will create a self-signed TLS certificate using <code>openssl</code>.</p>\n<p>Make sure you have <code>openssl</code> installed:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ which openssl</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">/usr/bin/openssl</span><br></div></code></pre></div></div>\n<p>Then run:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">openssl genrsa -des3 -passout pass:x -out server.pass.key 2048</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">openssl rsa -passin pass:x -in server.pass.key -out server.key</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">rm server.pass.key</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">openssl req -new -key server.key -out server.csr</span><br></div></code></pre></div></div>\n<p>Set <code>localhost</code> for Common Name when asked.</p>\n<p>The self-signed TLS certificate generated from the <code>server.key</code> private key and <code>server.csr</code> files:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt</span><br></div></code></pre></div></div>\n<p>After these manipulations you should have <code>server.crt</code> and <code>server.key</code> files in your working directory.</p>\n<p>To help you with process here is my console output during these steps (click to open):</p>\n<p>??? example \"My console output generating self-signed certificates\"</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ openssl genrsa -des3 -passout pass:x -out server.pass.key 2048</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Generating RSA private key, 2048 bit long modulus</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">...........................................................................................+++</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">.....................+++</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">e is 65537 (0x10001)</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ ls</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">server.pass.key</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ openssl rsa -passin pass:x -in server.pass.key -out server.key</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">writing RSA key</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ ls</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">server.key      server.pass.key</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ rm server.pass.key</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ openssl req -new -key server.key -out server.csr</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">You are about to be asked to enter information that will be incorporated</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">into your certificate request.</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">What you are about to enter is what is called a Distinguished Name or a DN.</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">There are quite a few fields but you can leave some blank</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">For some fields there will be a default value,</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">If you enter '.', the field will be left blank.</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">-----</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Country Name (2 letter code) []:RU</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">State or Province Name (full name) []:</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Locality Name (eg, city) []:</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Organization Name (eg, company) []:</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Organizational Unit Name (eg, section) []:</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Common Name (eg, fully qualified host name) []:localhost</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Email Address []:</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Please enter the following 'extra' attributes</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">to be sent with your certificate request</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">A challenge password []:</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ openssl x509 -req -sha256 -days 365 -in server.csr -signkey server.key -out server.crt</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Signature ok</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">subject=/C=RU/CN=localhost</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">Getting Private key</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ ls</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">server.crt server.csr server.key</span><br></div></code></pre></div></div>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"run-client-example\">Run client example<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#run-client-example\" class=\"hash-link\" aria-label=\"Direct link to Run client example\" title=\"Direct link to Run client example\" translate=\"no\">​</a></h2>\n<p>Now the last step. What we need to do is run Chrome Canary with some flags that will allow it to trust our self-signed certificates. I suppose there is an alternative way making Chrome trust your certificates, but I have not tried it.</p>\n<p>First let's find out a fingerprint of our cert:</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">openssl x509 -in server.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64</span><br></div></code></pre></div></div>\n<p>In my case base64 fingerprint was <code>pe2P0fQwecKFMc6kz3+Y5MuVwVwEtGXyST5vJeaOO/M=</code>, yours will be different.</p>\n<p>Then run Chrome Canary with some additional flags that will make it trust out certs (close other Chrome Canary instances before running it):</p>\n<div class=\"language-bash codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-bash codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">$ /Applications/Google\\ Chrome\\ Canary.app/Contents/MacOS/Google\\ Chrome\\ Canary \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    --origin-to-force-quic-on=localhost:4433 \\</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    --ignore-certificate-errors-spki-list=pe2P0fQwecKFMc6kz3+Y5MuVwVwEtGXyST5vJeaOO/M=</span><br></div></code></pre></div></div>\n<p>This example is for MacOS, for your system see <a href=\"https://www.chromium.org/developers/how-tos/run-chromium-with-flags\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">docs on how to run Chrome/Chromium with custom flags</a>.</p>\n<p>Now you can open <a href=\"https://googlechrome.github.io/samples/quictransport/client.html\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">https://googlechrome.github.io/samples/quictransport/client.html</a> URL in started browser and click <code>Connect</code> button. What? Connection not established? OK, this is fine since we need to run our server :)</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"writing-a-quic-server\">Writing a QUIC server<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#writing-a-quic-server\" class=\"hash-link\" aria-label=\"Direct link to Writing a QUIC server\" title=\"Direct link to Writing a QUIC server\" translate=\"no\">​</a></h2>\n<p>Maybe in future we will have libraries that are specified to work with WebTransport over QUIC or HTTP/3, but for now we should implement server manually. As said above we will use <a href=\"https://github.com/lucas-clemente/quic-go\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">github.com/lucas-clemente/quic-go</a> library to do this.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"server-skeleton\">Server skeleton<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#server-skeleton\" class=\"hash-link\" aria-label=\"Direct link to Server skeleton\" title=\"Direct link to Server skeleton\" translate=\"no\">​</a></h3>\n<p>First, let's define a simple skeleton for our server:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">package</span><span class=\"token plain\"> main</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">import</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"errors\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"log\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"github.com/lucas-clemente/quic-go\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Config for WebTransportServerQuic.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> Config </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// ListenAddr sets an address to bind server to.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tListenAddr </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// TLSCertPath defines a path to .crt cert file.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tTLSCertPath </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// TLSKeyPath defines a path to .key cert file</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tTLSKeyPath </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// AllowedOrigins represents list of allowed origins to connect from.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tAllowedOrigins </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// WebTransportServerQuic can handle WebTransport QUIC connections according</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// to https://tools.ietf.org/html/draft-vvv-webtransport-quic-02.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> WebTransportServerQuic </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tconfig Config</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// NewWebTransportServerQuic creates new WebTransportServerQuic.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewWebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">config Config</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">WebTransportServerQuic </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">WebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tconfig</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> config</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Run server.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">WebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Run</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> errors</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"not implemented\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">main</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tserver </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">NewWebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">Config</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tListenAddr</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">     </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"0.0.0.0:4433\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tTLSCertPath</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">    </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"server.crt\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tTLSKeyPath</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">     </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"server.key\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tAllowedOrigins</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"localhost\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"googlechrome.github.io\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> server</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Run</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"accept-quic-connections\">Accept QUIC connections<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#accept-quic-connections\" class=\"hash-link\" aria-label=\"Direct link to Accept QUIC connections\" title=\"Direct link to Accept QUIC connections\" translate=\"no\">​</a></h3>\n<p>Let's concentrate on implementing <code>Run</code> method. We need to accept QUIC client connections. This can be done by creating <code>quic.Listener</code> instance and using its <code>.Accept</code> method to accept incoming client sessions.</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Run server.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">WebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Run</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tlistener</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> quic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">ListenAddr</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">ListenAddr</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">generateTLSConfig</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tsess</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> listener</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Accept</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Background</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Printf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"session accepted: %s\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> sess</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">RemoteAddr</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">String</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">go</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">defer</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> sess</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">CloseWithError</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"bye\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"close session\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\ts</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">handleSession</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">sess</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">WebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">handleSession</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">sess quic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Session</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Not implemented yet.    </span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>An interesting thing to note is that QUIC allows closing connection with specific application-level integer code and custom string reason. Just like WebSocket if you worked with it.</p>\n<p>Also note, that we are starting our <code>Listener</code> with TLS configuration returned by <code>s.generateTLSConfig()</code> method. Let's take a closer look at how this method can be implemented.</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// https://tools.ietf.org/html/draft-vvv-webtransport-quic-02#section-3.1</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> alpnQuicTransport </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"wq-vvv-01\"</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">WebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">generateTLSConfig</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">tls</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Config </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tcert</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> tls</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">LoadX509KeyPair</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">TLSCertPath</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">TLSKeyPath</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Fatal</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">tls</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Config</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tCertificates</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token plain\">tls</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Certificate</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">cert</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tNextProtos</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\">   </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\">alpnQuicTransport</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Inside <code>generateTLSConfig</code> we load x509 certs from cert files generated above. WebTransport uses ALPN (<a href=\"https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">Application-Layer Protocol Negotiation</a> to prevent handshakes with a server that does not support WebTransport spec. This is just a string <code>wq-vvv-01</code> inside <code>NextProtos</code> slice of our <code>*tls.Config</code>.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"connection-session-handling\">Connection Session handling<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#connection-session-handling\" class=\"hash-link\" aria-label=\"Direct link to Connection Session handling\" title=\"Direct link to Connection Session handling\" translate=\"no\">​</a></h3>\n<p>At this moment if you run a server and open a client example in Chrome then click <code>Connect</code> button – you should see that connection successfully established in event log area:</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/PyEr9W9.png\" alt=\"client example\" class=\"img_ev3q\"></p>\n<p>Now if you try to send data to a server nothing will happen. That's because we have not implemented reading data from session streams.</p>\n<p>Streams in QUIC provide a lightweight, ordered byte-stream abstraction to an application. Streams can be unidirectional or bidirectional.</p>\n<p>Streams can be short-lived, streams can also be long-lived and can last the entire duration of a connection.</p>\n<p>Client example provides three possible ways to communicate with a server:</p>\n<ul>\n<li class=\"\">Send a datagram</li>\n<li class=\"\">Open a unidirectional stream</li>\n<li class=\"\">Open a bidirectional stream</li>\n</ul>\n<p>Unfortunately, <code>quic-go</code> library does not support sending UDP datagrams at this moment. To do this <code>quic-go</code> should implement one more draft called <a href=\"https://tools.ietf.org/html/draft-pauly-quic-datagram-05\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">An Unreliable Datagram Extension to QUIC</a>. There is already <a href=\"https://github.com/lucas-clemente/quic-go/pull/2162\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">an ongoing pull request</a> that implements it. This means that it's too early for us to experiment with unreliable UDP WebTransport client-server communication in Go. By the way, the interesting facts about UDP over QUIC are that QUIC congestion control mechanism will <a href=\"https://tools.ietf.org/html/draft-ietf-quic-datagram-00#section-5.3\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">still apply</a> and QUIC datagrams <a href=\"https://tools.ietf.org/html/draft-ietf-quic-datagram-00#section-5.1\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">can support acknowledgements</a>.</p>\n<p>Implementing a unidirectional stream is possible with <code>quic-go</code> since the library supports creating and accepting unidirectional streams, but I'll leave this for a reader (though we will need accepting one unidirectional stream for parsing client indication anyway – see below).</p>\n<p>Here we will only concentrate on implementing a server for a bidirectional case. We are in the Centrifugo blog, and this is the most interesting type of stream for me personally.</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"parsing-client-indication\">Parsing client indication<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#parsing-client-indication\" class=\"hash-link\" aria-label=\"Direct link to Parsing client indication\" title=\"Direct link to Parsing client indication\" translate=\"no\">​</a></h3>\n<p>According to <a href=\"https://tools.ietf.org/html/draft-vvv-webtransport-quic-02#section-3.2\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">section-3.2</a> of Quic WebTransport spec in order to verify that the client's origin allowed connecting to the server, the user agent has to communicate the origin to the server. This is accomplished by sending a special message, called client indication, on stream 2, which is the first client-initiated unidirectional stream.</p>\n<p>Here we will implement this. In the beginning of our session handler we will accept a unidirectional stream initiated by a client.</p>\n<p>At moment spec defines two client indication keys: <code>Origin</code> and <code>Path</code>. In our case an origin value will be <code>https://googlechrome.github.io</code> and path will be <code>/counter</code>.</p>\n<p>Let's define some constants and structures:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// client indication stream can not exceed 65535 bytes in length.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// https://tools.ietf.org/html/draft-vvv-webtransport-quic-02#section-3.2</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> maxClientIndicationLength </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">65535</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// define known client indication keys.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> clientIndicationKey </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">int16</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">const</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tclientIndicationKeyOrigin clientIndicationKey </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">0</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tclientIndicationKeyPath                       </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">1</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// ClientIndication container.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">type</span><span class=\"token plain\"> ClientIndication </span><span class=\"token keyword\" style=\"font-style:italic\">struct</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Origin client indication value.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tOrigin </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// Path client indication value.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tPath </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Now what we should do is accept unidirectional stream inside session handler:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">WebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">handleSession</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">sess quic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Session</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> sess</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">AcceptUniStream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Background</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Printf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"uni stream accepted, id: %d\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">StreamID</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    indication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">receiveClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Printf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"client indication: %+v\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> indication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">validateClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">indication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// this method blocks.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">communicate</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">sess</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">        log</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Println</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">err</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">receiveClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">stream quic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">ReceiveStream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> ClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> errors</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"not implemented yet\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">WebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">validateClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">indication ClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> errors</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"not implemented yet\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">WebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">communicate</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">sess quic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Session</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> errors</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"not implemented yet\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>As you can see to accept a unidirectional stream with data we can use <code>.AcceptUniStream</code> method of <code>quic.Session</code>. After accepting a stream we should read client indication data from it.</p>\n<p>According to spec it will contain a client indication in the following format:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">0                   1                   2                   3</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">|           Key (16)            |          Length (16)          |</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">|                           Value (*)                         ...</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br></div></code></pre></div></div>\n<p>The code below parses client indication out of a stream data, we decode key-value pairs from uni stream until an end of stream (indicated by EOF):</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">receiveClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">stream quic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">ReceiveStream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">ClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> clientIndication ClientIndication</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// read no more than maxClientIndicationLength bytes.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\treader </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> io</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">LimitReader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> maxClientIndicationLength</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tdone </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">false</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> done </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">break</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> key </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">int16</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\terr </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> binary</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Read</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">reader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> binary</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">BigEndian</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">==</span><span class=\"token plain\"> io</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">EOF </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tdone </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">else</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> clientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> valueLength </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">int16</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\terr </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> binary</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Read</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">reader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> binary</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">BigEndian</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">&amp;</span><span class=\"token plain\">valueLength</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> clientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tbuf </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">make</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">byte</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> valueLength</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tn</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> reader</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Read</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">buf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">==</span><span class=\"token plain\"> io</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">EOF </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">                </span><span class=\"token comment\" style=\"color:rgb(105, 112, 152);font-style:italic\">// still need to process indication value.</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\tdone </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">else</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> clientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">int16</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">n</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> valueLength </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> clientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> errors</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"read less than expected\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tvalue </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">buf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">switch</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">clientIndicationKey</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> clientIndicationKeyOrigin</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tclientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Origin </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> value</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">case</span><span class=\"token plain\"> clientIndicationKeyPath</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tclientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Path </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> value</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">default</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">:</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Printf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"skip unknown client indication key: %d: %s\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> key</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> value</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> clientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>We also validate Origin inside <code>validateClientIndication</code> method of our server:</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">var</span><span class=\"token plain\"> errBadOrigin </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">=</span><span class=\"token plain\"> errors</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">New</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"bad origin\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">WebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">validateClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">indication ClientIndication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\tu</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> url</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Parse</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">indication</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Origin</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> errBadOrigin</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">stringInSlice</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">u</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Host</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> s</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">config</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">AllowedOrigins</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> errBadOrigin</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\" style=\"display:inline-block\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">stringInSlice</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">a </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> list </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">[</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">]</span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">string</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">bool</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> b </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> </span><span class=\"token keyword\" style=\"font-style:italic\">range</span><span class=\"token plain\"> list </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> b </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">==</span><span class=\"token plain\"> a </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">true</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">false</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>Do you have <code>stringInSlice</code> function in every Go project? I do :)</p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"communicating-over-bidirectional-streams\">Communicating over bidirectional streams<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#communicating-over-bidirectional-streams\" class=\"hash-link\" aria-label=\"Direct link to Communicating over bidirectional streams\" title=\"Direct link to Communicating over bidirectional streams\" translate=\"no\">​</a></h3>\n<p>The final part here is accepting a bidirectional stream from a client, reading it, and sending responses back. Here we will just echo everything a client sends to a server back to a client. You can implement whatever bidirectional communication you want actually.</p>\n<p>Very similar to unidirectional case we can call <code>.AcceptStream</code> method of session to accept a bidirectional stream.</p>\n<div class=\"language-go codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-go codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token keyword\" style=\"font-style:italic\">func</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">s </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">*</span><span class=\"token plain\">WebTransportServerQuic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">communicate</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">sess quic</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token plain\">Session</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"> </span><span class=\"token builtin\" style=\"color:rgb(130, 170, 255)\">error</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token keyword\" style=\"font-style:italic\">for</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tstream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> sess</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">AcceptStream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">context</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Background</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\tlog</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Printf</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token string\" style=\"color:rgb(195, 232, 141)\">\"stream accepted: %d\"</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">StreamID</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">if</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">_</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:=</span><span class=\"token plain\"> io</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">.</span><span class=\"token function\" style=\"color:rgb(130, 170, 255)\">Copy</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">(</span><span class=\"token plain\">stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"> stream</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">)</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">;</span><span class=\"token plain\"> err </span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">!=</span><span class=\"token plain\"> </span><span class=\"token boolean\" style=\"color:rgb(255, 88, 116)\">nil</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t\t</span><span class=\"token keyword\" style=\"font-style:italic\">return</span><span class=\"token plain\"> err</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">\t</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>\n<p>When you press <code>Send</code> button in client example it creates a bidirectional stream, sends data to it, then closes stream. Thus our code is sufficient. For a more complex communication that involves many concurrent streams you will have to write a more complex code that allows working with streams concurrently on server side.</p>\n<p><img decoding=\"async\" loading=\"lazy\" src=\"https://i.imgur.com/5299Vr4.png\" alt=\"client example\" class=\"img_ev3q\"></p>\n<h3 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"full-server-example\">Full server example<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#full-server-example\" class=\"hash-link\" aria-label=\"Direct link to Full server example\" title=\"Direct link to Full server example\" translate=\"no\">​</a></h3>\n<p>Full server code can be found <a href=\"https://gist.github.com/FZambia/07dca3a7a75a264746101cd5657f1150\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">in a Gist</a>. Again – this is a toy example based on things that all work in progress.</p>\n<h2 class=\"anchor anchorTargetHideOnScrollNavbar_vjPI\" id=\"conclusion\">Conclusion<a href=\"https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport#conclusion\" class=\"hash-link\" aria-label=\"Direct link to Conclusion\" title=\"Direct link to Conclusion\" translate=\"no\">​</a></h2>\n<p>WebTransport is an interesting technology that can open new possibilities in modern Web development. At this moment it's possible to play with it using QUIC transport – here we looked at how one can do that. Though we still have to wait a bit until all these things will be suitable for production usage.</p>\n<p>Also, even when ready we will still have to think about WebTransport fallback options – since wide adoption of browsers that support some new technology and infrastructure takes time. Actually WebTransport spec authors consider fallback options in design. This was mentioned in IETF slides (<a href=\"https://www.ietf.org/proceedings/106/slides/slides-106-webtrans-webtrans-bof-slides-03\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">PDF, 2.6MB</a>), but I have not found any additional information beyond that.</p>\n<p>Personally, I think the most exciting thing about WebTransport is the possibility to exchange UDP datagrams, which can help a lot to in-browser gaming. Unfortunately, we can't test it at this moment with Go (but it's already possible using Python as server as shown <a href=\"https://github.com/GoogleChrome/samples/blob/gh-pages/quictransport/quic_transport_server.py\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"\">in the example</a>).</p>\n<p>WebTransport could be a nice candidate for a new Centrifugo transport next to WebSocket and SockJS – time will show.</p>",
            "url": "https://centrifugal.dev/blog/2020/10/16/experimenting-with-quic-transport",
            "title": "Experimenting with QUIC and WebTransport",
            "summary": "Here we experiment with QUIC and WebTransport in Chrome",
            "date_modified": "2020-10-16T00:00:00.000Z",
            "author": {
                "name": "Alexander Emelin"
            },
            "tags": [
                "quic",
                "webtransport",
                "go"
            ]
        },
        {
            "id": "https://centrifugal.dev/blog/2020/02/10/million-connections-with-centrifugo",
            "content_html": "<p>In order to get an understanding about possible hardware requirements for reasonably massive Centrifugo setup we made a test stand inside Kubernetes.</p>\n<p>Our goal was to run server based on Centrifuge library (the core of Centrifugo server) with one million WebSocket connections and send many messages to connected clients. While sending many messages we have been looking at delivery time latency. In fact we will see that about 30 million messages per minute (500k messages per second) will be delivered to connected clients and latency won't be larger than 200ms in 99 percentile.</p>\n<p>Server nodes have been run on machines with the following configuration:</p>\n<ul>\n<li class=\"\">CPU Intel(R) Xeon(R) CPU E5-2640 v4 @ 2.40GHz</li>\n<li class=\"\">Linux Debian 4.9.65-3+deb9u1 (2017-12-23) x86_64 GNU/Linux</li>\n</ul>\n<p>Some <code>sysctl</code> values:</p>\n<div class=\"language-text codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-text codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">fs.file-max = 3276750</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">fs.nr_open = 1048576</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">net.ipv4.tcp_mem = 3086496\t4115330\t6172992</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">net.ipv4.tcp_rmem = 8192\t8388608\t16777216</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">net.ipv4.tcp_wmem = 4096\t4194394\t16777216</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">net.core.rmem_max = 33554432</span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">net.core.wmem_max = 33554432</span><br></div></code></pre></div></div>\n<p>Kubernetes used these machines as its nodes.</p>\n<p>We started 20 Centrifuge-based server pods. Our clients connected to server pods using Centrifuge Protobuf protocol. To scale horizontally we used Redis Engine and sharded it to 5 different Redis instances (each Redis instance consumes 1 CPU max).</p>\n<p>To achieve many client connections we used 100 Kubernetes pods each generating about 10k client connections to server.</p>\n<p>Here are some numbers we achieved:</p>\n<ul>\n<li class=\"\">1 million WebSocket connections</li>\n<li class=\"\">Each connection subscribed to 2 channels: one personal channel and one group channel (with 10 subscribers in it), i.e. we had about 1.1 million active channels at each moment.</li>\n<li class=\"\">28 million messages per minute (about 500k per second) <strong>delivered</strong> to clients</li>\n<li class=\"\">200k per minute constant connect/disconnect rate to simulate real-life situation where clients connect/disconnect from server</li>\n<li class=\"\">200ms delivery latency in 99 percentile</li>\n<li class=\"\">The size of each published message was about 100 bytes</li>\n</ul>\n<p>And here are some numbers about final resource usage on server side (we don't actually interested in client side resource usage here):</p>\n<ul>\n<li class=\"\">40 CPU total for server nodes when load achieved values claimed above (20 pods, ~2 CPU each)</li>\n<li class=\"\">27 GB of RAM used mostly to handle 1 mln WebSocket connections, i.e. about 30kb RAM per connection</li>\n<li class=\"\">0.32 CPU usage on every Redis instance</li>\n<li class=\"\">100 mbit/sec rx и 150 mbit/sec tx of network used on each server pod</li>\n</ul>\n<p>The picture that demonstrates experiment (better to open image in new tab):</p>\n<p><img decoding=\"async\" loading=\"lazy\" alt=\"Benchmark\" src=\"https://centrifugal.dev/assets/images/benchmark-b670972866abdc8936d2aef84333dd0c.gif\" width=\"1860\" height=\"916\" class=\"img_ev3q\"></p>\n<p>This also demonstrates that to handle one million of WebSocket connections without many messages sent to clients you need about 10 CPU total for server nodes and about 5% of CPU on each of Redis instances. In this case CPU mostly spent on connect/disconnect flow, ping/pong frames, subscriptions to channels.</p>\n<p>If we enable history and history message recovery features we see an increased Redis CPU usage: 64% instead of 32% on the same workload. Other resources usage is pretty the same.</p>\n<p>The results mean that one can theoretically achieve the comparable numbers on single modern server machine. But numbers can vary a lot in case of different load scenarios. In this benchmark we looked at basic use case where we only connect many clients and send Publications to them. There are many features in Centrifuge library and in Centrifugo not covered by this artificial experiment. Also note that though benchmark was made for Centrifuge library for Centrifugo you can expect similar results.</p>\n<p>Read and write buffer sizes of websocket connections were set to 512 kb on server side (sizes of buffers affect memory usage), with Centrifugo this means that to reproduce the same configuration you need to set (config updated for Centrifugo v6+):</p>\n<div class=\"language-json codeBlockContainer_Ckt0 theme-code-block\" style=\"--prism-color:#bfc7d5;--prism-background-color:#292d3e\"><div class=\"codeBlockContent_QJqH\"><pre tabindex=\"0\" class=\"prism-code language-json codeBlock_bY9V thin-scrollbar\" style=\"color:#bfc7d5;background-color:#292d3e\"><code class=\"codeBlockLines_e6Vv\"><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token property\">\"websocket\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">{</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"read_buffer_size\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">512</span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">,</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">    </span><span class=\"token property\">\"write_buffer_size\"</span><span class=\"token operator\" style=\"color:rgb(137, 221, 255)\">:</span><span class=\"token plain\"> </span><span class=\"token number\" style=\"color:rgb(247, 140, 108)\">512</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\">  </span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><span class=\"token plain\"></span><br></div><div class=\"token-line\" style=\"color:#bfc7d5\"><span class=\"token plain\"></span><span class=\"token punctuation\" style=\"color:rgb(199, 146, 234)\">}</span><br></div></code></pre></div></div>",
            "url": "https://centrifugal.dev/blog/2020/02/10/million-connections-with-centrifugo",
            "title": "Million connections with Centrifugo",
            "summary": "Describing a test stand in Kubernetes where we connect one million websocket connections to a server, using Redis to scale nodes, and providing insights about hardware resources required to achieve 500k messages per second",
            "date_modified": "2020-02-10T00:00:00.000Z",
            "author": {
                "name": "Centrifugal team"
            },
            "tags": [
                "centrifuge",
                "go"
            ]
        }
    ]
}