Usa una tabla de Postgres antes de recurrir a una cola
No necesitas una cola de mensajes. Necesitas una tabla.
En cuanto alguien dice "trabajos en segundo plano" o "trabajo asíncrono", el reflejo es levantar SQS, RabbitMQ o Kafka. Ahora operas un segundo sistema con estado: su propia disponibilidad, sus propios modos de falla, sus propios dashboards, su propia forma de perder mensajes. Para mucho trabajo de bajo a medio volumen, ya tienes todo lo que necesitas en la base de datos que de todos modos estás corriendo.
El reflejo cuesta más de lo que parece
Un broker dedicado es superficie operativa real. Tienes que aprovisionarlo, monitorearlo, asegurarlo y mantenerlo disponible. También introduces un problema de doble escritura: tu app ahora escribe en Postgres y en la cola, y mantener esos dos sincronizados ante fallas (¿la fila hizo commit pero el encolado falló?) es exactamente el tipo de bug distribuido que se come semanas. Para un equipo pequeño con volumen modesto, es mucho costo por capacidad que no estás usando.
Una tabla es una cola perfectamente válida
Si ya corres Postgres, y tus productores y consumidores viven en el mismo despliegue, una tabla simple más una cláusula te da una cola correcta y durable. La clave es FOR UPDATE SKIP LOCKED, que permite que muchos workers tomen filas distintas de forma concurrente sin bloquearse entre sí:
CREATE TABLE jobs (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'pending',
run_after timestamptz NOT NULL DEFAULT now(),
attempts int NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now()
);
-- Un worker reclama un trabajo de forma atómica:
SELECT id, payload
FROM jobs
WHERE status = 'pending' AND run_after <= now()
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 1;
Como el reclamo y el encolado del trabajo pueden ocurrir en la misma transacción que tus escrituras de negocio, el problema de doble escritura desaparece: o la fila y el trabajo hacen commit ambos, o ninguno. Obtienes durabilidad, reintentos (incrementa attempts, ajusta run_after para el backoff) y un estado de dead-letter (status = 'failed') gratis, todo con herramientas que ya operas y que puedes consultar con SQL plano.
Cuándo graduarse de verdad
El punto no es "nunca uses un broker", es "no empieces con uno". Recurre a SQS/RabbitMQ/Kafka cuando llegues a límites reales:
| Señal | Quédate en Postgres | Muévete a un broker |
|---|---|---|
| Throughput | Cientos–miles/seg | Decenas de miles+/seg sostenidos |
| Productores y consumidores | Mismo despliegue / BD | Muchos servicios independientes |
| Fan-out / pub-sub | Cola de trabajo simple | Topics, muchos suscriptores, streaming |
| Orden y particionado | No crítico | Orden estricto por clave a escala |
| Carga de la BD | Con holgura de sobra | El polling de la cola compite con las consultas de la app |
Librerías maduras se apoyan exactamente en este patrón — las colas sobre Postgres son un camino trillado, no un truco. Gradúate cuando la evidencia lo fuerce, y la migración será obvia.
Movidas avanzadas
- Usa
SKIP LOCKED, no unSELECT ... LIMIT 1ingenuo — sin él, los workers concurrentes compiten por la misma fila y se serializan. - Agrega un índice parcial sobre
(status, run_after) WHERE status = 'pending'para que el reclamo siga siendo rápido a medida que la tabla crece. - Considera LISTEN/NOTIFY para despertar a los workers al instante en vez de bucles de polling apretados.
- Archiva o borra las filas completadas de forma programada para que la tabla caliente se mantenga pequeña.
- Usa una librería probada (p. ej.
pgmq,graphile-worker,River,Oban,solid_queue) antes de codificar a mano casos borde como los visibility timeouts.
Recursos
FOR UPDATE ... SKIP LOCKED— documentación de PostgreSQL- "Choose Boring Technology" — Dan McKinley
LISTEN/NOTIFY— documentación de PostgreSQL- pgmq — extensión ligera de cola de mensajes para Postgres
¿Desarrollas sistemas de IA o en la nube? Yeda AI audita y refuerza pipelines de LLM y de datos en producción. Hablemos · Lee el blog