← Home ← Codex ← DEBT
Browse by Category
+ added · updated 7d
← Back to glossary

Retry Pattern with Exponential Backoff

Architecture PHP 5.0+ Intermediate
debt(d7/e5/b5/t7)
d7 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7). Semgrep can flag tight retry loops but most retry-without-backoff issues only surface under load testing or in production incident review.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). Quick_fix says introduce exponential backoff with jitter, max retry count, and dead-letter destination — that's a retry helper plus rewiring each retry site, not a one-liner.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). Applies across web and queue-worker contexts; once retry/backoff/idempotency policy is established, every integration with external services must conform to it.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). Misconception states devs assume immediate retries are equivalent to backoff, and retrying non-idempotent operations (e.g. payment charges) silently doubles side effects — the naive implementation actively worsens the failure mode it tries to fix.

About DEBT scoring →

Also Known As

retry logic exponential backoff retry with backoff

TL;DR

Automatically retrying failed operations with increasing delays — preventing thundering herd and handling transient failures gracefully.

Explanation

Transient failures (network blips, rate limit 429s, database deadlocks) are often self-resolving. The Retry pattern retries the operation automatically, but naive immediate retries can overwhelm a struggling service. Exponential backoff doubles the wait on each attempt (1s, 2s, 4s, 8s...); jitter adds randomness to spread retries from many clients (prevents thundering herd). Best practices: cap maximum retries (3–5), cap maximum delay (30–60s), only retry idempotent operations (GET, PUT — not POST without idempotency keys), use a circuit breaker to stop retrying when the service is clearly down. Guzzle's retry middleware and symfony/http-client both support retry configuration. Log every retry with the delay and reason.

Diagram

flowchart TD
    REQ[Send Request] --> TRY{Success?}
    TRY -->|Yes| DONE[Done]
    TRY -->|No| CHECK{Retryable
error?}
    CHECK -->|No - 400 400 auth| FAIL[Fail immediately]
    CHECK -->|Yes - 500 429 timeout| WAIT[Wait with<br/>exponential backoff<br/>1s -> 2s -> 4s -> 8s]
    WAIT --> ATTEMPTS{Max attempts
reached?}
    ATTEMPTS -->|No| REQ
    ATTEMPTS -->|Yes| GIVEUP[Give up<br/>alert / dead letter]
style DONE fill:#238636,color:#fff
style FAIL fill:#f85149,color:#fff
style GIVEUP fill:#f85149,color:#fff
style WAIT fill:#d29922,color:#fff

Common Misconception

Retrying immediately after a failure is as effective as backoff. Immediate retries hammer a struggling dependency and worsen cascading failures. Exponential backoff with jitter spreads retry load, and idempotency checks ensure retried operations do not create duplicate side effects.

Why It Matters

Retrying transient failures (network blips, rate limits, temporary outages) automatically improves resilience — but retrying without backoff and jitter amplifies load on an already struggling service.

Common Mistakes

  • Retrying non-idempotent operations without checking — retrying a payment charge double-charges the customer.
  • Constant retry interval without exponential backoff — hammers a recovering service at full rate.
  • No jitter — all clients retry at the same time, creating a thundering herd.
  • No maximum retry limit — infinite retries hold resources and can cause cascading failures.

Code Examples

✗ Vulnerable
// Retry without backoff or limit — thundering herd:
for ($i = 0; $i < 10; $i++) {
    try {
        return $this->http->post('/payments', $data);
    } catch (Exception $e) {
        sleep(1); // Same interval every time — all clients retry simultaneously
    }
}
// Fix: sleep(2 ** $i + random_int(0, 1000) / 1000) — exponential backoff with jitter
✓ Fixed
// Guzzle retry middleware
$handler = HandlerStack::create();
$handler->push(Middleware::retry(
    fn($retries, $req, $res) => $retries < 3 && ($res?->getStatusCode() === 429),
    fn($retries) => (int)(1000 * 2 ** $retries) // exponential ms delay
));

Added 15 Mar 2026
Edited 19 Apr 2026
Views 62
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings W 1 ping T 0 pings F 0 pings S 0 pings S 3 pings M 1 ping T 2 pings W 1 ping T 0 pings F 0 pings S 5 pings S 2 pings M 1 ping T 0 pings W 0 pings T 0 pings F 0 pings S 1 ping S 0 pings M 0 pings T 0 pings W 2 pings T 0 pings F 0 pings S 0 pings S 0 pings M 1 ping T 0 pings W
No pings yet today
Ahrefs 1
ChatGPT 12 Scrapy 11 Amazonbot 9 Perplexity 5 Google 4 Ahrefs 4 Unknown AI 3 Claude 2 SEMrush 2 Qwen 1 Meta AI 1
crawler 49 crawler_json 5
DEV INTEL Tools & Severity
🟡 Medium ⚙ Fix effort: Medium
⚡ Quick Fix
Use exponential backoff with jitter for retries — not fixed delay, not infinite retries; always set a maximum retry count and a dead-letter destination
📦 Applies To
PHP 5.0+ web queue-worker laravel symfony
🔗 Prerequisites
🔍 Detection Hints
try/catch that immediately retries in a tight loop without backoff; queue job with $tries=0 unlimited retries
Auto-detectable: ✗ No semgrep
⚠ Related Problems
🤖 AI Agent
Confidence: Medium False Positives: Medium ✗ Manual fix Fix: Medium Context: Function Tests: Update


✓ schema.org compliant