Let the queue drop duplicate jobs
Problem. Put a heavy job behind an "Export orders" button and every click queues another copy. Impatient users click two, three, ten times, and each click is another full export competing for the same workers and the same database.
Guarding the dispatch with a cache flag looks like a fix, but Cache::has() followed by Cache::put() is two separate steps, so two quick requests can both get past the check. The flag also outlives the job: it stays set after the export has finished, so the button does nothing until the flag expires.
Fix. Implement ShouldBeUnique on the job and return the key that defines a duplicate from uniqueId(). When the job is dispatched, Laravel takes an atomic cache lock named after the job class and that key. If the lock is already held, the new dispatch is dropped. The lock is released as soon as the job finishes, or once it has failed all of its retries.
The #[UniqueFor] attribute is the safety net. It releases the lock after that many seconds even if a worker dies halfway through, so one crash cannot block exports for good.
// Before: a flag that two quick clicks can both get pastif (! Cache::has("export:{$team->id}")) { Cache::put("export:{$team->id}", true, 900); ExportOrders::dispatch($team);}// After: the queue holds the lock#[UniqueFor(900)]class ExportOrders implements ShouldQueue, ShouldBeUnique{ use Queueable; public function __construct(public Team $team) {} // One export per team; other teams still run in parallel public function uniqueId(): string { return (string) $this->team->id; } public function handle(): void { // Build and email the CSV... }}// Ignored while this team's export is queued or runningExportOrders::dispatch($team);Result. Extra clicks become no-ops instead of extra exports, and there is no flag to clear or forget. The lock lives in the cache, so use a store that supports atomic locks (Redis, Memcached, DynamoDB, database, file or array) and make sure every server shares it. If you only want to stop two copies running at once but still queue the second one, use the WithoutOverlapping job middleware instead.