Build1 distinct publisher3 min readUpdated
A notification stream on Spring WebFlux shows what horizontal scaling actually costs: Redis pub/sub fan-out, plus a subscribe-time race that silently drops events.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
The sink map is the part that does not survive contact with a load balancer. Each pod lazily builds a per-user `Sinks.Many` when someone subscribes, hands out its `Flux`, and deletes the sink when the last subscriber leaves [9]. That map is the only record of who is listening on that process, so a publish is a local lookup and nothing more. Redis pub/sub exists in this design purely to make pod B's write visible to pod A's map [14], because a count incremented on pod B lands in pod B's sink map and User 42's open connection on pod A never hears about it [13].
Which turns the lifecycle race into an operational problem rather than a curiosity. The author's sequence is precise: the `Flux` is returned holding a sink reference, the previous subscriber for that user unsubscribes, cleanup removes the sink from the map, a publish arrives and finds nothing to deliver to, and only then does the downstream attach to the orphan [10]. `Flux.defer` fixes it by rebuilding the map lookup at the moment the downstream attaches [11]. The reason that matters at scale is the stream's own design: the server hangs up after 30 minutes and the client reconnects with a fresh token [8], which is two unsubscribe-then-subscribe cycles per hour for every continuously connected user [17]. A window you would never hit by hand is now entered on a timer, on every pod, for every user.
Same timer, same arithmetic, on the cost side. A full-length connection carries 60 heartbeat comments [16], since the ping goes out every 30 seconds to keep intermediaries from treating an idle stream as a dead one [6]. Each reconnect also repeats the synchronous unread-count read that the endpoint does on subscribe [5], so the floor is two count queries per hour per connected user before anyone gets an actual notification [18]. That read was introduced to save the client a REST round-trip for first paint [5]; the forced-close policy hands part of it back twice an hour.
The backpressure choice is the one worth copying carefully. `directBestEffort` drops events for a slow subscriber instead of tearing the stream down, and the stated justification is payload-specific: the latest unread count replaces any count that got dropped [12]. That reasoning holds for a counter and stops holding the moment an event carries information no later event repeats, because the client is never told a drop happened. The published pattern advertises ordering gotchas alongside the lifecycle ones [15], and this is where they come from: a cross-pod bus feeding a local best-effort sink gives the browser no way to detect a gap unless something in the payload numbers itself.
Two of the remaining details are admissions about hops nobody in this codebase owns. `X-Accel-Buffering: no` is described as a hint to reverse proxies, which buffer by default and will hold events until the buffer fills [7] - a hint an unfamiliar proxy is free to ignore. The heartbeat is there because SSE looks idle to TCP intermediaries [6]. Both are workarounds for infrastructure behaviour, not application logic, and neither shows up in the single-pod version that took 30 lines [2].
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The team replaced notification-bell polling with Server-Sent Events.
On a single pod the implementation is Spring WebFlux plus a Flux of events, described as done in 30 lines.
On multiple pods, a write that happens on pod A needs to reach a subscriber on pod B, and naively it does not.
The /notifications/stream SSE endpoint emits the current unread count on subscribe, then live updates whenever the count changes.
The first ServerSentEvent carries the current unread count synchronously, so clients get the answer immediately and do not need a separate REST round-trip for first paint.
A comment event (:ping) is emitted every 30 seconds because SSE looks idle to TCP intermediaries; the heartbeat keeps the connection alive through proxy or CDN idle timeouts.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Concrete code, single self-reported source
The claims are unusually legible for a single-source story: the controller and Redis event-bus listings are quoted, and the mechanisms (initial emit, 30-second heartbeat, X-Accel-Buffering, take(30 minutes), Flux.defer around computeIfAbsent, directBestEffort) are visible in that code rather than asserted. But everything comes from one dev.to post by the implementing author, with no measurements, no independent replication, and no counter-source; the failure narratives (dropped publish race, pod A/pod B miss) are described, not demonstrated with logs or tests.
One self-reported deployment
Adoption evidence is limited to the author's own team running the pattern: polling replaced by SSE, then Redis pub/sub added when they scaled horizontally. There are no user counts, connection volumes, uptime figures, or any second organization reporting use of the pattern, so this registers as a single unquantified in-house deployment.
Slightly overstated by framing, honest in body
The headline framing ('30 lines on one pod') is promotional, and the piece presents itself as 'the full pattern' while omitting the parts an unverified claim would need: delivery guarantees under Redis or pod failure, connection capacity, and any latency numbers. Against that, the body is candid about the trade-offs it does cover — dropped events under directBestEffort, forced 30-minute disconnects, proxy buffering — so the gap is small rather than severe.
Author-visibility incentive, no vendor stake
The post is explicitly a cross-post of the author's own blog on a developer-publishing platform, which creates a straightforward audience-and-credibility incentive to present the pattern as clean and complete. There is no product being sold, no vendor sponsorship, and no commercial relationship disclosed with Spring, Redis, or nginx in the supplied material, so the distortion pressure is reputational rather than financial.
Mechanism-level confidence, no operational proof
Confidence is moderate: the technical mechanisms are internally consistent and directly readable from quoted code, and the derived arithmetic (60 heartbeats per full stream, two reconnects and two count reads per connected hour) follows from stated intervals. It is capped by a one-publisher cluster, self-reported adoption, absent measurements, and a truncated source body that cuts off inside the Redis listener, leaving parts of the implementation unseen.
build
The /userinfo fallback that quietly made Auth0 a hard dependency on every request1 distinct publisher
build
A 28-host Debian 12 cutover, and the 02:13 failure Ansible could not have prevented1 distinct publisher
security
Two Artifactory flaws poisoned metadata, not artifacts, and that was enough to break a shared cache1 distinct publisher
build
The 680 MB database that was really a 17 GB disk: self-hosted support platforms fail at month six1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 23, 2026