error-handling
exception-handling
retry-mechanism
programming-tutorial
software-development

How to retry after exception?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Retry logic is useful when failures are temporary, such as flaky network requests, rate limits, or transient database timeouts. It is not appropriate for every exception. A good retry mechanism retries only known transient failures, limits the number of attempts, and waits between attempts so it does not hammer the failing dependency.

Start With a Simple Retry Loop

A plain loop is often the clearest place to start.

python
1import time
2
3
4def fetch_with_retry(operation, retries=3, delay=1.0):
5    last_error = None
6    for attempt in range(1, retries + 1):
7        try:
8            return operation()
9        except TimeoutError as exc:
10            last_error = exc
11            if attempt == retries:
12                break
13            time.sleep(delay)
14    raise last_error

Use it like this:

python
1def flaky_call():
2    raise TimeoutError("temporary network issue")
3
4fetch_with_retry(flaky_call, retries=3, delay=0.5)

This is enough for straightforward scripts and small services.

Retry Only the Right Exceptions

Do not retry everything. Some exceptions represent permanent failures, such as invalid input, authentication errors, or programming bugs.

A retry loop should usually target a limited set of transient exceptions.

python
except (TimeoutError, ConnectionError) as exc:

That keeps the retry policy honest. If you catch Exception broadly, you may hide real defects and waste time repeating requests that can never succeed.

Add Exponential Backoff

Using the same fixed delay every time can overload a struggling service. Exponential backoff increases the wait after each failure.

python
1import time
2
3
4def fetch_with_backoff(operation, retries=5, base_delay=0.5):
5    last_error = None
6    for attempt in range(1, retries + 1):
7        try:
8            return operation()
9        except (TimeoutError, ConnectionError) as exc:
10            last_error = exc
11            if attempt == retries:
12                break
13            sleep_time = base_delay * (2 ** (attempt - 1))
14            time.sleep(sleep_time)
15    raise last_error

This is a much healthier default for remote systems than immediate rapid retries.

Add Jitter for Distributed Systems

If many clients fail at once, identical retry schedules can produce another spike of traffic all at the same moment. A small random jitter spreads those retries out.

python
1import random
2import time
3
4
5def fetch_with_jitter(operation, retries=5, base_delay=0.5):
6    last_error = None
7    for attempt in range(1, retries + 1):
8        try:
9            return operation()
10        except (TimeoutError, ConnectionError) as exc:
11            last_error = exc
12            if attempt == retries:
13                break
14            sleep_time = base_delay * (2 ** (attempt - 1))
15            sleep_time += random.uniform(0, 0.25)
16            time.sleep(sleep_time)
17    raise last_error

This is especially important in services running many workers or many deployed instances.

Retrying a Real HTTP Call

python
1import requests
2
3
4def get_json(url):
5    response = requests.get(url, timeout=3)
6    response.raise_for_status()
7    return response.json()
8
9
10def load_data():
11    return fetch_with_backoff(lambda: get_json("https://example.com/api/data"))

This is the typical use case: an external dependency may fail briefly, but succeeding on a later attempt is realistic.

Know When Not to Retry

Retries are appropriate for transient failures. They are usually wrong for:

  • malformed input
  • permission errors
  • '404 responses that reflect a missing permanent resource'
  • invariant violations and programming bugs

A retry policy should be tied to the failure mode, not added mechanically after every exception.

Common Pitfalls

The most common mistake is retrying all exceptions. That turns real bugs into delayed failures and makes diagnosis harder.

Another issue is retrying forever. Always cap the attempt count unless you are deliberately implementing a long-running worker with a different recovery strategy.

Developers also often forget backoff and hammer a failing service repeatedly. That can make the outage worse.

Finally, keep logging around retries. Without attempt counts and final failure logs, production retry behavior is hard to understand.

Summary

  • Retry only failures that are likely to be temporary.
  • Limit the number of attempts and preserve the final exception.
  • Use exponential backoff, and add jitter when many clients may retry together.
  • Do not retry programming errors or permanent business failures.
  • Keep retry logic explicit so the recovery policy is easy to reason about.

Course illustration
Course illustration

All Rights Reserved.