> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Control de ejecución de jobs de cola

> Explica cómo controlar la ejecución duplicada y los despachos consecutivos de jobs de cola mediante ShouldBeUnique, ShouldBeUniqueUntilProcessing y DebounceFor.

## Descripción general

La funcionalidad de colas de Laravel ofrece dos tipos de control de ejecución: la **deduplicación (Unique)** y el **debounce (Debounce)** de jobs. Ambos mecanismos sirven para «evitar ejecuciones innecesarias cuando el mismo job se despacha varias veces», pero su comportamiento es distinto.

| Funcionalidad           | Interfaz / atributo             | Objetivo                                                                              |
| ----------------------- | ------------------------------- | ------------------------------------------------------------------------------------- |
| Unique Jobs             | `ShouldBeUnique`                | Garantizar que en la cola exista un único job idéntico                                |
| Unique Until Processing | `ShouldBeUniqueUntilProcessing` | Mantener la restricción de unicidad solo hasta que empieza el procesamiento           |
| Debounced Jobs          | `#[DebounceFor]`                | Ejecutar únicamente el último despacho cuando se despacha varias veces en poco tiempo |

<Warning>
  Unique Jobs y Debounced Jobs son **excluyentes**. No implementes `ShouldBeUnique` en un job que utilice el atributo `DebounceFor`.
</Warning>

***

## Unique Jobs — `ShouldBeUnique`

Mientras el mismo job esté en la cola, se ignoran los despachos adicionales.

```php theme={null}
<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    // No se requieren métodos adicionales
}
```

Mientras `UpdateSearchIndex` esté encolado (o en procesamiento), cualquier intento de despachar el mismo job se ignora.

### Restringir la unicidad con una clave — `UniqueFor` + `uniqueId()`

Si aun tratándose de la misma clase de job quieres tratar «actualizar el producto A» y «actualizar el producto B» como jobs distintos, define la clave con el método `uniqueId()`.

```php theme={null}
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Attributes\UniqueFor;

#[UniqueFor(3600)] // El bloqueo se libera automáticamente al cabo de 1 hora
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    public function __construct(public readonly int $productId)
    {
    }

    public function uniqueId(): string
    {
        return (string) $this->productId;
    }
}
```

* El valor devuelto por `uniqueId()` es la clave del bloqueo en caché.
* Al indicar `#[UniqueFor(segundos)]`, el bloqueo se libera automáticamente tras esos segundos (medida de seguridad para el caso en que el job no llegue a procesarse).

### Especificar el driver de caché — `uniqueVia()`

Si quieres utilizar un driver de caché distinto al predeterminado, implementa `uniqueVia()`.

```php theme={null}
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function uniqueVia(): Repository
{
    return Cache::driver('redis');
}
```

<Info>
  Unique Jobs requiere un driver de caché que soporte bloqueos atómicos (`redis`, `database`, `memcached`, `dynamodb`, `file` o `array`).
</Info>

***

## `ShouldBeUnique` vs `ShouldBeUniqueUntilProcessing`

El bloqueo de `ShouldBeUnique` se mantiene **hasta que el job se completa o alcanza el límite de reintentos**. Esto puede ser un problema en algunos casos.

**Ejemplo:** hay 1 `UpdateSearchIndex(product_id: 42)` en la cola y quieres volver a despachar el mismo job justo después de que el worker empiece a procesarlo. Con `ShouldBeUnique`, el segundo job no se encolará hasta que finalice el procesamiento.

En este caso utiliza `ShouldBeUniqueUntilProcessing`. Como el bloqueo se **libera justo antes de comenzar el procesamiento**, se puede despachar el siguiente en cuanto el worker toma el job.

```php theme={null}
<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
    // ...
}
```

```mermaid theme={null}
sequenceDiagram
    participant D as Dispatcher
    participant Q as Cola
    participant W as Worker

    D->>Q: dispatch() — adquiere bloqueo
    D->>Q: dispatch() — bloqueo activo → ignorado

    note over Q,W: Caso ShouldBeUnique
    W->>Q: toma el job
    W->>W: procesando (bloqueo retenido)
    D->>Q: dispatch() — bloqueo activo → ignorado
    W->>W: procesamiento finalizado — se libera el bloqueo
    D->>Q: dispatch() — ahora sí se encola

    note over Q,W: Caso ShouldBeUniqueUntilProcessing
    W->>Q: toma el job — se libera el bloqueo
    D->>Q: dispatch() — sin bloqueo → se encola
    W->>W: procesando
```

### Comparativa

|                                             | `ShouldBeUnique`                            | `ShouldBeUniqueUntilProcessing`                                   |
| ------------------------------------------- | ------------------------------------------- | ----------------------------------------------------------------- |
| Momento de liberación del bloqueo           | Tras completarse / fallar el procesamiento  | Justo antes de comenzar el procesamiento                          |
| Despacho duplicado durante el procesamiento | Ignorado                                    | Se encola                                                         |
| Caso de uso                                 | Evitar por completo la ejecución simultánea | Poder encolar el siguiente job en cuanto empieza el procesamiento |

***

## Debounced Jobs — `#[DebounceFor]`

<Info>
  El atributo `DebounceFor` es una funcionalidad añadida en Laravel 13.
