Cuando trabajamos con Jobs y colas en Laravel, asumimos algo que en la práctica no siempre se cumple: que un Job se ejecutará una sola vez.
En entornos reales —Redis, Supervisor, Horizon, reintentos automáticos, timeouts o despliegues— un Job puede ejecutarse más de una vez, incluso cuando el código está “bien”.
Aquí es donde entra un concepto clave de arquitectura backend:
Idempotencia
Un Job idempotente es aquel que puede ejecutarse múltiples veces sin producir efectos secundarios no deseados.
En este artículo veremos:
Qué es la idempotencia aplicada a Jobs
Por qué es crítica en Laravel
Errores comunes
Estrategias prácticas para implementarla correctamente
¿Qué es idempotencia?
En términos simples:
Una operación es idempotente si ejecutarla una o varias veces produce el mismo resultado final.
Ejemplo conceptual:
❌ No idempotente:
Crear una orden, descontar stock y cobrar al cliente cada vez que corre el Job.✅ Idempotente:
Verificar si la orden ya fue procesada antes de crearla o cobrarla.
En Jobs, la pregunta clave siempre es:
¿Qué pasa si este Job se ejecuta dos veces?
Si la respuesta es “se duplica información, se cobra de nuevo o se rompe algo”, no es idempotente.
¿Por qué los Jobs pueden ejecutarse más de una vez?
Aunque no lo parezca, es completamente normal que ocurra:
Reintentos automáticos (
)triesTimeouts de workers
Fallos de red o base de datos
Reinicio de Supervisor
Deploys mientras hay Jobs en proceso
Deadlocks
Horizon reencolando Jobs
Laravel garantiza al menos una ejecución, no exactamente una.
Errores comunes al no manejar idempotencia
Algunos ejemplos reales en producción:
Registros duplicados
Correos enviados múltiples veces
Cobros repetidos
Inventarios negativos
Logs inconsistentes
Depreciaciones o cálculos financieros duplicados
Todos estos problemas no se solucionan con try/catch.
Se solucionan diseñando Jobs idempotentes.
Estrategias para hacer Jobs idempotentes en Laravel
1. Llaves únicas en base de datos (la base de todo)
Si un Job crea un registro, la base de datos debe protegerte.
$table->unique(['order_id', 'type']);
Y luego:
Model::firstOrCreate([ 'order_id' => $orderId, 'type' => 'payment', ]);
Si el Job corre dos veces, el segundo no hace nada.
2. Flags de estado antes de ejecutar lógica crítica
Antes de ejecutar la lógica principal:
if ($order->status === 'processed') { return; }
Después del proceso:
$order->update(['status' => 'processed']);
Esto aplica especialmente en:
Facturación
Procesos financieros
Asignaciones
Depreciaciones
Integraciones externas
3. Locks con Cache (Redis recomendado)
Laravel permite locks distribuidos:
Cache::lock("job:process-order:{$orderId}", 60)->block(5, function () { // lógica crítica });
Esto evita que dos workers ejecuten el mismo Job al mismo tiempo.
Ideal para:
Procesos largos
Jobs pesados
Integraciones externas
4. Uso correcto de ShouldBeUnique
ShouldBeUniqueLaravel ofrece:
class ProcessOrder implements ShouldQueue, ShouldBeUnique { public function uniqueId() { return $this->orderId; } }
Esto evita que el Job se encole dos veces, pero ojo:
❗ NO reemplaza la idempotencia interna
Si el Job falla a mitad del proceso y se reintenta, el código aún debe ser seguro.
5. Registrar ejecuciones (auditoría)
En procesos críticos, registra ejecuciones:
JobExecution::create([ 'job' => self::class, 'reference_id' => $orderId, 'executed_at' => now(), ]);
Antes de ejecutar:
if (JobExecution::where('job', self::class) ->where('reference_id', $orderId) ->exists()) { return; }
Muy útil para:
Auditoría
Depuración
Procesos financieros o legales
Regla de oro para Jobs en Laravel
Todo Job debe asumirse como si pudiera ejecutarse más de una vez.
Si tu Job:
Inserta datos
Modifica estados
Llama APIs
Cobra dinero
Mueve inventario
Debe ser idempotente. Siempre.
Conclusión
La idempotencia no es un “extra” ni una optimización avanzada.
Es un requisito de arquitectura cuando trabajas con colas y procesos asíncronos.
Laravel te da herramientas:
Base de datos
Locks
Unique Jobs
Estados
Pero la responsabilidad final es del diseño del Job.
Un Job idempotente:
Es seguro
Es predecible
Escala mejor
No genera errores silenciosos en producción
Y, sobre todo, duerme tranquilo al desarrollador.