Typorithm · medium

Retry budgets and respecting server delays

pythonapi-clientsautomationindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Retry budgets and respecting server delays

0%

Concept and contract

Calculate a bounded retry decision without sleeping, inventing success or shortening a server-requested delay.

Retrying is additional work, not a harmless error-handling detail. A client must decide whether the operation is suitable for repetition, how many retries remain and how long to wait.

This lesson only calculates a delay for an explicitly selected set of received HTTP statuses. attempt means retries already used, starting at zero. The function never sleeps or sends a request, making the policy easy to inspect without burdening an endpoint.

Mechanism and worked example

The default policy allows at most three retries and computes exponential fallback delays when no server delay is supplied.

The default policy allows at most three retries and computes exponential fallback delays when no server delay is supplied. A numeric Retry-After value takes precedence, with a small minimum pause.

If the server asks for more than this helper’s sixty-second wait budget, the result is None: stop automatic retries rather than retrying earlier than requested. This deliberately narrow header contract accepts delay-seconds only, not the separate HTTP-date representation.

Complete worked example

python

def retry_delay(status, attempt, retry_after=None):
    if type(status) is not int or not 100 <= status <= 599:
        raise ValueError("HTTP status required")
    if type(attempt) is not int or not 0 <= attempt <= 5:
        raise ValueError("attempt must be 0..5")
    if status not in {429, 502, 503, 504} or attempt >= 3:
        return None
    if retry_after is not None:
        if not isinstance(retry_after, str) or not retry_after.isascii() or not retry_after.isdecimal():
            raise ValueError("this contract accepts delay-seconds only")
        delay = int(retry_after)
        return max(0.25, delay) if delay <= 60 else None
    return min(4.0, 0.25 * 2 ** attempt)
Reasoning
Calculate a bounded retry decision without sleeping, inventing success or shortening a server-requested delay. Compare the stated values and failure boundary before changing the implementation.

Counterexamples and common mistakes

The counterexample shortens a server delay to the local fallback cap.

The counterexample shortens a server delay to the local fallback cap. A client could then retry while the server still expects a longer pause.

Another common mistake is retrying every status, including a persistent 404 or an authorization failure. A maximum count is necessary even for retryable statuses. The caller must also consider idempotency and total elapsed time; a classification alone does not make a repeated write safe.

Counterexample: a runnable policy defect

python

def retry_delay(status, attempt, retry_after=None):
    if type(status) is not int or not 100 <= status <= 599:
        raise ValueError("HTTP status required")
    if type(attempt) is not int or not 0 <= attempt <= 5:
        raise ValueError("attempt must be 0..5")
    if status not in {429, 502, 503, 504} or attempt >= 3:
        return None
    if retry_after is not None:
        if not isinstance(retry_after, str) or not retry_after.isascii() or not retry_after.isdecimal():
            raise ValueError("this contract accepts delay-seconds only")
        delay = int(retry_after)
        return min(4.0, max(0.25, delay))
    return min(4.0, 0.25 * 2 ** attempt)
Reasoning
This runnable counterexample violates the declared contract; the companion lab requires it to fail.

Independent practice: API client task

Add a max_attempts parameter defaulting to three, bounded from zero through five and rejecting booleans.

Add a max_attempts parameter defaulting to three, bounded from zero through five and rejecting booleans. Keep the server delay policy intact.

First test a zero-retry budget, then allow a fourth attempt and finally reject an invalid budget. This changes one policy input. It does not add real sleeping, random delays or a background scheduler to the small deterministic exercise.

Step 1: Add a bounded max_attempts policy parameter. Step 2: Do not shorten a server-requested pause to fit local convenience.

Practice starter: solve before reading the solution

python

def retry_delay(status, attempt, retry_after=None):
    # Remove this placeholder, then adapt the retained baseline:
    # Add a bounded max_attempts policy parameter.
    raise NotImplementedError
    if type(status) is not int or not 100 <= status <= 599:
        raise ValueError("HTTP status required")
    if type(attempt) is not int or not 0 <= attempt <= 5:
        raise ValueError("attempt must be 0..5")
    if status not in {429, 502, 503, 504} or attempt >= 3:
        return None
    if retry_after is not None:
        if not isinstance(retry_after, str) or not retry_after.isascii() or not retry_after.isdecimal():
            raise ValueError("this contract accepts delay-seconds only")
        delay = int(retry_after)
        return max(0.25, delay) if delay <= 60 else None
    return min(4.0, 0.25 * 2 ** attempt)
Reasoning
Add a bounded max_attempts policy parameter. Do not shorten a server-requested pause to fit local convenience.
  • Hint 1: Validate the new budget before comparing it with attempt.
  • Hint 2: A zero budget means no automatic retry, even for a 429.

Solution, tests and trade-offs

The cases include a long server-requested pause, a zero delay, exhausted attempts, a non-retryable status and malformed header text.

The cases include a long server-requested pause, a zero delay, exhausted attempts, a non-retryable status and malformed header text. The solution returns None when the requested pause exceeds the owned wait budget.

Exact returned delays distinguish respecting the server from merely returning some positive number. A production caller should additionally record its next permitted attempt time and reduce polling frequency after repeated overload responses.

Practice solution and acceptance checks

python

def retry_delay(status, attempt, retry_after=None, max_attempts=3):
    if type(status) is not int or not 100 <= status <= 599:
        raise ValueError("HTTP status required")
    if type(attempt) is not int or not 0 <= attempt <= 5:
        raise ValueError("attempt must be 0..5")
    if type(max_attempts) is not int or not 0 <= max_attempts <= 5:
        raise ValueError("max_attempts must be 0..5")
    if status not in {429, 502, 503, 504} or attempt >= max_attempts:
        return None
    if retry_after is not None:
        if not isinstance(retry_after, str) or not retry_after.isascii() or not retry_after.isdecimal():
            raise ValueError("this contract accepts delay-seconds only")
        delay = int(retry_after)
        return max(0.25, delay) if delay <= 60 else None
    return min(4.0, 0.25 * 2 ** attempt)
Reasoning
Validate the new budget before comparing it with attempt. A zero budget means no automatic retry, even for a 429.

Interview reasoning and next step

Explain the difference between abandoning an automatic retry and ignoring an operation.

Explain the difference between abandoning an automatic retry and ignoring an operation. A scheduler can retain work for later without repeatedly contacting the source now.

If using HTTP-date Retry-After, parse it against an explicit aware clock and preserve a total-work budget. Optional jitter can reduce synchronized clients, but its distribution must not violate the minimum server-requested pause. Test the policy separately from the mechanism that performs requests.

Optional stretch — after the core task works: Add deterministic injected jitter that never undercuts the server delay.

Computer practice: https://applydjinn.com/typorithm/python-automation-lab-20261105.zip. Run verify.py using your selected Python interpreter, then adapt the retained exercise baseline and check it with --practice followed by the lesson id. The standard-library lab uses owned temporary 127.0.0.1 loopback servers, fictional pages and disposable files; it never scans production feeds or opens the working ApplyDjinn database.

Calculate a bounded retry decision without sleeping, inventing success or shortening a server-requested delay.