Cadence

Latest Version on Packagist PHP Version Tests Static Analysis Total Downloads

Cadence is a Laravel package for applying progressive backoff based on consecutive failures.

Unlike traditional rate limiting, Cadence only introduces delays after repeated failed attempts. A successful operation immediately resets the backoff state, allowing normal traffic to continue uninterrupted while slowing down repeated failures.

Cadence stores its state using Laravel's cache abstraction and provides a clean, Laravel-native API through a manager, facade, and dependency injection. It supports multiple backoff strategies and optional jitter algorithms to help distribute retries and reduce synchronized retry bursts.

Features

  • Track repeated failures on a per-key basis
  • Apply progressive backoff after configurable free attempts
  • Multiple built-in backoff strategies (Exponential, Fibonacci, Linear, Quadratic)
  • Optional jitter algorithms to distribute retries and reduce synchronized retry bursts
  • Reset backoff immediately after successful operations
  • Store state using Laravel's cache abstraction
  • Configure free attempts, idle timeout, and cache store
  • Resolve engines via the facade or dependency injection

When Should I Use Cadence?

Cadence is designed for operations where repeated failures should temporarily slow down future attempts.

Common use cases include:

  • Login and authentication endpoints
  • One-time password (OTP) verification
  • Password reset attempts
  • API authentication
  • Third-party API integrations
  • Webhook delivery retries
  • Expensive or sensitive operations that should back off after consecutive failures

Cadence does not limit every request. Instead, it only introduces delays after repeated failures, allowing successful operations to proceed normally while discouraging abusive or repeated failed attempts.

Why Cadence?

Rate limiters controls how often an action can be performed.

Cadence controls how long the next attempt should wait after repeated failures.

Instead of limiting every request, Cadence progressively increases the delay between failed attempts and immediately resets the backoff state after a successful operation.

Rate Limiting vs. Progressive Backoff

AspectRate LimiterCadence
PurposeLimit request frequencySlow down repeated failures
Applies toEvery requestOnly after consecutive failures
Behavior on successContinues countingImmediately resets backoff state
State resetSlides over time windowClears on successful operation

When Cadence Shines

Cadence is ideal for scenarios where:

  • You want to allow normal traffic without delay
  • You need to discourage repeated failed attempts
  • You want progressive delays that increase with each failure
  • You need the backoff to reset immediately upon success
  • You want to combine backoff with optional jitter for distributed retries

Key Benefits

  1. No unnecessary delays - Successful operations are never throttled
  2. Progressive protection - Delays increase with consecutive failures
  3. Immediate recovery - One success clears all backoff state
  4. Flexible strategies - Choose from multiple backoff algorithms
  5. Jitter support - Reduce thundering herd with optional randomness

Installation

Requirements

  • PHP 8.4+
  • Laravel 9+

Install via Composer

composer require kodefarmers/laravel-cadence

Laravel will automatically discover and register the package.

Publish the Configuration

If you wish to customize the default settings, publish the configuration file:

php artisan vendor:publish --tag=cadence-config

This will create a config/cadence.php file in your application where you can modify the default driver, free attempts, idle timeout, cache store, and driver-specific options.

Configuration

The published configuration file is located at config/cadence.php.

return [

    'default' => env('CADENCE_DEFAULT_DRIVER', 'exponential'),

    'free_attempts' => 3,

    'idle_timeout' => 3600,

    'cache' => [
        'store' => env('CADENCE_CACHE_STORE'),
    ],

    'drivers' => [

        'exponential' => [
            'base_delay' => 2,
        ],

        'fibonacci' => [
            'base_delay' => 1,
        ],

        'linear' => [
            'base_delay' => 2,
        ],

        'quadratic' => [
            'base_delay' => 1,
        ],

    ],

];

Configuration Options

OptionDefaultDescription
defaultexponentialThe default backoff driver.
free_attempts3Number of failures allowed before backoff is applied.
idle_timeout3600Number of seconds to retain failure state before it expires.
cache.storenullLaravel cache store name used by Cadence. Leave null to use the default store.
drivers.exponential.base_delay2Base delay used by the exponential driver.
drivers.fibonacci.base_delay1Base delay used by the fibonacci driver.
drivers.linear.base_delay2Base delay used by the linear driver.
drivers.quadratic.base_delay1Base delay used by the quadratic driver.

Using a Specific Backoff Strategy

Cadence supports multiple backoff strategies. You can configure the default strategy using the CADENCE_DEFAULT_DRIVER environment variable.

