Event-driven architecture is nothing new. IBM MQ shipped in 1993. JMS has been around since 1998. Kafka launched in 2011.
But for most of that history, event-driven patterns were for specialized domains. Most developers never touched them.
That's changed. Real-time mobile features, such as chat, activity feeds, live collaboration, or presence indicators, have pushed event-driven patterns into everyday app development. If you've spent most of your career building REST APIs, you're now being asked to handle fan-out, persistent connections, message ordering, and continuous state synchronization.
The mental models don't transfer. A REST endpoint under heavy load slows down or throws errors, but the damage stays contained. Event-driven systems cascade. A slow consumer backs up the queue, triggering retries that double the load on an already-struggling service. Here's what makes these workloads different, how they break, and how to keep them stable.
Why Event-Driven Workloads Differ from Traditional APIs
A standard REST API handles discrete request/response pairs. A client sends a request, the server processes it, and the connection closes.
Load balancers can distribute these requests freely across any available server because each one is independent and stateless. Event-driven real-time systems violate every one of these assumptions.
Fan-Out Multiplies Load Non-Linearly
In a pub/sub system, a single published event can trigger delivery to hundreds or thousands of subscribers simultaneously. When a user posts a message in a group channel, the event is broadcast to every member's device. For example, an SNS-to-SQS fan-out on AWS distributes a single event to multiple independent queues, each processed by a different consumer.
This creates scaling issues that don't exist in request/response architectures. In real-time chat systems, if each user can message any other user, the number of potential message paths scales quadratically with the number of users.
| Where n is the number of connected users, and P is the number of potential message paths. In practice: | Connected Users (n) | Potential Message Paths | Growth Factor vs. Previous |
|---|---|---|---|
| 100 | 9,900 | — | |
| 1,000 | 999,000 | ~101x | |
| 10,000 | 99,990,000 | ~100x | |
| 100,000 | 9,999,900,000 | ~100x |
Every 10x increase in users produces a ~100x increase in potential message paths. A system handling 10,000 concurrent users has a fundamentally different resource profile than one handling 100,000, even if the per-user message rate is identical. In practice, channel-based architectures (where users subscribe to specific topics rather than maintaining paths to every other user) reduce the effective fan-out, but the underlying quadratic scaling remains the constraint that channel design must work against.
What can you do?
-
Partition by event type so that a spike in one event category (say, typing indicators) doesn't starve delivery of another (say, message sends). Each partition scales independently.
-
Run multiple event buses in parallel to eliminate the single-broker bottleneck. If one bus is saturated, others continue processing unaffected traffic.
-
Deploy event mesh architectures that distribute events across a network of interconnected brokers rather than routing everything through a central bus.
Distributed infrastructure designed for high-throughput event workloads is often required to handle this scale reliably.
Persistent Connections Change Every Scaling Assumption
HTTP connections are short-lived and stateless. WebSocket connections are persistent, stateful, and full-duplex. Each one consumes memory, bandwidth, and CPU for the duration of the session, which can last hours or days.
This distinction has implications for how you scale.
For one, load balancing gets harder. WebSocket connections require sticky sessions to keep each client routed to the server that holds its state. That works until a server fails and every client pinned to it disconnects at once, or until a rolling deployment forces you to drain all active sessions before shutting down a node.
Heartbeats also add up. Every connection needs regular keep-alive pings, which means at millions of concurrent connections, heartbeat frequency becomes a direct constraint on server capacity.
Reconnection storms then compound these failures. When an outage resolves, tens of thousands of devices try to reconnect at once, which can overwhelm the infrastructure and trigger a second failure. Without exponential backoff and rate limiting on reconnects, recovery causes exactly the problem it's trying to fix.
Burst Traffic is the Default, Not the Exception
Traditional API traffic tends to follow predictable daily curves. Event-driven traffic doesn't. It's inherently bursty, with usage patterns that shift based on application context, user behavior, or external events. A real-time feature might see 5x normal traffic during a viral moment, product launch, or live event.
The failure pattern during spikes tends to follow a predictable sequence:
-
Traffic surges beyond baseline capacity.
-
Serverless function cold-starts introduce latency.
-
Message queues back up as consumers can't keep pace.
-
Database throttling kicks in independently.
-
Customer-facing operations start failing.
Each step compounds the previous one, and the cascade plays out across domains: SaaS platforms during feature launches, fintech during fraud spikes, media platforms during live broadcasts. The trigger varies, but the failure shape is consistent.
Continuous Synchronization Replaces Request/Response
In a traditional architecture, the client asks a question and the server answers it. Between requests, nothing changes.
Event-driven systems work differently: the server pushes updates continuously, and the frontend has to keep up. That means coordinating local state with server and peer updates across active sessions, handling message ordering, deduplication, and conflict resolution, all in real time. The browser becomes a participant in a distributed system, not just a rendering surface.
The throughput numbers at the top end of this space set the context for what "scale" means here:
| Platform | Throughput | Source |
|---|---|---|
| Spotify | ~8 million events/sec (~500 billion/day) | Distributed event delivery pipelines |
| Trillions of events/day | Kafka-based infrastructure | |
| Slack | Billions of messages/day | Kafka + Redis |
These are sustained throughput numbers, not burst peaks.
Preventing Cascading Failures Under Load
The most dangerous property of event-driven systems is that failures propagate. A slow consumer can back up a queue, triggering retries, which in turn amplify the load on an already struggling service, causing timeouts.
Within minutes, a localized issue becomes a system-wide outage.
Preventing this requires multiple layers of defense, each addressing a different failure mode.
Backpressure: Controlling the Flow Before it Overwhelms Consumers
With millions of connected clients, data flow through the system can reach billions of messages per hour. Without flow control, producers overwhelm consumers, queues fill up, and the system collapses.
Effective backpressure operates at multiple levels:
-
Queue depth monitoring triggers scaling or throttling when message backlogs exceed configured thresholds. Kafka consumer lag is a primary signal; when it exceeds a threshold, autoscaler policies spin up additional consumer pods.
-
Rate limiting at ingestion controls flow to downstream services. Token bucket algorithms cap throughput at sustainable levels, allowing short bursts while preventing sustained overload.
-
Adaptive queues adjust buffer sizes and consumer throughput dynamically in response to load signals. Research on resilience patterns shows that static isolation limits blast radius but reduces utilization, while adaptive approaches improve efficiency under variable load.
-
Priority-based processing discards low-priority events during overload to protect capacity for critical operations.
A common failure mode is designing for average workloads rather than operational edge cases. If your system handles 10,000 events per second comfortably, but a spike pushes it to 50,000, the design needs to account for 50,000, not 10,000.
Queue Buffering: Decoupling Production from Consumption
Message queues (SQS, Kafka, RabbitMQ) serve as the primary shock absorber between producers and consumers. The buffer decouples the production rate from the consumption rate, so a burst of events doesn't immediately translate into a burst of processing.
Dead letter queues (DLQs) quarantine poison messages after retry budgets are exhausted, preventing a single malformed event from blocking an entire pipeline. The recommended approach is to track DLQ size as a key operational metric and set alerts that fire before it grows large enough to indicate a systemic problem.
Dedicated real-time platforms typically implement server-side flow control and isolation to prevent cascading failures during traffic spikes.
Isolation Boundaries: Containing the Blast Radius
There are three separate options for isolating failures:
-
The bulkhead pattern gives each service or consumer group its own isolated resource pool: dedicated thread pools, connection pools, and consumer infrastructure. If one service fails or slows down, only its resources are exhausted while others continue functioning.
-
For fan-out workloads specifically, isolation means decoupling consumers via separate queues and dedicated infrastructure. Without this, a single slow consumer sharing a resource pool can create backpressure that affects every other consumer on the same bus.
-
Shuffle sharding takes this further by partitioning consumers into overlapping subsets. A failure in one shard affects only a small percentage of traffic rather than an entire consumer group. This is particularly valuable for multi-tenant real-time systems where a single noisy tenant shouldn't degrade service for everyone else.
Circuit Breakers: Failing Fast Instead of Failing Slow
A slow dependency is more dangerous than a dead one. A dead service fails fast. A slow one ties up threads and connections across every caller, waiting for responses that may never come. The circuit breaker pattern addresses this by tracking success and failure rates over time and cutting off calls to a struggling service before the problem spreads.
It works as a state machine. In the closed state, requests flow normally, and failures are counted. When the failure rate crosses a threshold (typically 50% within a sliding window), the breaker trips to open: requests fail immediately with a fallback response instead of waiting. After a timeout, it enters a half-open state, allowing a small number of test requests through. If they succeed, the breaker resets. If they fail, it reopens.
What makes circuit breakers more effective than per-request retry logic is that they share state across all requests to the same backend. If one request fails, every subsequent request benefits from that knowledge immediately. Netflix's Hystrix, released in 2012, was the first dedicated circuit-breaking middleware, and the pattern has since become standard in every major service mesh and resilience library.
Retry Storms: The Failure That Looks Like Recovery
Retries, a mechanism designed to improve reliability, can make failures catastrophically worse.
When a service starts failing, clients retry. If multiple clients retry simultaneously without randomization, they create a synchronized burst of requests, a retry storm, that amplifies load on the already struggling service. In deep call chains, the amplification is exponential: one failure times 3 retries per layer across 4 layers produces 81 retry attempts from a single original request.
The fix is jitter combined with retry budgets (capping the total number of retries per time window) and circuit breakers that prevent retries when the downstream service is known to be unavailable. The Amazon DynamoDB outage in 2015 is a textbook example: a feedback loop between storage servers and a metadata service cascaded into a four-hour outage.
Dedicated real-time platforms typically implement server-side flow control and isolation to prevent these cascading failures during traffic spikes, removing the burden of building and tuning these patterns from application teams.
Designing for Traffic Spikes and Viral Growth
A mobile app that goes viral presents a specific infrastructure problem: the increase in traffic is sudden, large, and unpredictable. Traditional capacity planning, where you provision for expected peak load plus a safety margin, doesn't work when "peak" can be 10x or 50x your baseline overnight.
Horizontal Scaling: Adding Machines, Not Bigger Machines
Vertical scaling (adding more CPU and RAM to a single server) is simple but has hard limits. You eventually hit the largest available instance type, and a single machine is always a single point of failure. Horizontal scaling (adding more machines to distribute load) is the foundation for handling unpredictable growth.
For event-driven workloads, horizontal scaling requires specific architectural choices:
-
Pub/sub as the distribution layer. The most common pattern for horizontally scaling WebSocket systems is the pub/sub model: publishers and subscribers are decoupled by a message broker that groups messages into channels. This allows you to add WebSocket server nodes without each node needing direct knowledge of every other node.
-
Stateless application logic behind a stateful connection layer. WebSocket connections are inherently stateful, but the business logic they serve doesn't have to be. A hybrid architecture with stateful real-time gateways and stateless backends lets the backend layer scale freely while the gateway layer manages connection affinity.
-
Connection-aware load balancing. Unlike HTTP, where round-robin distribution works well, WebSocket connections require load balancers that account for connection duration, per-connection memory, and the current connection count per server.
Autoscaling: Faster Reaction, But Not Fast Enough Alone
Autoscaling dynamically adjusts compute resources based on demand. The mechanisms differ by what triggers the scaling:
-
Reactive autoscaling (the most common) monitors metrics like CPU usage or queue depth and adds instances when thresholds are breached. The fundamental limitation is latency: provisioning new instances takes time to boot, initialize, and warm caches. Very short-lived spikes may resolve before new capacity comes online.
-
Predictive autoscaling uses machine learning to forecast future load based on historical patterns and scales infrastructure in advance of predicted traffic. Google Cloud's Compute Engine offers this capability, and it works well for predictable patterns (daily peaks, weekly cycles), but it can't anticipate truly unexpected viral events.
-
Event-driven autoscaling with tools like KEDA (Kubernetes Event-Driven Autoscaling) triggers scaling based on event-specific metrics such as Kafka consumer lag, SQS queue depth, or custom application signals. This is more precise than CPU-based scaling for event-driven workloads because it responds to the actual backlog rather than a downstream symptom of it.
Stateless Services: Making Horizontal Scaling Possible
For horizontal scaling to work, application servers must be stateless. Any server must be able to handle any request without depending on data from previous interactions on that specific machine.
This means session data is stored in Redis or Memcached, not in local memory. Static assets go into object storage (S3, GCS), not the local filesystem. Application state is retrieved from the database or cache on every request, not from an in-memory variable set by the previous request. Load balancers distribute requests evenly, and if a server dies, another picks up the load without the user noticing.
Many teams adopt infrastructure built specifically for high-volume interactive traffic rather than adapting traditional request/response architectures.
The tension with real-time systems is that WebSocket connections are inherently stateful. The practical resolution is to externalize the connection state to a shared store while keeping the application logic itself stateless. When a client reconnects to a different server after a disconnection, the new server can reconstruct the session from shared state.
Avoiding Database Bottlenecks
The database is typically the first bottleneck as an application scales. Read traffic grows faster than write traffic, and what starts as harmless repeated queries eventually overwhelms the database. Every user action that hits the database directly, without caching, becomes unsustainable at high concurrency.
Strategies stack in layers of increasing complexity:
-
Caching (Redis, Memcached) stores the results of frequent, expensive queries in memory. This is the highest-leverage change for most applications because it eliminates repeated database round-trip for infrequently changing data.
-
Read replicas offload read queries to multiple copies of the primary database. For read-heavy, real-time workloads (activity feeds, notification timelines, presence indicators), replicas can handle most traffic while the primary handles writes.
-
Sharding partitions the database horizontally across multiple servers, with each server holding a subset of data. This is necessary when a single database instance can't handle the write throughput, but it adds significant operational complexity around query routing, rebalancing, and cross-shard queries.
-
CQRS (Command Query Responsibility Segregation) separates write models from read models entirely. Write models handle commands and events; read models are projections optimized for query patterns. This is a natural fit for event-driven systems, where the write path (publishing events) has different performance characteristics from the read path (querying the current state).
Many teams adopt infrastructure built specifically for high-volume interactive traffic rather than adapting traditional request/response architectures. Purpose-built real-time infrastructure handles connection management, fan-out, and state synchronization at the infrastructure layer, so application code doesn't need to solve these problems from scratch.
Multi-Region Reliability and Global Latency
A mobile app serving users across multiple continents from a single region introduces latency that real-time features can't tolerate. A single-region setup serving users in Europe and Asia from US-East-1 can add 200ms or more to every interaction, degrading experiences that depend on sub-100ms response times. Multi-region deployment solves latency and redundancy simultaneously, but it introduces its own complexity.
Active-Active vs. Active-Passive
Two primary architecture patterns exist for multi-region deployments, and the choice between them shapes every subsequent infrastructure decision.
-
Active-active runs all regions simultaneously, each handling live traffic. This provides maximum uptime and the lowest latency (users always hit their nearest region), but it requires solving cross-region data replication and conflict resolution. When two users in different regions modify the same data simultaneously, the system needs a strategy for reconciling those writes.
-
Active-passive designates one primary region for all traffic, with secondary regions on standby for failover. This is simpler and cheaper and avoids conflict resolution entirely. Still, recovery times during failover are longer, and the secondary region may not be fully warm when it needs to take over.
A middle ground, the warm standby model, keeps the secondary region running at reduced capacity. One case study measured this approach at approximately 50% cheaper than full active-active while still achieving a Recovery Time Objective (RTO) of 2-5 minutes and a Recovery Point Objective (RPO) under 30 seconds.
Global platforms commonly rely on multi-region deployments and automated failover to maintain reliability across geographies.
Failover That Works at 3 AM
Failover strategies fall into two categories: DNS-based and network-based.
DNS-based failover (e.g., Route 53 health checks) redirects traffic at the DNS layer when a region becomes unhealthy. The problem is propagation delay: DNS records are cached by clients and intermediate resolvers, so traffic continues flowing to the failed region for minutes after the failover is triggered.
Network-based failover via load balancing avoids DNS propagation issues entirely. AWS Global Accelerator provides anycast IPs that route traffic through the AWS backbone, reducing latency by 20-50ms compared to the public internet and enabling near-instant failover.
The most important lesson from production multi-region deployments: failover must be automated and tested regularly. Organizations that have never tested a regional failure are running on a hypothesis, not a capability.
Data Locality and Traffic Routing
How you route traffic to different regions determines both latency and compliance:
-
Latency-based routing sends each request to the region with the lowest measured latency, dynamically optimizing the user experience.
-
Geolocation routing directs requests to the nearest geographic location. This also satisfies data residency requirements: over 200 data privacy regulations worldwide mandate storing certain data in specific physical locations, including the GDPR for EU citizens and the CCPA for California residents.
-
Weighted routing distributes traffic based on configured weights, useful when some regions have more capacity than others or during gradual rollouts.
Within a region, availability zones connected by sub-5ms inter-zone links enable synchronous data replication and real-time failover without significant latency impact. Across regions, latency is higher, and replication is typically asynchronous.
Geo-Distribution for Real-Time Workloads
Real-time systems face a specific tension in multi-region deployments: maintaining data consistency across regions while keeping latency low enough for interactive use. A banking workload migrated to active-active Redis on AWS targets single-digit millisecond write latency in each region, sub-10ms for local reads (70% of the workload), and sub-20ms for cross-region reads requiring conflict resolution (30% of the workload).
For latency-sensitive applications like real-time gaming and chat, servers distributed across local data centers can reroute traffic during outages instantly, with transparent load balancing and near-real-time data replication, preserving session continuity.
One hidden failure mode: drift between regions in permissions, infrastructure configuration, and IAM policies. This drift is invisible during normal operations and catastrophic during failover. Infrastructure-as-Code (Terraform, CloudFormation) enforced in CI/CD pipelines is the primary defense.
Global platforms commonly rely on multi-region deployments and automated failover to maintain reliability across geographies. For teams building real-time mobile features, the latency and availability requirements make multi-region architecture a necessity, not an optimization.
The Operational Cost of Running Real-Time Infrastructure
Everything discussed so far, fan-out, connection management, circuit breakers, and multi-region failover, creates an operational surface that someone has to monitor, maintain, and respond to around the clock. Most teams underestimate this cost because so much of it is invisible during project planning.
24/7 Monitoring Across a Wider Surface Area
Real-time infrastructure introduces monitoring dimensions that traditional request/response systems don't have. On top of the usual metrics (request latency, error rates, CPU, and memory), you're now tracking WebSocket connection counts and health, consumer lag per topic and partition, fan-out delivery latency, dead-letter queue depth, and cross-region replication lag.
The toolchain to cover all of this typically includes centralized logging (Elastic Stack, Datadog, Loki), metrics dashboards (Prometheus, Grafana), and distributed tracing (OpenTelemetry, Jaeger). Every one of these needs configuration, maintenance, and tuning as the system evolves.
And there's a recursive problem that's easy to overlook: you need to monitor your monitoring system. The infrastructure that runs your alerting needs its own alerting. Managed platforms have solved this already. Self-hosted observability stacks haven't.
On-Call Rotations: The Hidden Cost Center
On-call for real-time infrastructure is more demanding than for traditional services because failures are more visible to users (dropped messages, stale feeds, broken presence indicators) and more likely to cascade.
The numbers are sobering:
-
65% of engineers reported experiencing burnout in the past year, with on-call stress as a major contributing factor.
-
Engineers on high-frequency on-call rotations have 40-50% higher voluntary turnover than those who aren't on rotation.
-
Replacing a senior engineer costs 50-200% of their annual salary, or $90K-$360K for a $180K engineer, when you account for recruiting, onboarding, lost productivity, and knowledge transfer.
-
During on-call periods, engineers allocate 30-40% of their bandwidth to incident responsibilities, reducing their capacity for feature work.
The context-switching overhead alone is significant: each page costs an average of 23 minutes of recovery time before the engineer regains full cognitive focus. Postmortem writing while fatigued takes 5-7 hours instead of 3-4. When you sum up the visible and invisible costs, a single mid-size engineering team's total on-call cost exceeds $155,000 per year, and most organizations have never calculated this number.
Google's SRE book recommends no more than 2-3 incidents per on-call shift. Consistently exceeding that threshold means the rotation, or the system underneath it, is unsustainable.
Incident Response and Coordination Overhead
Each incident costs more than the time spent fixing it. Before anyone starts troubleshooting, there are 10-15 minutes of coordination: acknowledging alerts, opening a channel, determining who should look into it, and getting everyone up to speed on what's happening.
Then there's the write-up. Post-incident reporting adds 6-8 hours per incident. That's time a senior engineer isn't spending on product work. The automation math here is hard to argue with: cutting postmortem time from 6 hours to 30 minutes across 6 incidents a month frees up 33 engineering hours, roughly $30,000 a year.
The shift that mature organizations make is straightforward: stop engaging humans for predictable problems. Routine restarts, rollbacks, and scaling events make up the bulk of alert volume. These should resolve automatically, with audit trails for review, so that engineers are paged only for problems that actually require judgment.
Capacity Planning is Continuous, Not Periodic
With traditional services, you can plan capacity quarterly and adjust as needed. Real-time infrastructure doesn't give you that luxury, because the resource model is fundamentally different:
-
WebSocket connections hold resources for the entire session (hours or days), not per request (milliseconds). Your baseline resource consumption is tied to the number of connected users, not to request volume.
-
Burst traffic can hit 5-10x the baseline, and predicting when it will hit is often impossible.
-
Cross-region replication adds bandwidth and storage costs that grow with write volume, not read volume.
-
Serverless cold starts introduce latency during scale-up, meaning critical paths rely on pre-warmed capacity that sits idle until it's needed.
The Build vs. Managed Decision
A survey of 500+ engineering leaders found that 93% of real-time infrastructure projects needed at least 4 engineers, 70% took more than 3 months, and 21% stretched past six months. Engineers at LinkedIn, Slack, and Box who had built real-time infrastructure in-house all described it the same way: significant upfront engineering with high ongoing operating costs.
The ongoing part is what catches teams off guard. Maintenance accounts for over 50% of the total cost of ownership for any software system. For infrastructure that requires 24/7 uptime, on-call coverage, and continuous capacity management, that share is even higher.
The fully loaded cost of a software engineer (salary, benefits, equipment, office space, management overhead) is about 2.7x the base salary. Two engineers spending six months on real-time infrastructure represents roughly $189,000 in direct costs, before the system handles a single production event, and before you account for what those engineers could have shipped instead.
For teams that don't view infrastructure reliability as a core differentiator, managed providers can significantly reduce operational burden and ongoing maintenance complexity.
The decision framework is simple: build when real-time infrastructure is your product. When it's a means to an end, when your product is a chat experience, an activity feed, a live collaboration feature, the engineering time is better spent on the things your users actually see.
Managed providers like Stream handle fan-out, connection management, multi-region failover, and 3 AM pages, so your team can focus on what makes your product different.
