Skip to content
enes
2026-04-11engineering7 min de lectura

SQS entre servicios, no solo para jobs largos.

Las llamadas síncronas son el reflejo por default. La mitad no debería serlo. Una cola entre productor y consumer cambia la forma de la falla del sistema entero.

La mayoría de los equipos usa SQS solo para jobs largos: procesamiento de imágenes, transcoding de video, reportes en background. Funciona bien para eso, y deja de lado la razón más grande por la que existen las colas: cambian el modo de falla de cada acoplamiento entre servicios.

Una llamada HTTP síncrona del servicio A al B tiene un estado sano y una lista larga de rotos. B está arriba con buena latencia, todo bien. B está lento, los threads de A se acumulan. B está caído, A devuelve 500s. B está reiniciando, el connection pool de A picotea. Una cola reemplaza todos esos por un único estado estable: el mensaje queda ahí hasta que B pueda tomarlo.

Cuándo una cola se gana su lugar

Tres señales indican que una llamada sync probablemente debería ser una cola.

El receptor no necesita estar vivo cuando el productor publica. Un usuario sube un archivo, querés procesarlo, el procesamiento toma tiempo. Una llamada sync significa que el upload espera. Una cola significa que el upload retorna inmediatamente, el procesamiento ocurre cuando hay capacidad, y el usuario ve una máquina de estados: encolado → procesando → listo.

El tráfico es bursty. Picos de venta. Cron jobs que se disparan en todo el fleet en el mismo minuto. Webhooks de un proveedor que batchea retries. Sin cola, tu input bursty se vuelve el perfil de carga de tu servicio. Con cola, la cola absorbe el burst y el consumer la drena a su ritmo.

El trabajo puede fallar y querés reintentar. El retry de una llamada sync es lo que decida el caller, usualmente nada. El retry de una cola viene incorporado: visibility timeout, redrive policy, dead-letter queue. La infraestructura lleva la lógica así el productor no tiene que hacerlo.

Si ninguna aplica, una cola es overhead que no necesitás.

La forma

El productor publica a una cola. El consumer pollea la cola, procesa un mensaje, lo borra cuando termina bien. Si falla, el mensaje vuelve a la cola tras el visibility timeout. Si falla repetidas veces, va a una dead-letter queue. Esa es toda la interfaz.

Diagrama: Servicio A encola en SQS, Worker recibe, tras N fallas el mensaje termina en DLQ.

El productor no sabe cuántos consumers existen ni qué tan rápido procesan. El consumer no sabe cuántos productores hay ni cómo es su perfil de bursts. Solo comparten la forma del mensaje. Ese desacoplamiento es el punto.

Lo que tenés que configurar bien

Idempotencia

Las colas Standard de SQS son entrega at-least-once. Un mensaje puede entregarse a tu consumer más de una vez. Incluso un procesamiento exitoso seguido de una falla de red en el delete resulta en que el mensaje reaparezca.

Tu consumer tiene que ser idempotente. Procesar el mismo mensaje dos veces tiene que producir el mismo resultado que procesarlo una. Si tu trabajo es "crear fila en la DB", incluí el message ID como idempotency key. Si tu trabajo es "mandar email", chequeá si ya mandaste para ese message ID. Si tu trabajo es "transferir plata", no deployes sin idempotency keys.

El modelo mental por default es "la cola no entrega duplicados". Ese modelo está mal. Construí para "la cola podría entregar duplicados".

Visibility timeout

Cuando un consumer agarra un mensaje, la cola lo esconde por la ventana de visibility timeout. Si el consumer no borra el mensaje dentro de esa ventana, reaparece para que otro consumer lo agarre. La ventana cubre el caso "consumer crasheó a la mitad del procesamiento".

La trampa: si tu procesamiento tarda más que el visibility timeout, el mensaje reaparece mientras seguís trabajando. Ahora dos consumers procesan el mismo mensaje en paralelo. Con idempotencia es derroche pero inofensivo, sin ella es un bug.

Configurá el visibility timeout a un sobrestimado generoso de cuánto tarda el trabajo. O usá el patrón long-running: extendé la visibility con un heartbeat del consumer (ChangeMessageVisibility). El primero es más simple, el segundo maneja el caso donde algunos mensajes son 10× la duración típica.