CADENCE_DEFAULT_DRIVER=fibonacci

See the Available Backoff Strategies section for the list of built-in drivers and their behavior.

If not specified, Cadence uses the exponential strategy by default.

Using a Specific Laravel Cache Store

Cadence uses Laravel's cache abstraction, so it can work with any cache store supported by your Laravel application.

Set the store with an environment variable:

CADENCE_CACHE_STORE=

Examples:

CADENCE_CACHE_STORE=redis
CADENCE_CACHE_STORE=database

If the value is empty or not set, Cadence falls back to Laravel's default cache store.

Quick Start

A typical workflow consists of three steps:

  1. Ensure the key is not currently locked.
  2. Record failures whenever the protected operation fails.
  3. Record a success to reset the backoff state.

Basic Example

use Kodefarmers\Cadence\Exceptions\CadenceLockedException;
use Kodefarmers\Cadence\Facades\Cadence;

$cadence = Cadence::driver();

$key = 'operation:resource-id';

try {
    $cadence->ensureNotLocked($key);

    // Perform the protected operation...

    if ($operationFailed) {
        $result = $cadence->recordFailure($key);

        return [
            'success' => false,
            'locked' => $result->isLocked,
            'retry_after' => $result->delay,
        ];
    }

    $cadence->recordSuccess($key);

    return [
        'success' => true,
    ];
} catch (CadenceLockedException $exception) {
    return [
        'success' => false,
        'message' => 'Operation is temporarily locked.',
        'retry_after' => $exception->retryAfter(),
    ];
}

By default, the first three failures are allowed without any delay. The fourth failure becomes the first backoff violation and applies the configured delay.

Using Different Backoff Strategies

The driver() method accepts the name of the backoff strategy to use.

$cadence = Cadence::driver('fibonacci');

See the Available Backoff Strategies section for the list of built-in drivers and their behavior.

Next Steps

Available Backoff Strategies

Cadence currently includes the following backoff strategies:

StrategyDescription
exponentialApplies progressively increasing delays using an exponential growth pattern, causing the delay to increase rapidly as failures accumulate.
fibonacciApplies progressively increasing delays following the Fibonacci sequence, providing a gradual and adaptive backoff pattern.
linearApplies progressively increasing delays using a linear growth pattern, increasing the delay by a consistent amount after each failure.
quadraticApplies progressively increasing delays using a quadratic growth pattern, causing the delay to increase at an accelerating rate as failures accumulate.

Every built-in backoff strategy can optionally be combined with a jitter algorithm. See Applying Jitter for examples and available algorithms.

Exponential Backoff

The exponential strategy applies delays that grow exponentially based on the configured base_delay.

$cadence = Cadence::driver('exponential');

Configuration:

'exponential' => [
    'base_delay' => 2,
],

Behavior: Each violation doubles the delay from the previous one, starting from the base delay.

Fibonacci Backoff

The fibonacci strategy applies delays that follow the Fibonacci sequence, multiplied by the configured base_delay.

$cadence = Cadence::driver('fibonacci');

Configuration:

'fibonacci' => [
    'base_delay' => 1,
],

Behavior: Each violation progresses through the Fibonacci sequence (1, 1, 2, 3, 5, 8, ...), multiplied by the base delay.

Linear Backoff

The linear strategy applies delays that increase linearly based on the configured base_delay.

$cadence = Cadence::driver('linear');

Configuration:

'linear' => [
    'base_delay' => 2,
],

Behavior: Each violation increases the delay by the base delay amount.

Quadratic Backoff

The quadratic strategy applies delays that grow quadratically based on the configured base_delay.

$cadence = Cadence::driver('quadratic');

Configuration:

'quadratic' => [
    'base_delay' => 1,
],

Behavior: Each violation increases the delay proportionally to the square of the violation count, multiplied by the base delay.

Choosing a Strategy

StrategyBest For
ExponentialMost use cases; fast escalation
FibonacciModerate escalation with predictable growth
LinearPredictable, steady increase in delays
QuadraticAggressive escalation for sensitive operations

Applying Jitter

Jitter is an optional post-processing step that decorates any backoff strategy. It adds controlled randomness to the delay returned by the selected strategy without replacing that strategy.

This helps reduce synchronized retry bursts and the thundering herd problem by spreading retries across a randomized window.

Basic Usage

use Kodefarmers\Cadence\Enums\JitterType;

