Key Takeaways
- Event-driven architectures often break under pressure due to retries, backpressure, and startup latency, especially during load spikes.
- Latency isn’t always the problem; resilience depends on system-wide coordination across queues, consumers, and observability.
- Patterns like shuffle sharding, provisioning, and failing fast significantly improve durability and cost-efficiency.
- Common failure modes include designing for average workloads, misconfigured retries, and treating all events equally.
- Designing for resilience means anticipating operational edge cases, not just optimizing for happy paths.
Event-driven architectures (EDA) look great on paper, as they have decoupled producers, scalable consumers, and clean async flows. But real systems are much messier than that.
Consider this common scenario: during a Black Friday event, your payment processing service receives five times the normal traffic. When that happens, your serverless architecture hits edge cases. For example, Lambda functions cold-starts, your simple queue service (SQS) queues back up as a result, and independently, you see DynamoDB throttles. Somewhere in this chaos, customer orders start failing). This isn't a theoretical problem, it's a normal day for many teams.
And it's not limited only to eCommerce. In SaaS platforms, feature launches lead to backend config spikes. In the FinTech business, which sees a huge influx of events during some fraud activity, even a few milliseconds makes a big difference. We can find similar examples (popular media broadcasts, live events like the Super Bowl) in our day-to-day life that follow similar patterns.
If you look at a high-level picture of the system, it’s fundamentally broken into three parts: producer, intermediate buffer, and consumer.
When you talk about resilience in these systems, it isn't just about staying available; it's also about staying predictable under pressure. Traffic spikes because of some upstream integrations, downstream hitting some bottlenecks, or some components doing unbounded retries all test how well your architecture can handle the spike. But real systems have their own opinions.
In this article, we will talk about how to think about building resilient and scalable event processing systems. We will look at different operational events that disrupt the reliability and scale, and use the learnings from them to design a better system.
Latency Isn’t the Only Concern
More often than not, when people talk about performance in event-driven systems, they talk about latency. But they forget that latency is only part of the story. For resilient systems, throughput of your system, how well your resources are utilized, and how well data is flowing between components matter equally.
Let's consider this example. You own a service whose underlying infrastructure depends on an SQS queue. Suddenly, there is a spike in traffic that overwhelms the downstream systems, leading to their full or partial failure, and that failure leads to inflated retries and subsequently causes monitoring data to skew. Additionally, if your consumer has a high startup time, whether due to cold start or container load time, you now have contention between messages that need fast processing and infrastructure that’s still getting ready. Now, if you think about it, the failure mode is not the timeout. Rather, the failure is the setup that shows up as lag, retries, and increased cost to customers.
Now let's add either dead letter queues (DLQs), exponential backoff, throttling policies, or stream partitions into the mix, and the problem becomes more complex. So, instead of debugging a function, you are figuring out what the different contracts are and what might be going on.
To design for resilience, we need to treat latency as a signal of pressure building up in the system. It shouldn’t only be about minimizing it. That shift in mindset is needed.
Given all this, let's look at some of the practical approaches that can be used to address the concerns identified:
Patterns That Scale Under Pressure
When talking about resiliency, I want you to think beyond just fixing things like reducing latency, tuning retries, or lowering failures. Consider designing a system that degrades gracefully when met with an unseen scenario and recovers automatically. Let’s talk about some of these patterns at different layers of your architecture:
Design patterns
Shard and shuffle shard
One of the foundational concepts in resilient system building is to degrade gracefully while also containing the blast radius. One way to do that is to segment your customers and make sure a problematic customer is not taking the whole fleet down. You can take your design a step further and add shuffle sharding. Shuffle sharding is assigning customers to a random subset of shards and, by doing that, reducing the probability of well-behaved customers fully colliding with noisy customers. Async systems which are backed by queues, for example, often hash all their customers to a handful of queues. When a noisy customer comes, it overwhelms the queue and in turn impacts every other customer who is also hashed onto that same queue. By applying shuffle sharding, the probability of a noisy customer falling onto the same shards as another customer drops drastically; the isolated failure minimizes the impact to others. You can see this concept in action in this blog: Handling billions of invocations – best practices from AWS Lambda.
Provisioning for Latency-Sensitive Workload
Provisioning means pre-allocating resources. It’s similar to reserving EC2 capacity upfront. It has a cost associated with it, so you need to be careful. Not all workloads need provisioned concurrency, but some may. In the FinTech industry, for example, fraud detection systems often rely on real-time signals, so if a fraudulent transaction is not flagged within seconds, it can damage the whole system. In that case, identify paths where seconds matter and invest accordingly. You can take it a notch up and use autoscaling in provisioned concurrency to further make it cost-effective if the workload is spiky and you are time-sensitive. You can see this concept in action in this blog: How Smartsheet reduced latency and optimized costs in their serverless architecture.
Infrastructure patterns
Decouple using Queues and Buffers
Resilient systems absorb load, rather than rejecting it. Queues like SQS, Kafka, and Kinesis, and buffers like EventBridge act as a shock absorber between producers and consumers. They protect consumers from bursty spikes and offer natural retry and replay semantics.
With Amazon SQS, you get powerful knobs like visibility timeout to control retry behavior, message tension for reprocessing, DLQs to isolate poison-pill messages, and batching/long-polling to improve efficiency and reduce costs. If you need ordering and exactly-once processing, FIFO queues are a better fit. Similarly, Kafka and Kinesis offer high throughput via partitioning while preserving record order within each shard or partition.
For example, a real-time bidding system in an ad tech platform decouples high-volume clickstream data via Kinesis using region-id for the sharding. Billing events, on the other hand, are routed through FIFO queues to guarantee order and avoid duplicate charges (especially during retries). This pattern ensures that each workload type can independently scale or fail without causing cascading impact across the system.
Operational Patterns
Fail Fast and Break Things
It's not only Meta/Facebook’s engineering tenet, it's also about the resilience mindset. In this context, if your consumer knows it’s in trouble (e.g., can’t connect to a database or fetch config), fail quickly. This helps avoid visibility timeouts, retries from poison-pill records, and also helps signal the platform to back off sooner rather than later. I once debugged an issue where a container-based consumer would hang on a failed DB auth call for thirty seconds. Once we added a five-second timeout and explicit error signaling, visibility timeout errors dropped, and retries were no longer added to the failure. There are numerous examples of this sort. Another common example is when the head of the queue message processing is done without any strict timeout, leading to a buildup of the backlog. This pattern is not about making systems aggressive but more predictable and recoverable.
Other design tools, like using batching and polling intervals to help with the overhead or lazy initialization to avoid loading big dependencies when they are not needed, come in handy and help improve overall resiliency.
Common pitfalls (how to handle them)
Resilient systems often break, not because of one big outage, but because of a slow buildup of architectural debt. This idea is beautifully captured in a paper I read a couple of years back, which had an excellent explanation about metastable systems and when they break and have catastrophic effects. In the paper, they specifically discussed how a system transitions from a stable state to a vulnerable state under load, then progresses to a metastable state where long-lasting impacts are observed before manual intervention is typically required. I won't go into much detail, but just highlight that it talks about a similar mindset shift to avoid painful service outages.
Let’s look at some of the characteristics that lead to this:
Over-indexing on Average Load Instead of Spiky Behavior
Real-world traffic is hardly smooth; it’s mostly unpredictable. If you tune batch sizes, memory, or concurrency for the fiftieth percentile, your system will break at the ninetieth percentile or higher. Even a well-architected system can crash under pressure if they are not designed to expect and absorb unpredictable loads. It's not a "if" question but a "when" question; the key is that you should be prepared for crashes. Most of the time, there are ways to build to be ready. Consider the case of latency-sensitive workloads processed through AWS Lambda functions. You can set the auto-scaling policy for adjusting the provisioned concurrency config by looking at different cloudwatch metrics like invocation error, latency, or queue depth. You can generate a load test in your test environment, which helps you exercise higher percentiles (p95, p99) use cases.
Treating Retries as Panacea
Retries are cheap, until they aren’t. If retries are your only line of defense, they may not be sufficient. They also have the potential to multiply failure. Retries can overwhelm downstream systems; creating invisible traffic loops is all too easy when retry logic is not smart. This retry logic often shows up in systems where every error, transient or not, gets retried with no cap, no delay, and no contextual awareness. This approach leads to issues like throttled databases, increased latency, and even a total system collapse, which is common, unfortunately.
Instead, what you need is bounded retries to avoid infinite failure loops, or if you retry, use exponential backoff with jitter to avoid contention. Along with this approach, you should always keep context in mind. Divide your errors into retryable and non-retryable buckets and smartly retry. When upstream is down, you won’t get much help if you continue to hit the network with the same speed. It also won’t help service recover faster and could instead lead to delayed recovery because of extra pressure caused by retries. I wrote about the retries and the dilemma that comes with it in much more detail in the article, Overcoming the Retry Dilemma in Distributed Systems.
Not taking observability seriously
Expecting resilience is one thing; knowing your system is resilient is another. I often remind teams that
"Observability separates the intentions from actualities".
You may intend your system to be resilient, but only observability confirms whether that's true. It's not enough to monitor latency or error metrics. Resilient systems need to have clear resilience indicators that go beyond surface-level monitoring. These indicators should ask harder questions. Howfast do you detect failure (time to detect)? How quick is recovery (time to recover)? Does the system fail gracefully? Is the blast radius contained to a tenant, availability zone, or region? Are retries helping or just hiding the real problem? How does the system handle backpressure or upstream outages? These are high-level signals that test your architecture under stress; they only make sense when viewed together, not in isolation.
You can implement these insights using CloudWatch metrics for queue depth, Log Insights for retry patterns, and X-ray to trace the request flows across services. For example, in one case, a customer’s system ran smoothly until a Lambda error started silently pushing messages to the DLQ. Everything appeared green until users reported missing data. The issue was only discovered hours later because no one had set an alarm on the DLQ size. Afterwards, the team added DLQ alerts and integrated this into their internal service level objective (SLO) dashboard.
Observability gives you the only lens to ask, "Is the system doing what I expect even under stress?" If the answer is "I don’t know", it’s time to level up!
Treating all the events equally
Not all events are created equal. A customer order event isn’t the same as a logging event. If your architecture treats them equally, you’re either wasting resources/computations or at least introducing risks. Consider the example of a payment confirmation event sitting behind hundreds of low-priority logging events in a queue and impacting the business outcomes. Worse still, these low priorities can be retried or reprocessed for some reason and starve the critical events. You need to have a way to differentiate between critical and low-priority events.
Either establish different queues (high priority and low priority) or event routing rules that filter these events into two different Lambdas. This filtering will also help with using something like provisioned mode only for the high priority queue and not the other for cost effectiveness. Teams often catch these issues too late, when cost spikes, retries spiral, or SLAs break. But with the right signals and architectural intent, most issues can be avoided early or at least recovered from predictably.
Final Thoughts
When we're architecting event-driven systems at scale, resilience isn't about avoiding failure, it's about embracing it. We're not chasing some mythical "perfect" system. Instead, we're building systems that can take a hit and keep running.
Think about it: robust retry mechanisms that don't cascade into system-wide failures, elasticity that absorbs traffic spikes without breaking a sweat, and failure modes that are predictable and manageable. That's the goal. But if you’re just starting, building a resilient system can feel overwhelming. Where do you even begin?
Start small! Try building a sample event-driven application using Amazon SQS and AWS Lambda. Don't try anything fancy in the beginning. Just a simple queue and a Lambda function. Once you get that working, explore other features like DLQs, failure handling, etc. You can use EventBridge Event Bus and learn how events can be routed to different targets using rules. Once you get comfortable, layer it with techniques like shuffle sharding and autoscaling provisioned concurrency using metrics.
If you are looking for practical examples and tutorials, Serverless Land is a great place to explore patterns, code, and architectural guidance tailored for AWS Native EDA systems.
Building resilience isn’t a single step, it’s a mindset. Start small, learn from how your system behaves, and layer in complexity as your confidence grows!