Case Study

From a Redis Relay to a Direct Feed: Rebuilding the Real-Time Pipeline

Lead Architect & Implementer · Jul 2023 – Present

Sep 1, 2026
6 min read
JJS
Written by Jatin Jain Saraf · Senior Software Engineer
60s → 230ms
Cold-Start Latency
5
Repos Migrated
~1 second
Upstream Reconnect Gap
WebSocketsRedisNode.js
case-studywebsocketsredisarchitectureperformance

From a Redis Relay to a Direct Feed: Rebuilding the Real-Time Pipeline

The old design for getting live block and transaction data onto SupraScan's frontend worked, and that's part of what makes it a useful case study. Nothing was broken. The indexer published every block and transaction to Redis pub/sub channels, a socket server subscribed to those channels and rebroadcast the data to connected browsers, and a second, entirely separate Redis stream fanned raw transaction data out to an external team's automated systems. It was a working relay with an extra hop, and the decision to remove that hop, across five separate repositories, is a good example of what disciplined deletion actually looks like.

text

The shape of the fix is visible in that comparison: the old path had the indexer sitting between the chain and the live feed even though the indexer's own job is writing to Postgres, not serving live updates. The new path removes it from that role entirely, and Redis goes from being the transport to being a small cache for one specific case, a client reconnecting mid-session.

The new shape: a client, not a relay

The replacement inverts the socket server's role. Instead of subscribing to an internal Redis channel that the indexer populates, the socket server itself becomes a WebSocket client, connecting directly to each blockchain node's own live feed for every environment it serves. The moment a message arrives from the node, it's broadcast straight to every connected browser, with no buffering interval and no per-tick cap holding messages back to smooth them out. Redis still plays a role, but a much smaller one: the last handful of blocks and transactions get cached there purely so a client that reconnects mid-session has something to show immediately, a snapshot, not the primary transport for live data anymore.

Proving the old path was actually dead before deleting it

The discipline here is in what happened before any code was removed, not in the replacement architecture itself. Before deleting the old pub/sub channels, a live subscriber count check against both the QA and production Redis instances confirmed those channels had zero active subscribers. That's a deliberate step, not an assumption: the team didn't reason their way to "nobody should be using this anymore," they checked, live, against production, and only then treated it as confirmed dead traffic. On top of that, the socket server's environment-enable configuration was flipped off for the two environments being migrated, on both QA and production, before the code that implemented the old path was even merged, specifically so there was no window where a live user could hit a code path that had already been half-removed.

The rollout itself crossed five separate repositories, and the merge order mattered: a shared library and the frontend went first, then the socket server, then the backend, then the indexer last. Each merge required a clean build and a grep-verified check that every deleted symbol genuinely had no remaining references anywhere in that repo, before moving on to the next one in the sequence.

Scope that grew on purpose, and a landmine avoided

One piece of functionality got folded into this same change deliberately, not accidentally: a service that computed live transactions-per-second by scanning a sliding window of recent blocks on every single block processed. It turned out to be more computationally expensive than either of the two systems that had already superseded it elsewhere in the stack, a lighter-weight live poll directly against the chain node's own metrics endpoint for the homepage display, and a completely separate metrics-accumulator pipeline feeding the analytics page. Removing a redundant, expensive computation while already touching this code was the right call, and it was made with the author's knowledge rather than discovered as scope creep after the fact.

One specific risk was flagged and handled explicitly during planning: the socket server's environment-enable configuration and its corresponding WebSocket routes had to be removed together, in the same change. Removing one without the other would either leave a live route pointing at handler code that no longer existed, or leave a stale configuration blocking routes that had already been deleted, both of which are the kind of half-finished cleanup that causes an outage days later when someone finally notices.

A second, opposite kind of care went into what wasn't removed. The indexer has its own internal publish and subscribe loop, structurally identical to the exact pattern being deleted everywhere else in this change. But this one is dual-purpose: the indexer's own main processing loop reads its current chain height from that same channel. Removing it under the assumption that it matched the pattern being cleaned up everywhere else would have broken block ingestion itself, not just an external, already-dead feed. It was explicitly flagged and kept, a reminder that the same-looking code in two places doesn't always mean the same thing.

The number worth keeping

A separate but related change in the same era replaced a cold-connect page load's dependency on a live RPC round-trip with a Redis-backed cache instead. The measured result: homepage cold-start hydration latency dropped from 60 seconds to 230 milliseconds. That's not a rounding improvement, it's a roughly 260x reduction in the worst-case time a new visitor waited before seeing live data, and it shipped as part of the same broader effort to make the real-time path leaner and less dependent on hops that weren't earning their cost.

What was accepted, not fixed

The new architecture isn't presented as flawless, and two specific limitations were knowingly accepted rather than solved. There's no per-client replay or acknowledgment: a client that disconnects and reconnects gets whatever the current cached snapshot happens to be, not a replay of exactly what it missed while offline. And the upstream connection from the socket server to each blockchain node has a roughly one-second reconnect gap, during which a block can genuinely be missed by that specific connection. Both are documented, known tradeoffs of favoring a simpler, more direct architecture over a more complete but heavier one, not oversights discovered later.

What this demonstrates

Deleting a working system safely is a different skill than building a new one. Every step here, checking live subscriber counts before removing channels, flipping configuration off before merging code, sequencing five repositories in a specific dependency order, and correctly distinguishing a truly dead code path from a structurally identical but load-bearing one, is about reducing the risk of the deletion itself, not about the destination architecture being clever. The destination here is genuinely simpler than what it replaced. Getting there without an outage was the actual work.