Cadence::driver('exponential')->jitter();

Cadence::driver('linear')->jitter(JitterType::EQUAL);

Available Jitter Algorithms

Cadence currently supports two jitter algorithms:

AlgorithmDescription
JitterType::FULLRandomizes the entire delay between 0 and the calculated delay.
JitterType::EQUALPreserves roughly half of the calculated delay while randomizing the rest.

Every built-in backoff strategy can be combined with jitter. JitterType::FULL is used by default.

Full Jitter

Full jitter randomizes the entire delay between 0 and the calculated delay.

use Kodefarmers\Cadence\Enums\JitterType;

$cadence = Cadence::driver('exponential')->jitter(JitterType::FULL);

Example: If the calculated delay is 8 seconds, the actual delay will be a random value between 0 and 8 seconds.

Equal Jitter

Equal jitter preserves roughly half of the calculated delay while randomizing the rest.

use Kodefarmers\Cadence\Enums\JitterType;

$cadence = Cadence::driver('exponential')->jitter(JitterType::EQUAL);

Example: If the calculated delay is 8 seconds, the actual delay will be at least 4 seconds plus a random value between 0 and 4 seconds.

When to Use Jitter

Use jitter when:

  • Multiple clients may retry the same operation simultaneously
  • You want to distribute retries across a time window
  • You need to reduce the thundering herd effect on downstream services
  • Your application handles high-concurrency scenarios

How Cadence Works

Cadence tracks failures for a unique key. A key can represent anything you want to protect, such as:

  • A user ID
  • An email address
  • An IP address
  • An API token
  • A webhook identifier

Failure Tracking Flow

Each failed attempt increases the recorded attempt count for that key.

Once the configured free-attempt threshold has been exceeded, Cadence temporarily locks the key using the configured backoff strategy.

While the key is locked, calling ensureNotLocked() throws a CadenceLockedException.

Calling recordSuccess() clears the recorded failures and immediately removes any active lock.

Lifecycle

  1. Initial State - No failures recorded for the key
  2. Free Attempts - Failures are recorded but no delay is applied (up to free_attempts)
  3. Backoff Applied - After exceeding free attempts, delays increase progressively
  4. Lock Active - ensureNotLocked() throws CadenceLockedException
  5. Success Recorded - All failure state is cleared, key is unlocked
  6. Idle Timeout - After idle_timeout seconds, failure state expires naturally

State Persistence

Cadence stores its state using Laravel's cache abstraction. This means:

  • State persists across requests
  • State can be shared across multiple application instances
  • State respects the configured cache store and TTL
  • State expires after the configured idle_timeout

Public API

Resolving a Cadence Engine

Using the Facade

use Kodefarmers\Cadence\Facades\Cadence;

$cadence = Cadence::driver();

Using Dependency Injection

use Kodefarmers\Cadence\CadenceManager;

class LoginService
{
    public function __construct(
        private readonly CadenceManager $cadence,
    ) {
    }

    public function handle(string $key): void
    {
        $cadence = $this->cadence->driver();

        // ...
    }
}

Recording Failures

$result = $cadence->recordFailure($key);

Returns a CadenceResult containing:

PropertyDescription
attemptCurrent attempt count.
violationCountCurrent violation count.
delayDelay applied in seconds.
isLockedWhether the key is currently locked.

Recording Success

$cadence->recordSuccess($key);

Resets the recorded attempts and removes any active lock.

Querying State

$cadence->ensureNotLocked($key);

$cadence->isLocked($key);

$cadence->remainingBackoff($key);

$cadence->attempts($key);

ensureNotLocked()

Throws CadenceLockedException if the key is currently locked.

isLocked()

Returns whether the key is currently locked.

remainingBackoff()

Returns the remaining lock duration in seconds.

attempts()

Returns the current recorded attempt count.

CadenceLockedException

When a key is locked, ensureNotLocked() throws CadenceLockedException.

The exception exposes:

MethodDescription
key()The locked key.
retryAfter()Seconds until the lock expires.
attempts()The attempt count when locked.
violationCount()The violation count when locked.

Testing

Running Tests

Run the test suite:

composer test

Static Analysis

Run static analysis:

composer analyse

Code Formatting

Format the codebase:

composer format

Contributing

Contributions are welcome.

Please open an issue to discuss significant changes before submitting a pull request. All pull requests should include appropriate tests for new functionality or behavior changes.

License

Cadence is open-source software licensed under the MIT License.