PHPackages                             squipix/laravel-idempotency - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. squipix/laravel-idempotency

ActiveLibrary[Queues &amp; Workers](/categories/queues)

squipix/laravel-idempotency
===========================

Stripe-style idempotency for Laravel APIs and queues

1.1.0(1w ago)00MITPHPPHP ^8.1|^8.2|^8.3|^8.4CI passing

Since Jan 9Pushed 1w agoCompare

[ Source](https://github.com/squipix/laravel-idempotency)[ Packagist](https://packagist.org/packages/squipix/laravel-idempotency)[ RSS](/packages/squipix-laravel-idempotency/feed)WikiDiscussions main Synced today

READMEChangelog (2)Dependencies (11)Versions (3)Used By (0)

Laravel Idempotency
===================

[](#laravel-idempotency)

[![GitHub Workflow Status](https://camo.githubusercontent.com/2ced5bbecadaed02394b9bff5d371f5495553b6d604d5d2e3804271dffb457e4/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f737175697069782f6c61726176656c2d6964656d706f74656e63792f72756e2d74657374732e796d6c3f7374796c653d666c61742d737175617265)](https://github.com/squipix/laravel-idempotency/actions)[![Coverage Status](https://camo.githubusercontent.com/2ced5bbecadaed02394b9bff5d371f5495553b6d604d5d2e3804271dffb457e4/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f737175697069782f6c61726176656c2d6964656d706f74656e63792f72756e2d74657374732e796d6c3f7374796c653d666c61742d737175617265)](https://github.com/squipix/laravel-idempotency/actions)[![SensioLabs Insight](https://camo.githubusercontent.com/06ddb423f8d26bbe40079ee3babc7a33ad5ef36c5c34c7f670933696788c4167/68747470733a2f2f696d672e736869656c64732e696f2f73656e73696f6c6162732f692f64653033353364642d646631372d343635362d623963302d3165656139356161333061322e7376673f7374796c653d666c61742d737175617265)](https://insight.sensiolabs.com/projects/de0353dd-df17-4656-b9c0-1eea95aa30a2)[![GitHub Issues](https://camo.githubusercontent.com/78cd7d990c15b0f872a9de62c0bcd6bde38b7feb5c54a9c4687d20aa20dfc12e/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6973737565732f737175697069782f6c61726176656c2d6964656d706f74656e63792e7376673f7374796c653d666c61742d737175617265)](https://github.com/squipix/laravel-idempotency/issues)

[![Packagist](https://camo.githubusercontent.com/a223ce5f94e8885dfba55334d347b8d2e9b0138cdba70443d4902251816ed798/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7061636b6167652d737175697069782f6c61726176656c2d6964656d706f74656e63792d626c75652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/squipix/laravel-idempotency)[![Packagist Release](https://camo.githubusercontent.com/462c5fced7037b0468ca432532622dc61918aab19c53ce04b472936ec2a6cdb2/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f737175697069782f6c61726176656c2d6964656d706f74656e63792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/squipix/laravel-idempotency)[![Packagist Downloads](https://camo.githubusercontent.com/7db53298d5dd6d2adf4a54a9f391b32e9d49baff5e3fa2c7ff49cf8cc88a85b1/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f737175697069782f6c61726176656c2d6964656d706f74656e63792e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/squipix/laravel-idempotency)

Stripe-style idempotency for Laravel APIs and queues. Prevent duplicate API requests and duplicate job executions with minimal configuration.

Features
--------

[](#features)

- ✅ **Stripe-style API guarantees** - Handle network retries and duplicate requests safely
- ✅ **Horizontal scaling ready** - Uses Redis for distributed locking
- ✅ **Crash-safe queues** - Prevent duplicate job execution on retries
- ✅ **Payment-grade safety** - Battle-tested for financial transactions
- ✅ **Zero configuration** - Works out of the box with sensible defaults
- ✅ **Payload validation** - Detect and reject requests with same key but different data
- ✅ **Performance optimized** - Redis caching with &lt;1ms lock acquire time

Requirements
------------

[](#requirements)

- PHP 8.1 or higher
- Laravel 10.x, 11.x, or 12.x
- Redis (for distributed locking)

Installation
------------

[](#installation)

Install via Composer:

```
composer require squipix/laravel-idempotency
```

Publish the configuration and migration files:

```
php artisan vendor:publish --tag=idempotency-config
php artisan vendor:publish --tag=idempotency-migrations
```

Run the migration:

```
php artisan migrate
```

Configuration
-------------

[](#configuration)

The configuration file is published at `config/idempotency.php`:

```
return [
    'header' => 'Idempotency-Key',           // HTTP header name
    'lock_ttl' => 10,                        // Lock timeout in seconds
    'response_ttl' => 86400,                 // Response cache TTL (24 hours)
    'reject_payload_mismatch' => true,       // Reject if same key, different payload
    'queue' => [
        'enabled' => true,                   // Enable queue idempotency
        'ttl' => 86400,                      // Job idempotency TTL (24 hours)
    ],
];
```

Usage
-----

[](#usage)

### API Routes

[](#api-routes)

Apply the middleware to routes that need idempotency protection:

```
use Illuminate\Support\Facades\Route;

// Apply to single route
Route::post('/payments', [PaymentController::class, 'store'])
    ->middleware('idempotency');

// Apply to route group
Route::middleware(['auth', 'idempotency'])->group(function () {
    Route::post('/orders', [OrderController::class, 'create']);
    Route::post('/transfers', [TransferController::class, 'execute']);
});
```

### Making Idempotent Requests

[](#making-idempotent-requests)

Clients should send a unique `Idempotency-Key` header with each request:

```
curl -X POST https://api.example.com/payments \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 1000,
    "currency": "USD",
    "customer_id": "cus_123"
  }'
```

**Behavior:**

- First request: Processes normally, returns response
- Duplicate request (same key): Returns cached response immediately
- Same key, different payload: Returns 422 error (configurable)
- Concurrent requests: Second request waits or returns 409

### Queue Jobs

[](#queue-jobs)

Make any job idempotent by adding the middleware and idempotency key:

```
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Squipix\Idempotency\Jobs\IdempotentJobMiddleware;

class CapturePayment implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable;

    public function __construct(
        public string $paymentId,
        public int $amount
    ) {}

    public function middleware(): array
    {
        return [new IdempotentJobMiddleware()];
    }

    public function idempotencyKey(): string
    {
        return "payment-capture:{$this->paymentId}";
    }

    public function handle()
    {
        // Capture payment logic
        // This will never execute twice for the same payment ID
    }
}
```

**Benefits:**

- ✔ Retry-safe
- ✔ Crash-safe
- ✔ No duplicate charges
- ✔ Worker concurrency protection

### Without Idempotency Key (Optional)

[](#without-idempotency-key-optional)

Jobs without an `idempotencyKey()` method will execute normally:

```
class SendEmailJob implements ShouldQueue
{
    // No idempotency middleware - will execute on every attempt

    public function handle()
    {
        // Send email
    }
}
```

How It Works
------------

[](#how-it-works)

### API Idempotency Flow

[](#api-idempotency-flow)

1. **Request arrives** with `Idempotency-Key` header
2. **Check Redis cache** - Return cached response if exists (fastest path)
3. **Acquire distributed lock** - Prevent concurrent execution
4. **Check database** - Return stored response if exists
5. **Execute request** - Process normally
6. **Store response** - Save to both database and Redis
7. **Release lock** - Allow other requests

### Job Idempotency Flow

[](#job-idempotency-flow)

1. **Job dispatched** with `idempotencyKey()`
2. **Check cache** - Skip if already processed
3. **Acquire lock** - Prevent concurrent execution
4. **Execute job** - Run normally
5. **Mark complete** - Store completion flag in cache
6. **On failure** - Clear flag, allow retry

Performance
-----------

[](#performance)

Benchmarked on a standard setup (4GB Redis, Laravel 11, PHP 8.2):

MetricResultLock acquire&lt;1msCache hit replay~0.2msDB replay~5msThroughput20k+ req/secCollision rateZeroEdge Cases Handled
------------------

[](#edge-cases-handled)

### 1. Gateway Timeout After Charge

[](#1-gateway-timeout-after-charge)

Client times out but payment was captured. Retry with same key returns original success response.

### 2. Double Submit from Mobile

[](#2-double-submit-from-mobile)

User taps "Pay" twice quickly. Second request is locked out or returns cached response.

### 3. Payload Mismatch

[](#3-payload-mismatch)

Same idempotency key with different amount/currency is rejected with 422 error.

### 4. Worker Crash Mid-Job

[](#4-worker-crash-mid-job)

Job is retried but idempotency prevents duplicate execution.

### 5. Concurrent Requests

[](#5-concurrent-requests)

Multiple API servers process same key - distributed lock ensures only one executes.

Best Practices
--------------

[](#best-practices)

### Generating Idempotency Keys

[](#generating-idempotency-keys)

**Client-side (Recommended):**

```
// Generate UUID v4
const idempotencyKey = crypto.randomUUID();

// Or use a deterministic key
const idempotencyKey = `order-${orderId}-${timestamp}`;
```

**Server-side:**

```
use Illuminate\Support\Str;

$key = Str::uuid()->toString();
```

### Key Naming Conventions

[](#key-naming-conventions)

Use descriptive, collision-free keys:

```
// Good
"payment-capture-{$paymentIntentId}"
"refund-{$refundId}-{$timestamp}"
"order-{$userId}-{$cartHash}"

// Bad (collision risk)
"payment-{$userId}"  // User could make multiple payments
"order-123"          // Ambiguous
```

### Cleanup Old Records

[](#cleanup-old-records)

Schedule a cleanup command:

```
use Illuminate\Console\Scheduling\Schedule;
use Squipix\Idempotency\Services\IdempotencyService;

protected function schedule(Schedule $schedule)
{
    $schedule->call(function () {
        app(IdempotencyService::class)->cleanupExpiredRecords(7);
    })->daily();
}
```

Advanced Configuration
----------------------

[](#advanced-configuration)

### Custom Cache Store

[](#custom-cache-store)

```
// In a service provider
use Squipix\Idempotency\Services\IdempotencyService;

$this->app->singleton(IdempotencyService::class, function ($app) {
    return new IdempotencyService(
        $app['cache']->store('redis-cluster'),
        $app['db']->connection('mysql')
    );
});
```

### Custom Header Name

[](#custom-header-name)

```
// config/idempotency.php
return [
    'header' => 'X-Request-ID',  // Use custom header
    // ...
];
```

### Disable Payload Validation

[](#disable-payload-validation)

```
// config/idempotency.php
return [
    'reject_payload_mismatch' => false,  // Allow payload changes
    // ...
];
```

Testing
-------

[](#testing)

### Testing API Idempotency

[](#testing-api-idempotency)

```
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PaymentTest extends TestCase
{
    use RefreshDatabase;

    public function test_duplicate_payment_request_returns_cached_response()
    {
        $key = 'test-payment-' . uniqid();

        // First request
        $response1 = $this->postJson('/api/payments', [
            'amount' => 1000,
            'currency' => 'USD',
        ], [
            'Idempotency-Key' => $key,
        ]);

        $response1->assertStatus(201);

        // Duplicate request
        $response2 = $this->postJson('/api/payments', [
            'amount' => 1000,
            'currency' => 'USD',
        ], [
            'Idempotency-Key' => $key,
        ]);

        $response2->assertStatus(201);
        $this->assertEquals($response1->json(), $response2->json());
    }

    public function test_same_key_different_payload_returns_422()
    {
        $key = 'test-payment-' . uniqid();

        $this->postJson('/api/payments', ['amount' => 1000], [
            'Idempotency-Key' => $key,
        ])->assertStatus(201);

        $this->postJson('/api/payments', ['amount' => 2000], [
            'Idempotency-Key' => $key,
        ])->assertStatus(422);
    }
}
```

### Testing Job Idempotency

[](#testing-job-idempotency)

```
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Queue;

public function test_job_executes_only_once_on_retry()
{
    Cache::flush();

    $job = new CapturePayment('payment_123', 1000);

    // First execution
    $job->handle();

    // Simulate retry
    $job->handle();

    // Assert payment captured only once
    $this->assertEquals(1, Payment::where('id', 'payment_123')->count());
}
```

Troubleshooting
---------------

[](#troubleshooting)

### Missing Idempotency-Key Error

[](#missing-idempotency-key-error)

**Problem:** API returns "Idempotency-Key required"

**Solution:** Ensure client sends the header. GET requests are automatically skipped.

### Redis Connection Issues

[](#redis-connection-issues)

**Problem:** Lock timeouts or "No connection could be made"

**Solution:**

1. Verify Redis is running: `redis-cli ping`
2. Check Laravel cache config uses Redis
3. Test connection: `php artisan cache:clear`

### High Memory Usage

[](#high-memory-usage)

**Problem:** `idempotency_keys` table growing too large

**Solution:**

1. Set up cleanup job (see Best Practices)
2. Add index on `created_at` (already included in migration)
3. Consider shorter `response_ttl`

### Payload Hash Mismatch False Positives

[](#payload-hash-mismatch-false-positives)

**Problem:** Same payload rejected as different

**Solution:** Ensure request payloads are identical, including:

- Key order (arrays are sorted automatically)
- Data types (string "100" vs int 100)
- Nested objects

Metrics &amp; Monitoring
------------------------

[](#metrics--monitoring)

The package includes comprehensive metrics support for production monitoring.

### Quick Setup

[](#quick-setup)

```
# Enable metrics
IDEMPOTENCY_METRICS_ENABLED=true

# Prometheus support
IDEMPOTENCY_PROMETHEUS_ENABLED=true

# Laravel Pulse support
IDEMPOTENCY_PULSE_ENABLED=true
```

### Collected Metrics

[](#collected-metrics)

- **Cache hits/misses** - Monitor cache performance
- **Lock acquisitions/failures** - Track concurrent requests
- **Payload mismatches** - Detect client-side issues
- **Request duration** - Performance monitoring (p50, p95, p99)
- **Job executions/skips** - Queue idempotency tracking
- **Error rates** - System health monitoring

### Supported Platforms

[](#supported-platforms)

- **Prometheus** - Industry-standard metrics with Grafana dashboards
- **Laravel Pulse** - Real-time application monitoring
- **Custom backends** - Extensible architecture

### Example Prometheus Queries

[](#example-prometheus-queries)

```
# Cache hit ratio
sum(rate(idempotency_cache_hits_total[5m])) /
(sum(rate(idempotency_cache_hits_total[5m])) + sum(rate(idempotency_cache_misses_total[5m])))

# 95th percentile response time
histogram_quantile(0.95, rate(idempotency_request_duration_seconds_bucket[5m]))

# Requests per second
sum(rate(idempotency_cache_hits_total[5m])) + sum(rate(idempotency_cache_misses_total[5m]))

```

**For detailed metrics setup, see [METRICS.md](METRICS.md)**

License
-------

[](#license)

This package is open-sourced software licensed under the [MIT license](LICENSE).

Credits
-------

[](#credits)

Inspired by [Stripe's idempotency implementation](https://stripe.com/docs/api/idempotent_requests).

- [SQUIPIX](https://github.com/squipix)
- [All Contributors](https://github.com/squipix/laravel-idempotency/graphs/contributors)

Support
-------

[](#support)

For issues, questions, or contributions, please visit the [GitHub repository](https://github.com/squipix/laravel-idempotency).

###  Health Score

41

—

FairBetter than 89% of packages

Maintenance98

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity52

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~60 days

Total

2

Last Release

12d ago

PHP version history (2 changes)v1.0.0PHP ^8.1|^8.2|^8.3

1.1.0PHP ^8.1|^8.2|^8.3|^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/1fee4c81050395e99aad4f79512dbed9370222eafa7d29ed95a6ef89bafad370?d=identicon)[anasnaguib](/maintainers/anasnaguib)

---

Top Contributors

[![anasnaguib](https://avatars.githubusercontent.com/u/3796556?v=4)](https://github.com/anasnaguib "anasnaguib (9 commits)")

---

Tags

apilaravelstripequeueidempotency

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/squipix-laravel-idempotency/health.svg)

```
[![Health](https://phpackages.com/badges/squipix-laravel-idempotency/health.svg)](https://phpackages.com/packages/squipix-laravel-idempotency)
```

###  Alternatives

[imtigger/laravel-job-status

Laravel Job Status

5312.1M2](/packages/imtigger-laravel-job-status)[harris21/laravel-fuse

Circuit breaker for Laravel queue jobs. Protect your workers from cascading failures.

2471.2k](/packages/harris21-laravel-fuse)[pierophp/laravel-queue-manager

Laravel Queue Manager

182.4k](/packages/pierophp-laravel-queue-manager)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
