Real-Time Balances: WebSockets vs SSE vs Long Poll
Choosing the wrong real-time protocol for balance feeds costs you battery life, server money, and user trust. Here's the breakdown that actually matters.

Knowing your exact balance — not the balance from three minutes ago — is table stakes for modern fintech. When a user taps "pay" at a coffee counter, they need to see their post-transaction balance before they put their phone back in their pocket. That's a 400-millisecond expectation, not a 4-second one. Building the infrastructure to meet it, reliably, at scale, is harder than most engineering blog posts admit.\n\nThis post is the unvarnished version. We'll walk through WebSockets, server-sent events (SSE), and long polling — their latency profiles, infrastructure costs, and what they do to your users' phone batteries — and then explain the fan-out topology AtlasForge Financial uses to serve real-time balances to more than 2 million concurrent connections without the architecture turning into a distributed systems horror story.\n\n## Why Protocol Choice Is a Product Decision\n\nMost engineering teams treat the transport layer as a pure infrastructure decision, made once in a planning doc and never revisited. That's a mistake. Your choice of real-time protocol directly shapes three things users feel:\n\n1. Staleness anxiety — the nagging sense that the number on screen might be wrong.\n2. App responsiveness — whether the UI feels live or feels like a web page from 2009.\n3. Battery drain — a real-money cost that users attribute to your brand, not your protocol.\n\nIt also shapes three things your finance team feels: egress costs, server-side connection overhead, and the complexity tax your on-call engineers pay at 2 a.m.\n\nNone of those trade-offs are identical across WebSockets, SSE, and long polling. Let's be specific.\n\n## Long Polling: The Baseline You're Probably Still Running\n\nLong polling works by having the client send an HTTP request, the server hold it open until new data is available (or a timeout fires), return the response, and repeat. It's been the backbone of "real-time" fintech for longer than most practitioners want to admit.\n\n### The honest performance profile\n\n- Latency: 200–800 ms end-to-end on mobile, dominated by TCP connection re-establishment after each response. On congested LTE networks, median latency in our own telemetry hit 620 ms — well past user-perceptible thresholds.\n- Server cost: Every reconnect is a full HTTP handshake. At 2 million concurrent "sessions," you're managing tens of millions of short-lived connections per hour. Our load tests showed a 4× higher per-user CPU cost compared to persistent connections.\n- Battery: Frequent radio wake events are brutal. A long-poll loop on a 30-second timeout cycles the cellular radio roughly 120 times per hour. Android's Battery Historian traces this as a top-3 wakelock offender in apps we profiled.\n\nLong polling is defensible in exactly one scenario: your backend is behind infrastructure you don't own (a legacy core banking system, a third-party ledger API) that exposes only request/response semantics. In that case, it's not a protocol choice — it's a constraint. Acknowledge it as such and plan the migration.\n\n## Server-Sent Events: The Underrated Middle Path\n\nSSE is a unidirectional, HTTP/1.1-compatible streaming protocol. The server holds a single persistent connection open and pushes newline-delimited data: frames whenever it has something to say. The client cannot send messages back over the same channel.\n\nThat unidirectionality sounds like a liability. For balance feeds, it's often an asset.\n\nBalance data flows in one direction: server to client. The client doesn't need to send acknowledgments, heartbeats, or sub-channel subscriptions over the same pipe. A separate REST call handles anything the client needs to initiate. SSE's simplicity means:\n\n- Automatic reconnection is built into the browser and most mobile HTTP clients. Drop the connection, and the EventSource interface re-establishes it using the Last-Event-ID header, so you don't re-deliver events the client already received.\n- HTTP/2 multiplexing lets you fan multiple SSE streams over a single TCP connection — critical on mobile where connection count is throttled.\n- Proxy and CDN compatibility is far better than WebSockets. Most enterprise firewalls and CDN edges (Cloudflare, Fastly, AWS CloudFront as of its 2024 HTTP/2 streaming GA) pass SSE without configuration changes. WebSocket upgrades still get dropped by a non-trivial share of corporate proxies.\n\n### SSE latency in practice\n\nIn our internal benchmarks (AWS us-east-1, clients simulated from us-west-2 and eu-west-1), SSE median event delivery latency was 47 ms from the moment our balance-update event hit the message bus to the moment the client's onmessage handler fired. P99 was 180 ms. That's comfortably inside the 400 ms threshold where users perceive a UI as "instant."\n\n> Callout: SSE's Achilles heel is bidirectional workflows. If your balance feed needs to support client-driven filtering — "only send me updates for account IDs X, Y, Z" — you'll either pass that as a query parameter at connection time or layer a separate REST/WebSocket channel on top. Neither is elegant, but it's a manageable seam.\n\n## WebSockets: Full Duplex, Full Responsibility\n\nWebSockets give you a persistent, full-duplex TCP connection after an HTTP upgrade handshake. Both sides can send frames at any time. For balance feeds, the relevant advantages over SSE are:\n\n- Bidirectional subscription management — the client can send a subscribe frame mid-session without tearing down and rebuilding the connection.\n- Binary frame support — if you're pushing dense balance snapshots (think: 50 accounts, each with multi-currency sub-balances), Protocol Buffers over WebSocket binary frames can reduce payload size by 60–70% versus JSON over SSE.\n- Lower per-message overhead — WebSocket frames have a 2–10 byte header versus SSE's newline-delimited text framing.\n\n### Where WebSockets hurt you\n\nConnection state is your problem. Every WebSocket connection is a stateful object your servers must track. At 2 million concurrent connections, naive implementations collapse. A single Node.js process can hold roughly 65,000 open sockets before file descriptor limits bite. You need a dedicated WebSocket gateway tier — and that tier must share state with your application servers somehow (more on this in the fan-out section).\n\nSticky sessions or shared state — pick your poison. If your load balancer routes a reconnecting client to a different WebSocket server, that server has no record of the client's subscriptions. You can solve this with sticky sessions (fragile, anti-scale) or by storing subscription state in a shared store like Redis (operationally correct, adds latency and a failure dependency).\n\nMobile battery is worse than SSE. WebSocket keepalive pings — necessary to prevent NAT tables and mobile firewalls from killing idle connections — fire every 20–30 seconds by default in most server libraries. Each ping/pong is a radio wake event. Our Android telemetry showed a 22% higher battery drain for WebSocket-connected sessions versus SSE sessions at equivalent update frequencies.\n\nThe IETF RFC 6455 spec governing WebSockets is mature and well-supported, but "supported" doesn't mean "free." Every feature you gain over SSE comes with an operational cost someone on your team will pay.\n\n## The Decision Matrix\n\nHere's how we'd frame the choice for a balance-feed use case specifically:\n\n- Use SSE if: Your balance updates are server-initiated, your clients are browsers or modern mobile apps, and you want the simplest possible failure mode. This covers roughly 80% of consumer fintech balance dashboards.\n- Use WebSockets if: You need bidirectional mid-session control (dynamic account subscriptions, client-side ack for regulatory audit trails), or you're pushing high-frequency binary payloads (trading platforms, treasury dashboards with 100+ account lines).\n- Use long polling if: You're behind infrastructure that doesn't support persistent connections and you need something working by Friday. Budget the refactor.\n\n## Fan-Out Architecture: How We Scaled to 2M Concurrent Balances\n\nProtocol choice is only half the battle. The harder problem is fan-out: when a single ledger event (one ACH settlement, one card authorization) should update the balance display for potentially thousands of clients — think a family account with shared sub-accounts, or a business account viewed simultaneously by a CFO, controller, and three AP clerks.\n\nHere's the topology we run in production at AtlasForge Financial:\n\n1. Ledger event source: Core banking events land on a Kafka topic (balance.updated) with account ID as the partition key. This guarantees ordering per account.\n2. Balance computation layer: A stateless consumer pool reads from Kafka, fetches the authoritative balance from our ledger database, and publishes a normalized BalanceEvent (account ID, new available balance, new ledger balance, timestamp, idempotency key) to a Redis Pub/Sub channel keyed by account ID.\n3. SSE gateway tier: A horizontally scaled pool of Go-based SSE servers, each holding up to 50,000 open client connections. Each server subscribes only to the Redis channels corresponding to its connected clients' account IDs. When a BalanceEvent arrives on a subscribed channel, the server fans it out to all connected clients watching that account.\n4. Client subscription registry: A lightweight Redis hash maps {account_id} → {set of gateway server IDs} — updated on connect and disconnect. This lets us route targeted publishes rather than broadcast everything everywhere.\n5. Presence and health: Gateway servers emit a heartbeat to the registry every 10 seconds. A client that loses its connection within 30 seconds — common during mobile network handoffs — reconnects using Last-Event-ID, and the gateway replays any missed events from a short Redis Stream buffer (TTL: 5 minutes).\n\nThis architecture keeps fan-out local to each gateway server for the common case (one account, one viewer) and cross-server only when multiple gateways hold connections for the same account. In practice, cross-server fan-out accounts for fewer than 3% of events — the Redis Pub/Sub publish-subscribe model handles it gracefully without a dedicated message router.\n\nThroughput numbers from our January 2027 capacity review: The fan-out layer processed a peak of 840,000 BalanceEvent messages per minute during a Friday afternoon settlement window, with a median gateway-to-client delivery latency of 38 ms and zero dropped events (verified via idempotency key reconciliation in our audit log).\n\nFor deeper reading on Kafka-based event streaming patterns in financial services, the Federal Reserve's FedNow Service Operator documentation is worth a read — it shaped several of our ordering guarantees. The CFPB's 2023 Open Banking rule also has implications for how balance data must be made available in real time, and compliance teams should have it on their radar.\n\n## Operational Gotchas You Won't Find in the README\n\n- SSE and HTTP/2 flow control: If a slow client can't consume events fast enough, HTTP/2 flow control will back-pressure your gateway. Set a per-client send buffer limit and drop-or-disconnect slow consumers aggressively. A stalled connection that holds a subscription is more expensive than a reconnect.\n- Redis Pub/Sub vs. Redis Streams: Pub/Sub is fire-and-forget — if your gateway server is restarting when an event publishes, it misses the message. This is why we buffer into Redis Streams (with MAXLEN ~5000 per account) and replay on reconnect. Don't skip this.\n- WebSocket ping tuning on mobile: If you do use WebSockets, extend your server-side ping interval to 55 seconds on mobile connections (detect via User-Agent or a client capability header). This keeps NAT tables alive on most carrier networks (NAT timeout floors are typically 60 seconds per IETF RFC 4787) while halving unnecessary radio wakeups.\n- Egress costs: At 2 million concurrent SSE connections sending a 200-byte event every 10 seconds, you're looking at roughly 3.4 TB/hour of egress. At AWS us-east-1 pricing (\$0.09/GB as of Q1 2027), that's \$306/hour — before CDN discounts. Event compression (gzip or Brotli over HTTP/2) cuts this by 65–75% for JSON payloads. Do not skip compression.\n\n## What This Means for Your Stack\n\nIf you're evaluating how to integrate real-time balance infrastructure without building this fan-out topology yourself, the AtlasForge Financial API exposes both an SSE endpoint (GET /v1/accounts/{id}/balance/stream) and a WebSocket endpoint (wss://stream.atlasforge.io/v1/balance) — with the fan-out, Redis buffering, replay, and compression layers already handled. Our platform overview walks through the SLA commitments (99.95% uptime, P99 delivery latency under 200 ms) and the compliance posture.\n\nFor teams building consumer-facing balance dashboards specifically, Safe to Spend 365 is the AtlasForge product that surfaces real-time spendable balance — distinct from ledger balance — using exactly this infrastructure, with a UI component library you can white-label. If you're exploring what "real-time" actually means in your product context, our blog on balance semantics has the companion piece to this one.\n\nThe engineering decisions described here weren't made in a vacuum — they were made after our on-call rotation got paged too many times and our mobile users started leaving one-star reviews mentioning battery drain. Protocol choice is a product decision. Treat it like one.
Further reading
Ready to build on AtlasForge?
Get sandbox API keys in 60 seconds — or install the Safe to Spend 365 app.
