Cadence
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
| Aspect | Rate Limiter | Cadence |
|---|---|---|
| Purpose | Limit request frequency | Slow down repeated failures |
| Applies to | Every request | Only after consecutive failures |
| Behavior on success | Continues counting | Immediately resets backoff state |
| State reset | Slides over time window | Clears 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
- No unnecessary delays - Successful operations are never throttled
- Progressive protection - Delays increase with consecutive failures
- Immediate recovery - One success clears all backoff state
- Flexible strategies - Choose from multiple backoff algorithms
- 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
| Option | Default | Description |
|---|---|---|
default | exponential | The default backoff driver. |
free_attempts | 3 | Number of failures allowed before backoff is applied. |
idle_timeout | 3600 | Number of seconds to retain failure state before it expires. |
cache.store | null | Laravel cache store name used by Cadence. Leave null to use the default store. |
drivers.exponential.base_delay | 2 | Base delay used by the exponential driver. |
drivers.fibonacci.base_delay | 1 | Base delay used by the fibonacci driver. |
drivers.linear.base_delay | 2 | Base delay used by the linear driver. |
drivers.quadratic.base_delay | 1 | Base 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:
- Ensure the key is not currently locked.
- Record failures whenever the protected operation fails.
- 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
- Learn about Available Backoff Strategies
- Explore Applying Jitter for distributed retries
- Read the Public API for complete method documentation
Available Backoff Strategies
Cadence currently includes the following backoff strategies:
| Strategy | Description |
|---|---|
exponential | Applies progressively increasing delays using an exponential growth pattern, causing the delay to increase rapidly as failures accumulate. |
fibonacci | Applies progressively increasing delays following the Fibonacci sequence, providing a gradual and adaptive backoff pattern. |
linear | Applies progressively increasing delays using a linear growth pattern, increasing the delay by a consistent amount after each failure. |
quadratic | Applies 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
| Strategy | Best For |
|---|---|
| Exponential | Most use cases; fast escalation |
| Fibonacci | Moderate escalation with predictable growth |
| Linear | Predictable, steady increase in delays |
| Quadratic | Aggressive 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:
| Algorithm | Description |
|---|---|
JitterType::FULL | Randomizes the entire delay between 0 and the calculated delay. |
JitterType::EQUAL | Preserves 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
- Initial State - No failures recorded for the key
- Free Attempts - Failures are recorded but no delay is applied (up to
free_attempts) - Backoff Applied - After exceeding free attempts, delays increase progressively
- Lock Active -
ensureNotLocked()throwsCadenceLockedException - Success Recorded - All failure state is cleared, key is unlocked
- Idle Timeout - After
idle_timeoutseconds, 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:
| Property | Description |
|---|---|
attempt | Current attempt count. |
violationCount | Current violation count. |
delay | Delay applied in seconds. |
isLocked | Whether 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:
| Method | Description |
|---|---|
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.