El error siempre es el mismo. Tu despliegue está “green” hasta que deja de estarlo, y entonces cada nodo empieza a cantar:
toomanyrequests, 429, pull rate limit exceeded. De repente tu “infraestructura inmutable”
parece muy mutable: se transforma en un montón de pods en Pending y trabajos de CI fallidos.
El throttling del registro ya no es un caso raro en el borde. Es un resultado predecible del comportamiento moderno: runners efímeros, clústeres con autoscaling,
builds en paralelo, imágenes multi-arch y una incapacidad colectiva para dejar las cosas en paz. Arreglémoslo correctamente: diagnostica qué se está limitando,
detén las tormentas de pulls, cachea lo que puedas y haz que tu pipeline vuelva a ser aburrido.
Qué significa realmente “too many requests”
“Too many requests” no es una sola cosa. Es una familia de limitaciones que ocurren en distintas capas, y la solución depende de cuál capa te esté frenando.
La mayoría de los equipos lo tratan como un problema de Docker. Por lo general es un problema de diseño de sistemas con un síntoma con forma de Docker.
Las manifestaciones comunes
- HTTP 429 desde un endpoint del registro: limitación clásica por tasa. Estás excediendo una cuota por IP, por usuario, por token o por organización.
- HTTP 403 con “denied: requested access” que solo ocurre bajo carga: a veces un registro devuelve errores de autenticación engañosos cuando está aplicando límites.
- Kubernetes ImagePullBackOff / ErrImagePull con “toomanyrequests”: el kubelet está haciendo pulls en muchos nodos a la vez. El registro dice “nope”.
- Fallas en CI donde trabajos en paralelo tiran la misma imagen base repetidamente. La imagen está “cacheada” solo en teoría.
- No es throttling, pero parece: fallos de DNS, problemas de MTU, proxies corporativos o interceptación TLS pueden producir tormentas de reintentos que parecen limitación por tasa.
Dónde puede aplicarse el throttling
El throttling puede ocurrir en el registro, en un CDN que haga de front al registro, en tu proxy de salida corporativo o en tu propia puerta NAT.
Incluso podrías estar limitándote a ti mismo: tablas conntrack, agotamiento de puertos efímeros o un mirror local infra-provisionado.
Un modelo mental útil: un “docker pull” no es una sola petición. Es una secuencia de fetch de tokens, requests de manifest y descargas de capas—a menudo muchas capas, a veces para múltiples arquitecturas.
Multiplica eso por 200 trabajos de CI o 500 nodos, y has construido un generador de denial-of-service con YAML.
Una idea parafraseada de John Allspaw (operaciones/reliability): La fiabilidad proviene de diseñar para el fallo, no de esperar que los fallos no ocurran.
Trata el throttling del registro igual: como un modo de fallo conocido alrededor del cual diseñas.
Broma #1: Una pull storm es simplemente la forma en que tu infraestructura dice que extraña los días en que las caídas tenían una única causa raíz.
Guía rápida de diagnóstico (primero/segundo/tercero)
Cuando producción está ardiendo, no tienes tiempo para bailar interpretativo con logs. Aquí está la ruta más rápida hacia el cuello de botella.
Primero: confirma que es limitación real (no red)
- En un nodo/runner afectado, reproduce con un pull único (no todo el despliegue).
- Busca HTTP 429 y cualquier cabecera RateLimit.
- Comprueba si las fallas se correlacionan con IPs de NAT (¿todos los nodos salen por una misma IP? Compartes cuota).
Segundo: identifica el radio del impacto y el patrón de tráfico
- ¿Es una imagen/tag o es todo?
- ¿Son muchos nodos simultáneamente (scale-up del clúster, rolling restart, reemplazo de nodos)?
- ¿Es paralelo en CI (50 jobs que arrancan a la vez) o escritorios de desarrolladores (lunes por la mañana)?
Tercero: elige la clase de mitigación correcta
- Mitigación a corto plazo: reduce la velocidad de pulls (desfase de despliegues), reutiliza nodos, pre-pull, aumenta backoff, reduce concurrencia.
- Mediano plazo: autentica pulls, fija digests, reduce tamaño/capas de imágenes, deja de rebuildar tags idénticos.
- Largo plazo: añade un proxy/mirror de caché, ejecuta un registro privado, replica imágenes críticas y diseña tu CI para reutilizar caches.
Hechos interesantes y contexto histórico (lo que explica el dolor actual)
- El rate limiting de Docker Hub se endureció en 2020, y muchos flujos gratuitos que dependían de pulls anónimos se volvieron frágiles de la noche a la mañana.
- La distribución de imágenes de contenedores toma prestado ideas de Git y repos de paquetes, pero a diferencia de apt/yum, las imágenes son voluminosas y se descargan en paralelo—genial para velocidad, terrible para cuotas.
- Las especificaciones OCI estandarizaron el formato, lo que mejoró la portabilidad pero también hizo fácil que todas las herramientas golpearan los mismos registros de la misma manera.
- Las capas direccionadas por contenido significan que las capas idénticas se reusan entre tags e imágenes—si realmente dejas persistir caches. Los runners efímeros tiran por la borda esa ventaja.
- Los CDN frontan la mayoría de los registros públicos; podrías estar siendo limitado por políticas de borde aunque el origin esté bien.
- Kubernetes normalizó las pull storms: un solo deployment puede disparar cientos de pulls casi simultáneos durante churn de nodos o autoscaling.
- Las imágenes multi-arch incrementaron el número de requests: tu cliente puede pedir un index manifest, luego manifests por arquitectura y después las capas.
- Las puertas NAT concentran identidad: mil nodos detrás de una única IP pública pueden parecer un cliente extremadamente impaciente.
- La auth de registro usa bearer tokens: cada pull puede incluir requests al token service; esos endpoints pueden ser limitados de forma independiente.
Tareas prácticas: comandos, salidas y la decisión que tomas
Estas son las herramientas de campo. Cada tarea incluye un comando, lo que podrías ver y qué decidir después. Ejecútalas en un nodo/runner que esté fallando.
No “arregles” nada hasta haber hecho al menos las primeras cuatro.
Tarea 1: Reproducir un pull único con salida algo verbosa
cr0x@server:~$ docker pull nginx:1.25
1.25: Pulling from library/nginx
no matching manifest for linux/amd64 in the manifest list entries
Significado: Esto no es throttling. Es un desajuste de arquitectura (común en runners ARM o imágenes base raras).
Decisión: Arregla la selección de imagen/tag/plataforma antes de perseguir límites por tasa.
cr0x@server:~$ docker pull redis:7
7: Pulling from library/redis
toomanyrequests: You have reached your pull rate limit. You may increase the limit by authenticating and upgrading
Significado: Esto es limitación real del registro (frase clásica de Docker Hub).
Decisión: Pasa inmediatamente a autenticación + caching/mirroring; ralentizar puede ganar tiempo pero no arregla la clase de problema.
Tarea 2: Identificar tu IP de egress (¿compartes cuota detrás de NAT?)
cr0x@server:~$ curl -s https://ifconfig.me
203.0.113.42
Significado: Esa es la IP pública que el registro ve.
Decisión: Si muchos nodos/runners muestran la misma IP, asume cuota compartida. Planifica un mirror o dividir egress.
Tarea 3: Revisar logs del daemon y del runtime para 429 y churn de auth
cr0x@server:~$ sudo journalctl -u docker --since "30 min ago" | tail -n 30
Jan 03 10:41:22 node-7 dockerd[1432]: Error response from daemon: toomanyrequests: Rate exceeded
Jan 03 10:41:22 node-7 dockerd[1432]: Attempting next endpoint for pull after error: Get "https://registry-1.docker.io/v2/": too many requests
Significado: El daemon está siendo throttled en el endpoint del registro.
Decisión: Sigue midiendo concurrencia de pulls e introduce caching/mirroring.
Tarea 4: Confirmar identidad del registro y cabeceras (429 vs proxy)
cr0x@server:~$ curl -I -s https://registry-1.docker.io/v2/ | head
HTTP/2 401
content-type: application/json
docker-distribution-api-version: registry/2.0
www-authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io"
Significado: Un 401 aquí es normal; prueba que estás alcanzando el registro esperado y el flujo de auth.
Decisión: Si ves cabeceras de proxy corporativo o un servidor inesperado, podrías estar throttled o bloqueado upstream por tu proxy/CDN.
Tarea 5: Inspeccionar cache de imágenes por nodo (¿re-pull porque los nodos son nuevos?)
cr0x@server:~$ docker images --digests | head
REPOSITORY TAG DIGEST IMAGE ID CREATED SIZE
nginx 1.25 sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c 8c3a9d2f1b2c 2 weeks ago 192MB
Significado: El digest indica direccionamiento por contenido; si fijas este digest puedes ser más determinista.
Decisión: Si los nodos no tienen la imagen, necesitas pre-pulls, nodos de vida más larga o un mirror que haga hits de cache locales.
Tarea 6: Revisar eventos de Kubernetes por pull storms y backoff
cr0x@server:~$ kubectl get events -A --sort-by='.lastTimestamp' | tail -n 12
default 8m12s Warning Failed pod/api-7c8d9c6d9c-7lqjv Failed to pull image "nginx:1.25": toomanyrequests: Rate exceeded
default 8m11s Normal BackOff pod/api-7c8d9c6d9c-7lqjv Back-off pulling image "nginx:1.25"
Significado: Kubelet está reintentando repetidamente. Los reintentos aumentan el volumen de requests. El volumen aumenta el throttling. Ves el lazo.
Decisión: Rompe el lazo: pausa rollouts, reduce churn de réplicas y pon un cache delante del registro.
Tarea 7: Cuantificar cuántos nodos están haciendo pull simultáneamente
cr0x@server:~$ kubectl get pods -A -o wide | awk '$4=="ContainerCreating" || $4=="Pending"{print $1,$2,$4,$7}' | head
default api-7c8d9c6d9c-7lqjv ContainerCreating node-12
default api-7c8d9c6d9c-k3q2m ContainerCreating node-14
default api-7c8d9c6d9c-px9z2 ContainerCreating node-15
Significado: Esto es una pull storm en vivo: muchos pods bloqueados en pulls de imagen a la vez en varios nodos.
Decisión: Diferencia el rollout o reduce la escala, luego implementa pre-pulling o un DaemonSet para calentar el cache, además de mirror/caching.
Tarea 8: Validar que imagePullPolicy no te sabotee
cr0x@server:~$ kubectl get deploy api -o jsonpath='{.spec.template.spec.containers[0].imagePullPolicy}{"\n"}'
Always
Significado: Always garantiza un hit al registro incluso si la imagen existe localmente. Eso está bien para hábitos con “latest”; es terrible bajo limitación por tasa.
Decisión: Si usas tags inmutables o digests, cambia a IfNotPresent y fija las imágenes adecuadamente.
Tarea 9: Comprobar si usas “latest” (una forma cortés de decir “no determinista”)
cr0x@server:~$ kubectl get deploy api -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
myorg/api:latest
Significado: No puedes razonar sobre caching cuando los tags flotan. Cada nodo podría necesitar legítimamente un pull fresco al mismo tiempo.
Decisión: Deja de usar :latest en producción. Usa un tag de versión y/o fija por digest.
Tarea 10: Confirmar que containerd es el runtime real (y dónde configurar mirrors)
cr0x@server:~$ kubectl get nodes -o jsonpath='{.items[0].status.nodeInfo.containerRuntimeVersion}{"\n"}'
containerd://1.7.13
Significado: Necesitas configuración de mirrors para containerd, no para el daemon de Docker (incluso si sigues diciendo “docker pull” por costumbre).
Decisión: Configura mirrors de registro en containerd y reinicia con cuidado (drain del nodo, restart, uncordon).
Tarea 11: Inspeccionar config de registry de containerd (común en nodos Kubernetes)
cr0x@server:~$ sudo grep -n "registry" -n /etc/containerd/config.toml | head -n 30
122:[plugins."io.containerd.grpc.v1.cri".registry]
123: config_path = ""
Significado: No se está usando configuración de mirror por registro (o está externa vía config_path).
Decisión: Añade un endpoint mirror para Docker Hub (o el registro correspondiente) mediante la configuración correcta de containerd.
Tarea 12: Validar DNS y TLS rápidamente (similares a limitaciones por tasa)
cr0x@server:~$ getent hosts registry-1.docker.io
2600:1f18:2148:bc02:6d3a:9d22:6d91:9ef2 registry-1.docker.io
54.85.133.21 registry-1.docker.io
Significado: DNS resuelve. Si esto falla intermitentemente, los reintentos del kubelet pueden imitar throttling con impacto operativo similar.
Decisión: Si DNS es inestable, arregla DNS primero. Si no, procede a cuota/caching del registro.
Tarea 13: Buscar presión de conntrack o puertos efímeros durante pull storms (throttling auto-infligido)
cr0x@server:~$ sudo conntrack -S | head
entries 18756
searched 421903
found 13320
new 9321
invalid 12
ignore 0
delete 534
delete_list 24
insert 9321
insert_failed 0
drop 0
early_drop 0
Significado: Si insert_failed o drops suben durante las tormentas, estás perdiendo conexiones localmente, causando reintentos y más carga.
Decisión: Ajusta conntrack, reduce concurrencia o mejora el dimensionamiento del nodo. No culpes al registro hasta tener tu casa en orden.
Tarea 14: Ver si tus runners de CI están cacheando algo
cr0x@server:~$ docker system df
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 3 1 1.2GB 1.1GB (90%)
Containers 1 0 12MB 12MB (100%)
Local Volumes 0 0 0B 0B
Build Cache 0 0 0B 0B
Significado: Tu runner es básicamente amnésico. El build cache es cero; las imágenes son mayormente reclaimable. Eso es receta para pulls repetidos.
Decisión: Usa runners persistentes, cache compartida (BuildKit) o imágenes pre-pobladas. O acepta que necesitas un proxy cache local.
Tarea 15: Fijar por digest y probar un pull (reduce churn de tags y sorpresas)
cr0x@server:~$ docker pull nginx@sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c
sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c: Pulling from library/nginx
Digest: sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c
Status: Image is up to date for nginx@sha256:2f7f7d3f2c0a7a6e1f6b0c1a3bcbf5b0e6c2e0d2a3a2e9a0b1c2d3e4f5a6b7c
Significado: Fijar por digest hace que el caching y los rollouts sean predecibles. Si la imagen existe localmente, el runtime puede evitar descargar capas.
Decisión: Para producción, prefiere pin por digest (o al menos tags de versión inmutables) y alinea la policy de pull acorde.
Soluciones que funcionan (y por qué)
1) Autentica los pulls (sí, incluso para imágenes públicas)
Los pulls anónimos se tratan como un servicio público. Los servicios públicos se miden. Si tu producción depende de pulls anónimos, tu producción depende de “la generosidad de otros”.
Eso no es estrategia; es una vibra.
La autenticación puede aumentar límites y mejora la atribución. También facilita razonar quién está tirando qué.
En Kubernetes, a menudo significa imagePullSecrets. En CI, significa docker login con un token y evitar que los jobs compartan una sola identidad limitada.
2) Deja de tirar lo mismo 500 veces: añade un mirror en caché
Un mirror/proxy cache de registro es la solución adulta. Tu clúster debe tirar de algo que controles (o al menos algo más cercano),
que luego descargue desde el registro público una vez y sirva a muchos.
Opciones incluyen:
- Docker Registry en modo proxy cache: simple, funciona, pero debes operarlo (almacenamiento, HA, backups).
- Harbor proxy cache: más pesado, pero con controles empresariales (proyectos, RBAC, replicación).
- Registros de artefactos de proveedores cloud con caching pull-through o patrones de replicación (varía por proveedor; revisa límites).
El mirror debe estar cerca de tus nodos (misma región/VPC) para reducir latencia y ancho de banda. Debe usar almacenamiento rápido y manejar descargas concurrentes de capas.
Y debe tener suficiente disco. Nada dice “operaciones profesionales” como un cache que expulsa las capas calientes cada hora.
3) Fija por digest y alinea la pull policy con la realidad
Si fijas por digest y mantienes imagePullPolicy: IfNotPresent, obtienes lo mejor de ambos mundos: contenido determinista y menos hits al registro.
Si mantienes tags flotantes y Always, estás eligiendo golpear el registro. Puede ser aceptable en un clúster de dev pequeño. Es temerario a escala.
4) Pre-pulla imágenes deliberadamente (calienta caches)
Si un clúster va a escalar o sabes que un rollout tocará cada nodo, pre-pulla la imagen una vez por nodo antes de enrutar tráfico.
El patrón es anticuado, aburrido y extremadamente efectivo.
En Kubernetes: un DaemonSet que ponga la imagen (y quizá no haga otra cosa) para que los nodos la cacheen. Luego despliega la carga real.
Esto esparce los pulls en el tiempo y hace visibles las fallas antes del rollout.
5) Reduce la concurrencia donde hace daño
Puedes absolutamente reducir cuánto te autorestrictes siendo menos entusiasta. Reduce jobs paralelos en CI que todos tiran la misma imagen base.
Desfase las olas de despliegue. Evita reinicios en todo el clúster durante horario pico a menos que disfrutes la atención.
6) Haz las imágenes más pequeñas y más reusables por capa
El rate limiting trata sobre conteos de requests, pero el ancho de banda y el tiempo aún importan. Imágenes más pequeñas significan pulls más rápidos, menos conexiones concurrentes, menos reintentos y menos tiempo en la zona de peligro.
Además: menos capas pueden reducir requests totales, pero no persigas “una sola capa” a costa de cacheabilidad. El buen diseño de capas sigue siendo una habilidad.
7) Controla la identidad de egress (divide NAT, no concentres todos los pulls)
Si toda tu flota sale por una puerta NAT, has hecho a una sola IP responsable del peor momento del mundo: tu evento de scale-up.
Considera múltiples IPs de egress, egress público por nodo (con cuidado) o conectividad privada a tu registry/mirror cuando sea posible.
Broma #2: Las NAT gateways son como las máquinas de café de la oficina—todos las comparten hasta que el lunes demuestra que fue un error.
Modos de fallo específicos de Kubernetes (porque kubelet nunca tira “un poco”)
Kubernetes convierte “tirar una imagen” en una actividad distribuida y concurrente. Es eficiente cuando el registro es tolerante y los caches están calientes.
Es un desastre cuando tienes churn de nodos, caches fríos y un registro externo con cuotas estrictas.
ImagePullBackOff es un multiplicador
El backoff está pensado para reducir carga, pero en clústeres grandes se convierte en un mecanismo de sincronización: muchos nodos fallan, luego muchos reintentan al mismo tiempo,
especialmente después de un problema de red o cuando el registro se recupera. El resultado es una estampida sincronizada.
El churn de nodos crea caches fríos
Autoscaling, instancias spot y reciclaje agresivo de nodos están bien—hasta que te das cuenta de que constantemente creas máquinas nuevas con caches vacíos.
Si tu registro está throttled, los caches fríos no son una molestia menor; son un desencadenante de outage.
La política de pull y la disciplina de tags importan más de lo que crees
Kubernetes por defecto pone imagePullPolicy a Always cuando usas :latest.
Eso es Kubernetes diciendo educadamente que no uses :latest si te importa la estabilidad. Escúchalo.
containerd vs Docker: configura lo correcto
Muchos equipos todavía “arreglan Docker” en nodos Kubernetes que corren containerd. El arreglo nunca llega porque se aplica a un servicio que no está en la ruta.
Identifica el runtime primero y luego configura su soporte de mirror de registro correctamente.
CI/CD: por qué tus runners son los peores consumidores
Los sistemas CI están optimizados para throughput y descartabilidad. Eso es genial para seguridad y reproducibilidad.
También es genial para tirar repetidamente la misma imagen base hasta que el registro te dice que pares.
Los runners efímeros tiran por la borda las dos mayores ventajas
- Cache de capas: las capas direccionadas por contenido solo ayudan si las conservas.
- Build cache: BuildKit puede evitar re-descargas y re-builds, pero no si cada job arranca desde un disco vacío.
La paralelización no es gratis
Los vendors y equipos de CI aman la paralelización. Los registros no. Si necesitas 50 jobs paralelos, dales un mirror compartido en tu red y autentica.
Si no, solo estás pagando para descubrir límites más rápido.
La disciplina de tags reduce pulls inútiles
Reusar el mismo tag para distinto contenido (“volvimos a sobrescribir dev”) obliga a los clientes a revisar y re-pullar.
Usa tags únicos por build y conserva un tag “humano” que apunte solo como referencia.
Tres mini-historias corporativas desde el frente
Incidente: la suposición equivocada (“las imágenes públicas son básicamente gratis”)
Una compañía SaaS mediana corría un clúster Kubernetes que autoscaleaba agresivamente. El equipo usaba mayormente imágenes base públicas—comunes y conocidas—
y asumía que Internet lo aguantaría. Tenían un registro interno, pero solo para sus propias imágenes. Todo lo demás venía directo del registro público.
Una mañana laboral ocupada, los nodos empezaron a reciclar más rápido de lo habitual debido a una actualización de kernel combinada con churn de instancias spot.
Nuevos nodos se unieron con caches vacíos. Kubelet hizo lo que hace: pull de todo. A la vez. En muchos nodos.
En minutos, los pods se acumularon en ContainerCreating y luego cayeron en ImagePullBackOff.
El on-call vio “too many requests” y asumió que era un blip transitorio. Reiniciaron algunos nodos—creando aún más caches fríos y más pulls.
El gráfico de pulls fallidos pareció una escalera hacia el arrepentimiento.
La causa raíz no fue “Docker fallando”. Fue una suposición equivocada: que la capacidad y cuotas del registro público estaban alineadas con su comportamiento de escalado.
La solución fue simple pero no rápida: autenticar, introducir un proxy cache para el registro público y pre-pullar imágenes críticas durante el aprovisionamiento de nodos.
Después de eso, el churn de nodos siguió siendo molesto, pero dejó de ser desencadenante de outage.
Optimización que salió mal: “purguemos caches para ahorrar disco”
Otra compañía se enorgullecía de tener nodos esbeltos. Ejecutaban un job en cada nodo para hacer prune agresivo de imágenes. Los gráficos de disco eran inmaculados.
Las diapositivas de revisión mensual de infra eran hermosas: “Reducimos almacenamiento desperdiciado en 40%.” Todos asintieron.
Luego introdujeron una estrategia de canary que hizo rollouts frecuentes en la flota. Cada rollout causaba una ola de pulls.
Pero como el job de prune había borrado la mayoría de las capas, cada nodo se comportaba como nuevo.
El registro los rate-limitó intermitentemente. Cuando eso ocurría, los pods fallaban readiness, el canary se detenía, la automatización reintentaba y todo el pipeline se prolongaba.
La parte realmente divertida: el incidente era intermitente, y por eso perfecto para gastar tiempo humano.
La “optimización” ahorró disco y gastó fiabilidad. La solución fue dejar de tratar el cache de imágenes como basura.
Configuraron umbrales sensatos de disco, mantuvieron un baseline de imágenes calientes ancladas en nodos y movieron la limpieza agresiva a ventanas no pico.
El almacenamiento es más barato que el downtime, y también más barato que la gente.
Práctica aburrida pero correcta que salvó el día: “pre-pull y fijar”
Un equipo de servicios financieros ejecutaba cargas reguladas con control de cambios estricto. Su proceso de release era lento, lo que molestaba a desarrolladores,
pero tenía un hábito que pagó dividendos: cada candidate de release estaba fijado por digest y pre-pullado en el clúster antes de cambiar el tráfico.
Una noche, un registro público empezó a throttlear en su región por un evento upstream. Otros equipos entraron en pánico y retrocedieron.
Este equipo casi no notó nada. Sus nodos ya tenían las capas requeridas localmente y el deployment usaba IfNotPresent.
Aún vieron errores en pulls de fondo para imágenes no relacionadas, pero el rollout en producción no se vio afectado.
El informe del on-call fue casi vergonzoso: “Sin impacto a clientes. Observado throttling externo. Continuamos según lo planeado.”
Ese es el sueño: el mundo exterior puede estar en llamas y tu sistema no se inmuta.
La lección no es “sé lento como finanzas.” La lección es: las prácticas aburridas—fijar, pre-pullar, rollouts controlados—crean slack.
El slack es lo que evita que dependencias externas se conviertan en outages.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: “toomanyrequests” solo durante despliegues
Causa raíz: pull storms por rollouts simultáneos, scale-ups o reemplazos de nodos.
Solución: desfasar rollouts, pre-pull vía DaemonSet, añadir un mirror en caché y dejar de usar tags flotantes.
2) Síntoma: funciona en laptops, falla en CI
Causa raíz: las máquinas de desarrollador tienen caches calientes; los runners de CI son efímeros y arrancan fríos cada vez.
Solución: runners persistentes o cache compartido, autenticar pulls, introducir un proxy cache dentro de tu red.
3) Síntoma: errores 403/401 aleatorios bajo carga
Causa raíz: throttling del token service o fallas de auth mal interpretadas por limitación por tasa.
Solución: autentica correctamente, evita compartir tokens entre mucha concurrencia e inspecciona cabeceras/logs para confirmar 429 vs fallos de auth.
4) Síntoma: solo un clúster/región está afectado
Causa raíz: una IP de egress específica está caliente, o un PoP regional del CDN aplica políticas más estrictas.
Solución: divide egress/NAT, despliega un cache/mirror regional o replica imágenes críticas en un registro más cercano.
5) Síntoma: “tenemos un mirror” pero los pulls aún van a Docker Hub
Causa raíz: mirror configurado para el daemon Docker pero los nodos usan containerd; o el hostname del mirror no está confiable; o solo algunos nodos se actualizaron.
Solución: confirma el runtime, configura el mirror en la capa correcta, haz rollout con drain de nodos y prueba con un pull controlado.
6) Síntoma: el throttling empeoró tras cambios de “limpieza”
Causa raíz: el prune agresivo borró capas calientes; cada rollout se volvió cold-start.
Solución: mantiene imágenes baseline, ajusta umbrales de garbage collection y alinea la limpieza con riesgo real (presión de disco), no estética.
7) Síntoma: fallos de pull parecen throttling pero no hay 429
Causa raíz: DNS flaps, MTU/intercepción TLS, resets de proxy, agotamiento de conntrack o saturación de red local.
Solución: verifica DNS/TLS, vigila conntrack/puertos, revisa logs del proxy y reduce pulls concurrentes mientras arreglas fundamentos de red.
Listas de verificación / plan paso a paso
Fase 0: estabilizar producción (hoy)
- Pausa la estampida: detén/ralentiza rollouts; reduce réplicas temporalmente si es seguro.
- Autentica los pulls para los sistemas afectados inmediatamente (CI y nodos del clúster donde sea posible).
- Fija el artefacto de release (tag o digest) para que los reintentos no tiren a un objetivo móvil.
- Reduce concurrencia: paralelismo de jobs CI, maxUnavailable/maxSurge de rollouts, agresividad del autoscaler.
- Escoge un nodo de prueba y verifica una ruta de pull limpia antes de reintentar el rollout.
Fase 1: dejar de depender del comportamiento del registro público (esta semana)
- Despliega un mirror de registro en caché cercano al clúster/runners.
- Configura containerd/Docker para usar el mirror (y confirma que realmente se usa).
- Pre-pulla imágenes críticas en nodos (DaemonSet warm-up o bootstrap de nodos).
- Arregla tags/pull policy: deja de usar
:latest, usaIfNotPresentcon tags/digests inmutables. - Haz visibles las fallas: alerta sobre tasas de ImagePullBackOff y 429s del registro en logs.
Fase 2: volver aburrido y resiliente (este trimestre)
- Replica dependencias: mirror/replica las imágenes de terceros que dependes en tu propio registro.
- Adopta una estrategia de base-images: menos imágenes base, estandarizadas, parcheadas regularmente y almacenadas internamente.
- Planifica capacidad del cache: disco, IOPS, concurrencia y requisitos de HA; trátalo como producción.
- Gobierna concurrencia: políticas de rollout de clúster, controles de concurrencia en CI y guardrails para scale-up.
- Ejecuta game days: simula fallos/throttles del registro; asegúrate de que tu sistema degrade con gracia.
Preguntas frecuentes
1) ¿Es esto solo un problema de Docker Hub?
No. Cualquier registro puede throttlear: registros públicos, registros cloud y tu propio registro detrás de un load balancer.
Docker Hub es solo el lugar más famoso para recibir un 429 y una lección de vida.
2) ¿Por qué la limitación nos pega “aleatoriamente”?
Porque tu tráfico es explosivo. Despliegues, autoscaling y fan-out de CI crean picos.
Las cuotas se aplican a menudo por ventana, por IP o por token; una vez que cruzas la línea, todos los que comparten esa identidad sufren.
3) Si nos autenticamos, ¿ya está resuelto?
La autenticación ayuda, pero no elimina el problema arquitectónico. Aún puedes exceder cuotas autenticadas y puedes saturar tu NAT/proxy.
Usa auth como condición mínima, no como todo el plan.
4) ¿Cuál es la mejor solución a largo plazo?
Un mirror/proxy de caché cercano a tu cómputo. Convierte una “dependencia de Internet” en una “dependencia local”, y las dependencias locales al menos son tu problema para resolver.
5) ¿Debemos fijar por digest en todas partes?
Para despliegues de producción, sí cuando sea factible. Los digests hacen los rollouts deterministas y armonizan bien con IfNotPresent.
Para flujos de desarrollo, tags de versión inmutables pueden bastar, pero latest sigue siendo una trampa.
6) Ya usamos IfNotPresent. ¿Por qué seguimos tirando?
Porque la imagen no está presente en ese nodo (nodo nuevo, cache purgada, arquitectura distinta), o porque el tag apunta a nuevo contenido y el runtime lo comprueba de todos modos.
Verifica presencia local en cache y deja de reutilizar tags para builds distintos.
7) ¿Podemos solo aumentar el backoff de Kubernetes y estar bien?
El backoff reduce la presión inmediata pero no resuelve la demanda subyacente. Además, un backoff sincronizado en muchos nodos puede crear olas de reintentos.
Usa el ajuste de backoff solo como estabilizador mientras implementas caching y políticas.
8) ¿Y en entornos air-gapped o regulados?
Terminarás ejecutando tu propio registro y curando imágenes internamente. La ventaja: no hay throttling externo.
La desventaja: debes encargarte de parcheo, escaneo y disponibilidad. Sigue siendo la decisión correcta para muchos entornos regulados.
9) ¿Cómo sabemos si nuestro mirror se está usando realmente?
Revisa la configuración del runtime en el nodo, luego observa logs/métricas del mirror durante un pull. También compara resoluciones DNS y conexiones salientes:
si los nodos aún hablan directamente con el registro público, tu mirror es ornamental.
10) ¿Podría el cuello de botella ser nuestro almacenamiento?
Sí—especialmente para caches auto-hospedados. Si tu proxy cache está en disco lento, serializará lecturas de capas y hará los pulls lentos,
provocando más intentos concurrentes y más reintentos. El almacenamiento rápido y la concurrencia importan.
Conclusión: próximos pasos prácticos
El throttling de registros no es un accidente fortuito. Es el resultado esperado de infraestructuras elásticas modernas que golpean un servicio externo compartido.
Tu trabajo no es esperar que no vuelva a ocurrir. Tu trabajo es hacerlo irrelevante.
- Hoy: confirma 429 vs problemas de red, detén la pull storm, autentica, fija el artefacto de release.
- Esta semana: despliega un mirror de caché cerca del cómputo, configura el runtime real (containerd/Docker) y pre-pulla imágenes críticas.
- Este trimestre: internaliza dependencias de terceros, estandariza imágenes base y gobierna la concurrencia para que “autoscaling” no signifique “auto-outage”.
Haz que la entrega de imágenes sea aburrida. Aburrido es lo que quieres a las 3 a.m.