Dead Letter Queue (DLQ) en GCP Pub/Sub: Manejo de Errores y Reintentos
Diseña un mecanismo de tolerancia a fallos para Google Cloud Pub/Sub que evite reintentos infinitos, aísle mensajes problemáticos y permita recuperar eventos sin convertir un error puntual en un incidente de disponibilidad o un problema de costes.
Lo que aprenderás
Una DLQ no consiste simplemente en crear otro topic. La arquitectura correcta combina política de reintentos, límites de entrega, IAM, observabilidad y un procedimiento operativo para reprocesar eventos.
Stack tecnológico
Matriz de decisión: ¿reintentar o enviar a DLQ?
El error arquitectónico más habitual es tratar todos los fallos como si fueran transitorios. La decisión correcta depende de la naturaleza del fallo y de la posibilidad real de que un nuevo intento tenga éxito.
| Situación | Comportamiento | Latencia | Coste | Patrón recomendado |
|---|---|---|---|---|
| API externa temporalmente caída | El mensaje puede funcionar posteriormente. | Media / alta | Controlable con backoff | Retry + exponential backoff |
| Timeout puntual de base de datos | La dependencia puede recuperarse. | Media | Moderado | Retry |
| JSON malformado | Reprocesar el mismo payload no lo corrige. | Innecesaria | Crece con cada intento | DLQ |
| Schema incompatible | Requiere intervención o corrección de datos. | Innecesaria | Alto si se reintenta indefinidamente | DLQ + reparación |
| Error de negocio permanente | El mismo evento continuará fallando. | Infinita | Potencialmente creciente | DLQ |
Un consumidor que devuelve NACK ante cualquier excepción puede provocar que Pub/Sub vuelva a entregar continuamente el mismo mensaje. El problema no es únicamente de rendimiento: también puede incrementar el tráfico, saturar consumidores, generar logs innecesarios y retrasar el procesamiento de mensajes sanos.
- Define un límite de entrega: utiliza max_delivery_attempts entre 5 y 100 según la criticidad del flujo.
- Aplica backoff: separa los reintentos mediante minimum_backoff y maximum_backoff.
- Aísla los mensajes: utiliza un Dead Letter Topic asociado a la suscripción.
- Monitoriza la DLQ: una DLQ llena no es una solución; es una señal de que existe un problema que debe diagnosticarse.
Implementación production-ready
La DLQ se configura como una propiedad de la suscripción, no del topic de origen. El siguiente ejemplo crea el flujo completo: topic principal, suscripción, Dead Letter Topic, suscripción de diagnóstico, retry policy y permisos IAM para la cuenta de servicio administrada por Pub/Sub.
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 7.45"
}
}
}
variable "project_id" {
type = string
}
variable "project_number" {
type = string
}
provider "google" {
project = var.project_id
}
# ------------------------------------------------------------
# 1. Topic principal
# ------------------------------------------------------------
resource "google_pubsub_topic" "orders" {
name = "orders"
}
# ------------------------------------------------------------
# 2. Dead Letter Topic
# ------------------------------------------------------------
resource "google_pubsub_topic" "orders_dlq" {
name = "orders-dlq"
}
# ------------------------------------------------------------
# 3. Subscription para inspeccionar la DLQ
# ------------------------------------------------------------
resource "google_pubsub_subscription" "orders_dlq" {
name = "orders-dlq-sub"
topic = google_pubsub_topic.orders_dlq.id
message_retention_duration = "604800s"
}
# ------------------------------------------------------------
# 4. Subscription principal
# ------------------------------------------------------------
resource "google_pubsub_subscription" "orders" {
name = "orders-sub"
topic = google_pubsub_topic.orders.id
ack_deadline_seconds = 30
# Backoff entre entregas consecutivas
retry_policy {
minimum_backoff = "10s"
maximum_backoff = "300s"
}
# Después de aproximadamente N intentos,
# Pub/Sub puede reenviar el mensaje a la DLQ.
dead_letter_policy {
dead_letter_topic = google_pubsub_topic.orders_dlq.id
max_delivery_attempts = 10
}
}
# ------------------------------------------------------------
# 5. IAM: Pub/Sub puede publicar en la DLQ
# ------------------------------------------------------------
resource "google_pubsub_topic_iam_member" "dlq_publisher" {
topic = google_pubsub_topic.orders_dlq.name
role = "roles/pubsub.publisher"
member = "serviceAccount:service-${var.project_number}@gcp-sa-pubsub.iam.gserviceaccount.com"
}
# ------------------------------------------------------------
# 6. IAM: Pub/Sub puede ACKear mensajes reenviados
# ------------------------------------------------------------
resource "google_pubsub_subscription_iam_member" "source_subscriber" {
subscription = google_pubsub_subscription.orders.name
role = "roles/pubsub.subscriber"
member = "serviceAccount:service-${var.project_number}@gcp-sa-pubsub.iam.gserviceaccount.com"
}
Consumidor Python: ACK, NACK y errores
El consumidor debe evitar una política simplista de “NACK ante cualquier excepción”. La DLQ es responsable de aislar mensajes que no pueden procesarse después de los intentos configurados, mientras que el consumidor debe decidir correctamente cuándo una operación puede ser reintentada.
import json
import logging
from google.cloud import pubsub_v1
from google.api_core.exceptions import DeadlineExceeded, ServiceUnavailable
PROJECT_ID = "my-project"
SUBSCRIPTION_ID = "orders-sub"
logging.basicConfig(level=logging.INFO)
def process_order(payload: dict) -> None:
"""
Ejecuta la lógica de negocio.
Los errores transitorios deben propagarse para permitir
que Pub/Sub vuelva a entregar el mensaje.
"""
# Ejemplo de lógica de negocio.
order_id = payload["order_id"]
logging.info("Processing order=%s", order_id)
# Llamada a una dependencia externa...
# external_service.process(payload)
def callback(message: pubsub_v1.subscriber.message.Message) -> None:
try:
payload = json.loads(message.data.decode("utf-8"))
process_order(payload)
# ACK únicamente después de completar
# correctamente la operación.
message.ack()
except (DeadlineExceeded, ServiceUnavailable) as exc:
# Error potencialmente transitorio.
# NACK permite que Pub/Sub vuelva a entregar.
logging.warning(
"Transient error. Retrying message=%s: %s",
message.message_id,
exc,
)
message.nack()
except (json.JSONDecodeError, KeyError, ValueError) as exc:
# Error probablemente permanente.
#
# No debemos mantener indefinidamente un mensaje
# estructuralmente inválido en el flujo normal.
#
# En una arquitectura real puede registrarse el
# contexto y ACKearse si el proceso de remediación
# se realiza fuera de Pub/Sub.
logging.error(
"Invalid message=%s: %s",
message.message_id,
exc,
)
message.ack()
except Exception:
# Error desconocido:
# conservar el mensaje para permitir reintento.
logging.exception(
"Unexpected error for message=%s",
message.message_id,
)
message.nack()
def main() -> None:
subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path(
PROJECT_ID,
SUBSCRIPTION_ID,
)
streaming_pull_future = subscriber.subscribe(
subscription_path,
callback=callback,
)
logging.info(
"Listening on %s",
subscription_path,
)
try:
streaming_pull_future.result()
except KeyboardInterrupt:
streaming_pull_future.cancel()
streaming_pull_future.result()
if __name__ == "__main__":
main()
Patrones de diseño para producción
Una DLQ madura debe formar parte de una estrategia completa de resiliencia y observabilidad, no ser simplemente un mecanismo de almacenamiento de mensajes fallidos.
Retry con backoff
Utiliza reintentos para fallos que tienen una probabilidad razonable de resolverse. Un backoff progresivo reduce la presión sobre las dependencias que están degradadas.
DLQ como aislamiento
Separa eventos que requieren intervención de los mensajes sanos. La DLQ permite investigar, corregir y reprocesar sin bloquear el pipeline principal.
Observabilidad
Monitoriza el volumen de mensajes reenviados, errores del consumidor, edad de los mensajes y tendencia de crecimiento de la DLQ.
Idempotencia
Diseña los consumidores para tolerar entregas duplicadas. ACK, reintentos y fallos de proceso pueden provocar que una operación observable sea ejecutada más de una vez.
Reprocesamiento controlado
Nunca conviertas la DLQ en una fuente automática de reintentos ilimitados. El replay debe aplicar validaciones, límites y trazabilidad.
Gobernanza IAM
Concede únicamente los permisos necesarios a la service account administrada por Pub/Sub: publicar en la DLQ y confirmar mensajes en la suscripción de origen.
Framework de implementación paso a paso
- Identifica los tipos de error. Clasifica excepciones como transitorias, permanentes o desconocidas.
- Crea el Dead Letter Topic. La DLQ debe ser un destino explícito y gobernado.
- Crea una suscripción sobre la DLQ. Un topic sin una suscripción consumidora puede provocar pérdida de mensajes publicados en ese topic.
- Configura dead_letter_policy. Empieza con un límite de intentos coherente con el SLA y la naturaleza del workload.
- Configura retry_policy. Ajusta minimum_backoff y maximum_backoff para evitar reintentos demasiado agresivos.
- Configura IAM. La cuenta de servicio administrada por Pub/Sub debe poder publicar en el Dead Letter Topic y confirmar mensajes en la suscripción de origen.
- Haz el consumidor idempotente. No diseñes el procesamiento asumiendo que cada mensaje se ejecutará exactamente una vez en términos de efectos externos.
- Instrumenta métricas y alertas. Una métrica creciente de mensajes enviados a la DLQ debe disparar una investigación operativa.
- Define un procedimiento de replay. Determina quién puede reprocesar mensajes, cómo se corrigen y cómo se evita introducir de nuevo el mismo error.
- Prueba el failure path. Introduce deliberadamente payloads inválidos y fallos transitorios antes de considerar el sistema preparado para producción.
Preguntas frecuentes
¿Qué es una Dead Letter Queue en Google Cloud Pub/Sub?
En Pub/Sub, el patrón se implementa mediante un Dead Letter Topic asociado a una suscripción. Cuando un mensaje resulta difícil de entregar después de aproximadamente el número de intentos configurado, Pub/Sub puede reenviarlo al topic de mensajes fallidos. Una suscripción separada permite consumir esos mensajes para diagnóstico, recuperación o reprocesamiento.
¿Cuántos reintentos permite configurar una DLQ de Pub/Sub?
La propiedad max_delivery_attempts admite valores entre 5 y 100. Es importante no interpretarlo como una garantía exacta: el dead lettering funciona bajo un modelo best effort, por lo que el número real de entregas puede diferir del valor configurado.
¿Por qué la DLQ necesita permisos IAM específicos?
Para reenviar un mensaje, Pub/Sub necesita poder publicarlo en el Dead Letter Topic y confirmarlo en la suscripción de origen. Estos permisos se conceden a la cuenta de servicio administrada por Pub/Sub. Si la configuración IAM es incorrecta, el mecanismo de dead lettering puede no contabilizar los intentos o reenviar mensajes como se espera.
