Tarde o temprano, todo job corre dos veces. El worker cae después de procesar y antes del ack, el visibility timeout expira en medio de la ejecución, una partición de red divide la verdad entre dos nodos. Un sistema de colas diseñado para ejecución única acumula cobros duplicados, emails repetidos e inventario descuadrado. Diseña cada handler para ejecutarse al menos dos veces.

¿Cómo garantizar idempotencia en la práctica?

Tres técnicas cubren casi todos los casos. Claves naturales deduplican por usuario, acción y ventana: UNIQUE(user_id, action, DATE(created_at)) bloquea el segundo procesamiento del mismo evento en el día. Idempotency tokens guardados bajo constraint unique convierten el retry en no-op; el segundo insert falla y el handler devuelve éxito. Updates condicionales retornan filas afectadas: cero filas significa trabajo ya hecho por otra ejecución.

Visibility timeout: ¿cuánto ajustar?

Punto de partida: el p99 de la duración del job multiplicado por dos. Timeout demasiado corto genera duplicados en masa; demasiado largo retrasa el retry de jobs perdidos. Cada herramienta implementa el mecanismo a su manera: BullMQ renueva locks, SQS expone el parámetro nativo VisibilityTimeout, Sidekiq usa reliable fetch con super_fetch. Monitorea los jobs que se pasan del timeout; ellos son la fuente de los duplicados.

Retry sin thundering herd

Backoff exponencial con jitter esparce los intentos: 1s, 4s, 16s con ruido aleatorio impide que mil workers martillen el servicio externo en el mismo segundo. Clasifica el error antes de agendar: 5xx y timeout son retryables; la validación rechazada es permanente y va directo a la dead-letter queue. Max attempts agotado alimenta la DLQ con el payload original, listo para replay manual durante el incidente.

Jerarquía de colas

Una cola única mezcla captura de pago con exportación de reportes, y el backlog del bulk mata la criticidad. Separa tres niveles con workers dedicados: critical (payments, antifraude), default (flujo normal) y bulk (emails, exports). La exportación de 200 mil registros deja de retrasar el webhook de captura.

Observabilidad y jobs recurrentes

  • Profundidad de la cola, wait time p95, processing time p95 y failure rate por clase de job
  • La alerta dispara con la tendencia de crecimiento de la profundidad; un valor absoluto alto puede ser rutina de hora pico

El job recurrente necesita lock distribuido: SETNX en Redis con TTL o advisory lock de Postgres (pg_advisory_lock). Sin lock, dos schedulers hacen fire doble en el mismo cron, y el token de idempotencia se vuelve la última línea de defensa. Pon el lock; cuesta una línea y evita el ticket de madrugada.