Dead-letter queue

Un mensaje envenenado, uno que consistentemente falla, sin DLQ va a reintentarse para siempre. El visibility timeout significa que cicla de vuelta a la cola activa, lo agarra alguien, falla, y el ciclo se repite hasta que intervengas a mano. Mientras tanto la alarma de queue depth está sonando, la tasa de error de tu consumer se ve mal, y no sabés si un mensaje malo está arrastrando todo el sistema o si todo está roto.

Una DLQ con un maxReceiveCount razonable (3-5 es el rango común) saca al mensaje malo del medio. La cola activa se drena, las métricas te dicen cuál mensaje específicamente está envenenado, y podés investigar a tu ritmo.

La DLQ también es la unión entre retry y observabilidad. Un mensaje en la DLQ es una alerta. El número de mensajes en la DLQ a lo largo del tiempo es una métrica de salud. Sin DLQ, ninguna de las dos señales existe.

Long polling

Short polling, el default, hace que el consumer pegue contra la cola continuamente, incluso cuando está vacía, pagando un request por poll. Long polling le dice a la cola "si no hay mensaje, esperá hasta 20 segundos antes de responder". El consumer hace muchas menos requests.

Configurá WaitTimeSeconds a 20 (el máximo) en cada llamada del consumer. La penalidad de latencia para el primer mensaje en una cola vacía es como mucho una ida y vuelta. En steady state con tráfico, la cola responde inmediatamente.

Standard vs FIFO

Las colas Standard son entrega at-least-once y orden best-effort. Escalan a throughput casi ilimitado. Las FIFO son exactly-once (con deduplicación) y orden estricto, capadas a throughput menor por group.

La mayoría de las cargas son Standard. El orden no importa para procesamiento de imágenes, envío de email, agregación de logs. Construí para at-least-once y conseguís escala masiva.

FIFO importa cuando "proceso B ocurre después de proceso A sobre la misma entidad" es un requisito de corrección. Transiciones de máquina de estados, asientos de ledger ordenados, reconciliación cross-system. El cap de throughput rara vez muerde a la escala típica de producto.

Lo que ganás cuando está bien hecho

La confiabilidad del productor deja de depender de la del consumer. El consumer puede deployarse, reiniciarse, escalar, hacer failover, nada de eso aparece para el productor. El productor publica y se va.

Los bursts no te tiran abajo el fleet. La cola absorbe el pico, los workers drenan a su ritmo natural. Pagás por capacidad de steady-state, no por capacidad de pico.

Los retries son infraestructura, no código de aplicación. El consumer que falla por un error transiente recibe el mensaje de nuevo automáticamente. El consumer que falla por un mensaje envenenado deja de recibirlo después de unos intentos.

Los consumers pueden ser cosas distintas. Algunos workers procesan la cola, una herramienta de analytics también puede suscribirse, una herramienta de debug puede samplear mensajes. La cola es una interfaz compartida.

Lo que no te da

Latencia más baja. Una cola suma una ida y vuelta. Si el trabajo es corto y el productor necesita el resultado, sync es más rápido.

Consistencia más fuerte. Las colas son eventuales. Si dos mensajes afectan la misma entidad y el orden importa, Standard no lo preserva. FIFO sí, con límites de throughput. A veces la respuesta correcta es una cola por entidad.

Observabilidad gratis. Queue depth, edad del mensaje más viejo, conteo de DLQ son tus dashboards. Configuralos antes de enviar a producción, no después de que el primer incidente te enseñe que los necesitabas.

La regla del pulgar

Árbol de decisión: si el productor necesita el resultado, sync. Si no y el orden importa, FIFO. Si no, Standard.

Esa es la mayor parte de la decisión. El resto es idempotencia, visibility timeout, DLQ, long polling, aplicados sin excepción cada vez que levantás una cola nueva.

El reflejo de arrancar con una llamada sync existe porque HTTP te lo da gratis. Una vez que la cola entra en la caja de herramientas, la pregunta deja de ser "¿esto debería ser async?" y pasa a ser "¿qué frena a esto de ser async?".