Retries & Backoff

Jobs may be requeued after failure based on the configured retry policy. The JobLifecycleCoordinator drives the retry loop using RetryPolicyFixed, which implements all strategies in a single class.

RetryPolicyFixed

The unified retry class handles all strategies:

use Daycry\Jobs\Execution\RetryPolicyFixed;

$policy = new RetryPolicyFixed(
    base: 5,
    strategy: 'exponential',
    multiplier: 2.0,
    max: 300,
    jitter: true,
);
$delay = $policy->computeDelay($attempt); // returns seconds
  • First attempt (attempt <= 1) always returns 0 (no delay before first execution).

  • Strategy is read from global config by JobLifecycleCoordinator automatically.

Strategy

Configured via retryBackoffStrategy:

  • none – immediate requeue (no delay)

  • fixed – constant delay derived from retryBackoffBase

  • exponential – growth via multiplier and capped ceiling

Parameters

Setting

Used In Strategies

Description

retryBackoffBase

fixed, exponential

Baseline seconds for first retry.

retryBackoffMultiplier

exponential

Factor applied for subsequent attempts.

retryBackoffMax

fixed, exponential

Upper bound on any computed delay.

retryBackoffJitter

any

If true, add +/- up to 15% randomness.

Formula (Exponential)

delay = base * multiplier^(attempt-2)   where attempt is the current retry number

computeDelay(attempt) returns 0 when attempt <= 1 (no delay before the first execution). The exponent starts at 0 for attempt 2, so the first retry delay equals base.

Example

base=5, multiplier=2:
computeDelay(1) -> 0s  (first attempt, no pre-delay)
computeDelay(2) -> 5s  (before 2nd attempt: 5 * 2^0 = 5)
computeDelay(3) -> 10s (before 3rd attempt: 5 * 2^1 = 10)
computeDelay(4) -> 20s (before 4th attempt: 5 * 2^2 = 20)
computeDelay(5) -> 40s (before 5th attempt: 5 * 2^3 = 40)

With max=45 attempt 6 (80s) would clamp to 45.

Jitter

Adds randomness to reduce thundering herd: delay = delay ± (delay * rand(0,0.15)).

Applying Delay

Backoff integration typically happens before re-enqueue inside failure handling; ensure scheduling/delay attribute is applied to job/envelope before pushing.

Custom Policies

Implement custom logic using attempts:

if ($job->getAttempt() >= 5) {
    // give up
} else {
    // compute delay and requeue
}

Metrics

Track retries with the jobs_requeued counter. v1.0.3 added two more lifecycle counters worth alerting on:

Counter

Meaning

jobs_failed

Any failure attempt (including those that will be retried).

jobs_requeued

Failure that resulted in a requeue.

jobs_failed_permanently

Retries exhausted; the helper attempted to forward to the DLQ.

jobs_dlq_failed

DLQ unconfigured or push to DLQ failed — silent-loss alert.

You can export additional histograms for delay distribution by wrapping RetryPolicyFixed::computeDelay() in your own decorator and observing the result before sleeping.

Backend integration (v1.1+)

The Database backend honours retries via optimistic locking (10 attempts with exponential backoff + jitter, capped at 500ms). Redis preserves the original payload (and identifier) across requeues — the retry counter on the job advances, the message identity does not.

DLQ ordering (v1.0.3+)

On permanent failure, RequeueHelper::finalize() calls DeadLetterQueue::store() before clearing the message from the origin queue. If store() returns false (DLQ unconfigured or push failed), the helper increments jobs_dlq_failed so operators can alert on it. Configure Config\Jobs::$deadLetterQueue to avoid this.