Logo GCP con Eduardo

GCP con Eduardo

Descarga el código de la lección

DEAD LETTER QUEUE (DLQ) en GCP Pub/Sub: Manejo de Errores y Reintentos
✦ Guía Técnica & Arquitectura

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.

Aislar mensajes tóxicos Evita que un evento permanentemente inválido bloquee el flujo normal.
Controlar reintentos Configura backoff y máximo de intentos para reducir tormentas de tráfico.
Automatizar con Terraform Versiona topics, subscriptions, DLQ e IAM como infraestructura reproducible.
Operar con Python Diferencia errores transitorios de errores permanentes antes de decidir el ACK.

Stack tecnológico

Google Cloud Pub/Sub Dead Letter Topic Terraform Python IAM Cloud Monitoring Event-Driven Architecture Retry Policy

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
Timeout puntual de base de datos La dependencia puede recuperarse. Media Moderado
JSON malformado Reprocesar el mismo payload no lo corrige. Innecesaria Crece con cada intento
Schema incompatible Requiere intervención o corrección de datos. Innecesaria Alto si se reintenta indefinidamente
Error de negocio permanente El mismo evento continuará fallando. Infinita Potencialmente creciente
⚠️ Antipatrón crítico: NACK infinito sin 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.

main.tf Terraform / GCP
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.

consumer.py Python / google-cloud-pubsub
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.

01

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.

02

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.

03

Observabilidad

Monitoriza el volumen de mensajes reenviados, errores del consumidor, edad de los mensajes y tendencia de crecimiento de la DLQ.

04

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.

05

Reprocesamiento controlado

Nunca conviertas la DLQ en una fuente automática de reintentos ilimitados. El replay debe aplicar validaciones, límites y trazabilidad.

06

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

  1. Identifica los tipos de error. Clasifica excepciones como transitorias, permanentes o desconocidas.
  2. Crea el Dead Letter Topic. La DLQ debe ser un destino explícito y gobernado.
  3. Crea una suscripción sobre la DLQ. Un topic sin una suscripción consumidora puede provocar pérdida de mensajes publicados en ese topic.
  4. Configura dead_letter_policy. Empieza con un límite de intentos coherente con el SLA y la naturaleza del workload.
  5. Configura retry_policy. Ajusta minimum_backoff y maximum_backoff para evitar reintentos demasiado agresivos.
  6. 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.
  7. Haz el consumidor idempotente. No diseñes el procesamiento asumiendo que cada mensaje se ejecutará exactamente una vez en términos de efectos externos.
  8. Instrumenta métricas y alertas. Una métrica creciente de mensajes enviados a la DLQ debe disparar una investigación operativa.
  9. 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.
  10. 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.

Sobre el Autor: Eduardo Martínez Agrelo

AI & Data Architect

Eduardo Martínez Agrelo es AI & Data Architect, especializado en arquitectura de datos, inteligencia artificial y sistemas distribuidos. Su enfoque combina profundidad técnica con decisiones arquitectónicas orientadas a producción: escalabilidad, resiliencia, observabilidad, gobierno, FinOps y mantenibilidad.

En contenidos técnicos aborda problemas reales de arquitectura cloud, ingeniería de datos, plataformas de IA y sistemas event-driven, buscando conectar la implementación concreta con los trade-offs que aparecen cuando una solución pasa de laboratorio a producción.

© 2026 Eduardo Martínez Agrelo · AI & Data Architect

Arquitectura cloud · Data Engineering · AI · Sistemas distribuidos