Skip to main content
Laravel, shipping fast.

Here's the uncomfortable truth about queues: your job will run more than once, eventually. A worker gets killed mid-execution during a deploy. A job exceeds its timeout and the queue driver assumes it's dead and hands it to another worker, right as the first worker's process is still finishing up. Laravel's queues are at-least-once, not exactly-once, and no configuration option changes that. The job has to be able to run twice and produce the same end state as running once.

Deletes are naturally forgiving here: deleting something that's already gone is a no-op, not an error. Creates are the opposite. Calling the license-creation endpoint twice with the same input creates two licenses, not one. That asymmetry is exactly why DeleteLicenseJob above is comfortable retrying blind and LicenseCreateAction is not:

// app/Http/Integrations/Statamic/StatamicAPI.php
public function addLicense(string $name, string $domain): array
{
    $response = $this->send('POST', '/sites', [
        'name' => trim($name),
        'domain' => trim($domain),
    ]);

    $data = $response->json('data');

    if (! is_array($data) || ! isset($data['key'])) {
        throw new \RuntimeException(
            'Statamic addLicense returned a malformed response: '
            .json_encode(['status' => $response->status(), 'body' => $response->body()])
        );
    }

    return $data;
}

Notice what this does not do: it doesn't silently treat a 2xx with a missing key as "probably fine, try again." It throws. A creation call that returns something it can't parse is exactly the ambiguous case where a naive retry produces a duplicate license, so the driver refuses to guess and forces the caller's retry policy to make that decision explicitly instead of making it by accident.

// Bad: retries a create with no way to tell "already succeeded" from "actually failed"
public function handle(): void
{
    try {
        LicenseFacade::addLicense(name: $this->url, domain: $this->url);
    } catch (Throwable $e) {
        $this->release(30); // if the first call actually succeeded, this makes a second license
    }
}
// Good: collapse duplicate deliveries with an atomic claim before doing the work
if (! Cache::add("license:site:{$site->id}:issuing", true, now()->addMinutes(10))) {
    return; // another delivery of this job already claimed it
}

The Cache::add() idiom is the same one the fleet uses to make a chain's terminal-failure handler idempotent when both the chain's own catch() and a separate watchdog process can independently decide to fail the same resource. Cache::add() only succeeds for the first caller; every redelivery after that sees the key already present and backs off. Idempotency isn't a property jobs have by default. It's something you build, once, with a claim key, and reuse everywhere a duplicate delivery would otherwise do real damage.