En un pipeline de CI pequeño, mandar un email de verificación parece una operación barata. El problema aparece cuando cien jobs de Kubernetes arrancan juntos: cada uno crea un buzón efímero, solicita un mensaje, hace polling y reintenta si la entrega tarda. El cluster sigue sano, pero el proveedor de correo recibe una tormenta y la señal llega tarde.
He visto este patrón confundirse con un problema de DNS o de rendimiento del navegador. Muchas veces es simplemente concurrencia sin dueño. Un generador de cuentas de correo temporal o un generador de correo desechable puede servir para probar, pero no decide cuántas pruebas deben ejecutarse a la vez.
El síntoma: CI verde pero lento
La primera señal suele ser engañosa: los jobs terminan correctamente, solo que cada vez tardan mas. Después aparecen respuestas 429, mensajes duplicados y buzones que nunca se limpian porque el runner fue cancelado. El equipo mira el consumo de CPU del nodo, aunque el cuello de botella está fuera del cluster.
Conviene separar tres tiempos:
- Admisión: cuánto tarda el job en obtener permiso para iniciar una prueba de email.
- Entrega: cuánto pasa desde que el proveedor acepta el envío hasta que el mensaje aparece.
- Limpieza: cuánto tiempo vive el buzón después de la aserción.
Si solo medimos el tiempo total del job, no sabemos si Kubernetes está esperando capacidad o si el servicio de correo está lento. En una incidencia, esa diferencia ahorra bastante conversación circular.
Medir la concurrencia antes de tocar Kubernetes
Antes de subir nodos o cambiar requests, añade un identificador de ejecución y cuatro métricas. El identificador debe acompañar al Job, al mensaje y al buzón. Un nombre como ci-email-${RUN_ID} es más útil que compartir una bandeja de staging.
Las métricas mínimas son email_admission_wait_seconds, email_delivery_seconds, email_cleanup_seconds y email_active_jobs. La última debe tener una alerta suave cuando se acerque al límite acordado. Si las señales llega tarde, registra también el motivo de cada espera: cola local, respuesta del proveedor o límite del namespace.
Una prueba de email debería conocer su presupuesto. Por ejemplo, puede aceptar hasta 20 ejecuciones concurrentes y esperar un máximo de 90 segundos para la entrega. Esos números son un punto de partida, no una verdad universal; ajustalos con datos de varias semanas.
Un límite sencillo con cola y semáforo
La forma más fácil de empezar es mantener el Job de aplicación separado del verificador de correo. Un pequeño worker consume una cola y usa un semáforo para limitar las solicitudes externas. Kubernetes conserva el aislamiento y el worker hace visible la presión.
apiVersion: batch/v1 kind: Job metadata: generateName: ci-email-gate- labels: component: email-gate spec: backoffLimit: 0 activeDeadlineSeconds: 240 ttlSecondsAfterFinished: 900 template: metadata: labels: component: email-gate spec: restartPolicy: Never containers: - name: verifier image: example/email-verifier:ci env: - name: MAX_PARALLEL_EMAILS value: "20" - name: DELIVERY_TIMEOUT_SECONDS value: "90" MAX_PARALLEL_EMAILS no debería ser un valor escondido en el código. Déjalo en la configuración del entorno, documenta quién lo puede cambiar y registra el valor al arrancar. Cuando el proveedor empiece a devolver 429, reduce el límite de forma temporal y observa si baja la latencia de entrega.
No mezcles el retry del pod con el retry de lectura del mensaje. Un pod reiniciado puede volver a crear el buzón, mientras que el cliente de correo repite una consulta sobre el mismo buzón. Si ambos reintentan, una sola prueba puede generar cuatro o más solicitudes sin que nadie lo note.
Para el contenido de la prueba, sigue siendo útil probar emails transaccionales sin mezclar bandejas. La misma regla aplica aquí: identidad por ejecución, expiración clara y un propietario de limpieza. Un dummy e mail dejado en una variable o un temp mailid copiado entre jobs no es una identidad operativa.
Qué hacer cuando el proveedor se degrada
Define una política antes del incidente. Si la cola crece pero el proveedor aún acepta mensajes, deja entrar nuevos jobs a una velocidad limitada. Si aparecen 429 o errores de autenticación, pausa la admisión y conserva los jobs en espera durante un periodo corto. Después, falla de forma explícita con la evidencia disponible.
Cada fallo debería indicar una clase:
- Admisión: el semáforo o la cola alcanzó su límite.
- Aplicación: no se produjo la solicitud de envío.
- Proveedor: hubo rechazo, throttling o una entrega fuera de plazo.
- Datos: se leyó un mensaje viejo, el buzón era equivocado o faltó limpieza.
- Cluster: el pod quedó pendiente, reinició o perdió red.
No uses el mismo timeout para las cinco clases. Un pod pendiente requiere revisar recursos; una entrega lenta requiere revisar capacidad externa. El dato no esta completo si el log solo dice email test failed.
También protege los logs. Un mensaje de verificación puede contener un token o un enlace de recuperación. Guarda el ID del mensaje, los tiempos y la clase de fallo, pero redacta el cuerpo. Para la interfaz que muestra el resultado, vale la pena mantener estados de email accesibles, incluso cuando el estado sea “en cola” y no “fallido”.
Preguntas rápidas y checklist de operación
¿Debo limitar por namespace o por proveedor?
Por los dos cuando sea posible. El namespace protege al cluster y el límite por proveedor protege la dependencia externa. Si solo aplicas uno, el otro lado puede seguir saturándose.
¿Una cola elimina la necesidad de retries?
No. La cola controla admisión; el retry trata fallos transitorios. Cada uno debe tener contador, límite y motivo. Si no, el sistema parece estable hasta que una incidencia lo vuelve impredecible.
Antes de cerrar el cambio, revisa:
- Cada Job tiene una identidad de ejecución y un dueño de limpieza.
- La concurrencia máxima está configurada y visible.
- Admisión, entrega y limpieza tienen métricas separadas.
- Los retries del pod y del cliente no se multiplican accidentalmente.
- Los 429 provocan backpressure, no una tormenta adicional.
- Los logs conservan evidencia sin exponer tokens ni cuerpos de mensajes.
- Una cancelación de CI libera el buzón y el permiso del semáforo.
La meta no es que el correo de CI nunca falle. Es que falle de manera acotada, explicable y recuperable. Un límite de concurrencia sencillo suele dar más estabilidad que otro nodo, porque convierte una dependencia externa en una parte visible del contrato SRE.

