Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to the next lesson in our course on distributed systems architecture.
Introduction
In our last lesson, we tackled the critical concept of idempotency. We established that by using an idempotency key, we can make operations like payments safe to retry, even if we're unsure of their outcome. This prevents dangerous side effects like duplicate charges.
However, making retries safe is only half the battle. We also need to make them smart. If a service is temporarily down or overloaded, bombarding it with immediate, repeated requests will only worsen the situation, potentially causing a cascading failure.
Today, we will focus on the client-side strategy for intelligently handling these transient failures. We will learn how to implement a robust retry mechanism that gives a struggling service breathing room to recover.
Learning Outcome:
Implement client-side retry logic with exponential backoff and jitter for handling transient failures in service communication.
Given your experience building high-throughput financial systems at Revolut, you'll recognize the importance of these resilience patterns in maintaining system stability under stress.
1. The Problem: From Timeouts to Thundering Herds
Before a client can decide to retry, it must first decide that the original request has failed. In a network call, the most common signal for a transient failure is a timeout. The client waits a reasonable amount of time for a response, and if none arrives, it assumes something went wrong.
But what should the client do next? A naive approach might be to retry immediately, perhaps in a simple loop. This leads to a significant problem known as a "thundering herd."
The following video provides an excellent deep dive into this scenario. It starts by establishing the need for timeouts and then demonstrates why simple, repetitive retries are so dangerous.
Retries & Exponential Backoff - Deep Dive
The video 'Retries & Exponential Backoff - Deep Dive' by glich.stream sets the stage by explaining why timeouts are necessary and then shows the destructive effect of naive retries.
Please watch from 04:36 to 19:07. The first part, 'The Need for Timeouts' (until 11:47), explains how to set reasonable timeouts. The second part, 'Basic Retries and Their Pitfalls' (from 13:34), illustrates how a simple retry loop can overwhelm a recovering service.
As the video explains, if a service fails and then comes back online, it will be instantly flooded by thousands of synchronized retry attempts from all its clients. This new spike in traffic is often enough to crash the service again, creating a vicious cycle of failure and recovery.
This isn't just a theoretical problem. The next video discusses how these principles apply to a real-world trading application, a context you're very familiar with.
Exponential Backoff | Microservices/Distributed Systems Strategy | Zerodha System Design
The video 'Exponential Backoff' by Tech Primers uses the Zerodha trading platform as a case study, which is a great practical example.
Watch the section 'Exponential Backoff in a Trading System (Zerodha)' from 04:31 to 09:09. Notice how they use these strategies to handle failures in critical dependencies like a payment gateway and for rate-limiting API clients.
2. The Solution: Exponential Backoff
To prevent the thundering herd, clients need a more sophisticated retry strategy. Instead of retrying at a constant rate, they should back off, increasing the delay between each subsequent attempt. The standard algorithm for this is exponential backoff.
The core idea is simple: the wait time before the next retry grows exponentially with each failure. This gives the downstream service an increasing amount of time to recover.
Let's return to the "Deep Dive" video to see how this is calculated.
Retries & Exponential Backoff - Deep Dive
This segment explains the formula for exponential backoff and visualizes how the delay grows over time.
Watch the section 'Exponential Backoff Explained' from 19:07 to 22:42. Pay attention to the formula presented.
The general formula for the delay can be expressed in a few ways, but a common version is:
Where:
baseis the initial delay (e.g., 100ms).attemptis the number of the retry attempt (starting at 0 or 1).capis a maximum delay to prevent the backoff period from becoming excessively long.
This strategy ensures that as failures persist, the client reduces the pressure it applies, allowing the system to stabilize.
3. The Refinement: Adding Jitter
Exponential backoff is a huge improvement, but it has a subtle flaw. If many clients experience a failure at the same time, they will all follow the same backoff schedule. Their retries will still be synchronized, creating smaller, periodic "thundering herds."
The solution is to add jitter—a small amount of randomization to the delay. This breaks the synchronization and spreads the retry attempts out over time.
Retries & Exponential Backoff - Deep Dive
The final conceptual piece is jitter. This last segment from the 'Deep Dive' video explains why it's needed and references a foundational AWS article on the topic.
Watch 'Introducing Jitter for Distributed Retries' from 22:42 to 29:54. The video shows graphs that clearly demonstrate how jitter distributes request load more evenly.
A common and effective strategy is "full jitter," where the next delay is a random value between 0 and the calculated exponential backoff delay.
The following images, taken from the AWS article mentioned in the video, powerfully illustrate the benefits of jitter.


4. Implementation in Python
Now let's turn to the implementation. In Python, you can implement this logic manually, often using a decorator for clean, reusable code. However, the most common approach is to use a well-tested library.
The following resource provides excellent Python examples for both a manual implementation and for using the popular tenacity library.
The OpenAI Cookbook article 'How to handle rate limits' provides concise, practical Python code for implementing exponential backoff. Although the context is rate limiting, the mechanism is identical to what we need for transient error handling.
Please study two examples from this article. First, review 'Example 3: Manual backoff implementation' to understand the underlying logic. Note how it's wrapped in a decorator and includes parameters for initial_delay, exponential_base, jitter, and max_retries. Second, review 'Example #1: Using the Tenacity library'. This shows the declarative, production-ready way to achieve the same result.
Key Implementation Considerations
Whether you build it manually or use a library, a robust retry implementation should always consider the following, as mentioned in the resources:
- Limit Retries: Always have a
max_retriesormax_timeto prevent indefinite retries for a persistent failure. - Retry on Transient Errors Only: You should only retry on errors that are likely to be temporary. These are typically network errors or HTTP 5xx server errors (e.g., 500, 502, 503, 504). It rarely makes sense to retry client errors like 4xx (e.g., 401 Unauthorized, 404 Not Found).
- Logging and Monitoring: Log retry attempts and monitor their frequency. A high rate of retries is a strong indicator of instability in a downstream service.
Conclusion
You have now learned how to build resilient clients that can gracefully handle the transient failures inherent in distributed systems. By moving beyond naive retries to a strategy of exponential backoff with jitter, you can prevent your system from collapsing under its own weight during periods of instability.
Key Takeaways:
- Naive, immediate retries can create "thundering herd" problems that overwhelm recovering services.
- Exponential backoff is an algorithm where the delay between retries increases exponentially, giving services time to recover.
- Jitter adds randomization to the backoff delay, preventing clients from retrying in synchronized waves.
- In Python, this pattern can be implemented manually using decorators or, more commonly, by using robust libraries like
tenacity. - A sound retry strategy is selective: it should have a maximum number of attempts and should only trigger on transient errors.
Preview of the Next Lesson:
We have now covered two crucial pillars for building reliable systems: making retries safe (idempotency) and making them smart (exponential backoff). These concepts are fundamental to achieving specific delivery guarantees. In our next lesson, we will formalize this by analyzing the system-wide implications of at-least-once, at-most-once, and exactly-once delivery semantics, and see how the patterns we've learned help us achieve the coveted "exactly-once" processing goal.