</Info>

Cuando el mismo job se despacha muchas veces en poco tiempo, se ejecuta **solo el último despachado**. Es la misma idea que el debounce en un frontend web.

```php theme={null}
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\DebounceFor;

#[DebounceFor(30)] // Los redespachos dentro de 30 s se ignoran (solo se ejecuta el último)
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;

    public function __construct(public readonly int $productId)
    {
    }

    public function debounceId(): string
    {
        return (string) $this->productId;
    }
}
```

* El valor devuelto por `debounceId()` identifica el job (el debounce se aplica de forma independiente por cada ID de producto).
* Aunque se despache 10 veces con el mismo `productId` en 30 s, solo se ejecuta el último.

### `maxWait` — límite del tiempo máximo de espera

Con datos que se actualizan con mucha frecuencia, el debounce podría prolongarse indefinidamente y el job podría no ejecutarse nunca. Con `maxWait` puedes fijar el retraso máximo.

```php theme={null}
#[DebounceFor(30, maxWait: 120)]
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;
    // ...
}
```

En este ejemplo, el job se ejecuta como muy tarde 120 s después del primer despacho (aunque el debounce de 30 s siga renovándose, se cortará a los 120 s).

### Especificar el driver de caché — `debounceVia()`

```php theme={null}
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function debounceVia(): Repository
{
    return Cache::driver('redis');
}
```

### Evento `JobDebounced`

Los jobs que quedan sobrescritos por un despacho posterior emiten el evento `Illuminate\Queue\Events\JobDebounced` y se eliminan de la cola. Escuchando este evento puedes rastrear y monitorizar los jobs debounced.

***

## Cuál elegir

```mermaid theme={null}
flowchart TD
    A["El mismo job puede despacharse varias veces"] --> B{"¿Despachos consecutivos en poco tiempo<br>y solo quieres ejecutar el último?"}
    B -->|Sí| C["#[DebounceFor]"]
    B -->|No| D{"¿Debe existir un único<br>job en la cola?"}
    D -->|Sí| E{"¿Quieres evitar duplicados también<br>durante el procesamiento?"}
    E -->|Sí| F["ShouldBeUnique"]
    E -->|No| G["ShouldBeUniqueUntilProcessing"]
    D -->|No| H["Job normal"]
```

| Caso de uso                                                                                             | Recomendación                   |
| ------------------------------------------------------------------------------------------------------- | ------------------------------- |
| No tiene sentido tener dos o más jobs iguales en la cola                                                | `ShouldBeUnique`                |
| Quieres evitar la ejecución en paralelo incluso durante el procesamiento                                | `ShouldBeUnique`                |
| Quieres poder encolar el siguiente en cuanto el worker toma el job                                      | `ShouldBeUniqueUntilProcessing` |
| Aunque el usuario pulse el botón «Guardar» varias veces, quieres ejecutarlo una sola vez                | `#[DebounceFor]`                |
| Reconstruir el índice de búsqueda cada vez que se actualiza el modelo (durante actualizaciones masivas) | `#[DebounceFor]` + `maxWait`    |

***

## Implementación interna

### Mecanismo de bloqueo de Unique Jobs

Cuando se despacha un job `ShouldBeUnique`, Laravel obtiene internamente un [bloqueo atómico](/es/cache#operaciones-atómicas-bloqueos) de la caché. La clave del bloqueo tiene el siguiente formato:

```
laravel_unique_job:{nombreDeClaseDelJob}:{uniqueId()}
```

Si no se puede obtener el bloqueo (porque otro job ya lo tiene), el job no se añade a la cola.

### Implementación de los Debounced Jobs

`DebounceFor` utiliza internamente una entrada de caché que gestiona una «ventana de debounce». Cada vez que llega un nuevo despacho:

1. Se elimina el job existente de la cola (emitiendo el evento `JobDebounced`).
2. Se añade el nuevo job a la cola (con el retraso indicado por los segundos de debounce).
3. Se reinicia el temporizador en la caché.

Si `maxWait` está definido, también se registra la marca de tiempo del primer despacho para evitar que el debounce se prolongue más allá de los segundos de `maxWait`.

***

## Enlaces de referencia

* [Documentación oficial de Laravel — Unique Jobs](https://laravel.com/docs/queues#unique-jobs)
* [Documentación oficial de Laravel — Debounced Jobs](https://laravel.com/docs/queues#debounced-jobs)
* [`Illuminate\Contracts\Queue\ShouldBeUnique`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Contracts/Queue/ShouldBeUnique.php)
* [`Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Contracts/Queue/ShouldBeUniqueUntilProcessing.php)
* [`Illuminate\Queue\Attributes\DebounceFor`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Queue/Attributes/DebounceFor.php)
* [`Illuminate\Queue\Attributes\UniqueFor`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Queue/Attributes/UniqueFor.php)


## Related topics

- [Técnicas prácticas de Laravel Telescope](/es/blog/telescope-introduction.md)
- [Introducción a Laravel Nightwatch](/es/blog/nightwatch-introduction.md)
- [Laravel Horizon](/es/horizon.md)
- [Programación de tareas](/es/scheduling.md)
- [Resumen de las novedades de Laravel 13](/es/blog/laravel-13-new-features.md)
