Cold start en Cloud Run: cuándo configurar instancias mínimas y cuándo es gastar presupuesto de más
Cloud Run crea y destruye instancias de contenedor según la demanda. Cuando no hay tráfico, una revisión puede escalar a cero: ninguna instancia queda corriendo, y el servicio no cuesta nada mientras nadie lo usa. El problema aparece en el momento inverso, cuando llega una solicitud y no hay ninguna instancia lista para atenderla. Ahí GCP tiene que levantar una nueva: preparar el contenedor, iniciar el proceso dentro de él, y esperar a que ese proceso esté listo para responder. Ese intervalo es el cold start, y el error que vemos repetirse en proyectos nuevos es tratarlo como un problema binario -"tenemos cold starts, hay que eliminarlos"- en vez de preguntar primero si ese tiempo de arranque lo termina pagando un usuario esperando una respuesta, o lo paga un proceso en segundo plano al que nadie le importa si tarda un poco más en arrancar.
Qué controla el tiempo de arranque de una revisión
El tiempo que tarda una instancia nueva en estar lista depende de varios factores que están, en su mayoría, bajo control del equipo que construye el servicio, no de Cloud Run en sí.
El primero es el tamaño y la composición de la imagen del contenedor. Una imagen armada sobre una base pesada, con capas que incluyen herramientas de compilación o dependencias que solo hacían falta para construir el artefacto y nunca se usan en tiempo de ejecución, tarda más en quedar lista que una imagen multi-stage que en su etapa final solo contiene el runtime y los binarios estrictamente necesarios para correr.
El segundo es el runtime del lenguaje y lo que ese runtime necesita hacer antes de poder ejecutar la primera línea de la aplicación. Un binario de Go ya compilado de forma estática arranca el proceso casi de inmediato. Un runtime como la JVM tiene que cargar clases, inicializar el classloader y dejar que el JIT empiece a compilar antes de llegar a un estado de rendimiento estable, lo que añade trabajo de inicialización que un binario compilado no tiene. Node y Python, por su parte, pagan el costo de interpretar y evaluar el código de arranque -incluyendo todos los import y require de nivel superior- antes de que el proceso pueda aceptar una conexión.
El tercero, y el que más veces decide si el arranque es rápido o lento, es lo que el propio código de la aplicación hace antes de marcarse como listo: abrir un pool de conexiones a una base de datos, cargar un modelo en memoria, leer secretos de Secret Manager uno por uno en vez de en una sola llamada, o ejecutar migraciones al arrancar en lugar de como un paso separado del despliegue. Cloud Run además ofrece un modo de refuerzo de CPU durante el arranque, pensado justamente para acortar esa fase inicial en runtimes con más trabajo de inicialización, pero ese refuerzo ayuda con el tiempo de CPU disponible, no con dependencias externas que de por sí son lentas en responder.
Cuándo el cold start importa de verdad
Importa cuando hay un humano esperando esa respuesta en el otro extremo, de forma síncrona, dentro de una ventana de tiempo donde un arranque adicional se nota: un endpoint que atiende una app móvil o una interfaz web, una API consultada directamente desde el navegador antes de poder pintar una pantalla, cualquier flujo donde el usuario ve un spinner mientras espera.
También importa cuando el patrón de tráfico del servicio tiene huecos: picos de uso separados por períodos sin ninguna solicitud lo bastante largos como para que Cloud Run decida escalar a cero entre un pico y el siguiente. En ese patrón, cada pico vuelve a pagar el costo completo de arranque en sus primeras solicitudes, porque no queda ninguna instancia calentada del pico anterior.
Cuándo no importa, aunque exista
No importa en un trabajo disparado por un mensaje de Pub/Sub o una tarea de Cloud Tasks que un worker procesa de forma asíncrona, sin que ningún usuario esté mirando un spinner mientras tanto: ahí el arranque se suma a un tiempo total de procesamiento que de por sí nadie está midiendo contra una expectativa de respuesta inmediata.
Tampoco importa en un job disparado por Cloud Scheduler que corre una vez por hora o una vez al día: el arranque es una fracción pequeña frente a la duración total de un proceso por lotes, y optimizarlo no cambia en ningún sentido práctico cuándo termina el trabajo.
Y no importa, aunque en teoría exista, en un servicio con tráfico sostenido donde la concurrencia configurada y el volumen real de solicitudes mantienen instancias vivas casi todo el tiempo. Ahí el cold start ocurre, como mucho, una vez después de cada despliegue, y el resto del tiempo las solicitudes caen sobre instancias ya calientes.
El costo de fijar instancias mínimas
La forma directa de eliminar el cold start es configurar un número mínimo de instancias que Cloud Run mantiene corriendo siempre, incluso sin tráfico. Eso funciona: una instancia que ya está arriba no necesita arrancar. Pero esa capacidad reservada se paga de forma continua, exista o no una solicitud que la use en un momento dado. Configurar instancias mínimas sin más justificación que "arrancar rápido es mejor" es convertir un costo puntual y ocasional -el arranque de una instancia nueva cuando hace falta- en un costo fijo y permanente, para un beneficio que solo se nota en los primeros milisegundos de las solicitudes que efectivamente hubieran encontrado el servicio escalado a cero.
Lo que se corrigió: instancias mínimas puestas en el servicio equivocado
En un proyecto con varios servicios de Cloud Run detrás de una misma API, uno de ellos atendía directamente a una aplicación móvil y sí tenía sentido mantenerlo con instancias mínimas, porque sus respuestas forman parte de un flujo donde el usuario espera de forma activa. Semanas después, el equipo recibió reportes de que la aplicación se sentía lenta en ciertos momentos del día, y alguien, buscando una solución rápida, subió también el número de instancias mínimas de un servicio interno que procesa eventos desde una suscripción de Pub/Sub, asumiendo que más instancias siempre ayudan contra la lentitud reportada.
Ese cambio no tocó el problema real. El servicio interno nunca tuvo a nadie esperando su respuesta de forma síncrona: procesaba sus mensajes dentro de un tiempo que ya era aceptable para el flujo asíncrono del que formaba parte, con o sin instancias calentadas de antemano. La lentitud que los usuarios reportaban terminó rastreándose a una dependencia externa que el endpoint móvil consultaba de forma síncrona y que, en ciertos momentos, tardaba más de lo habitual en responder, algo que ninguna instancia mínima en ningún servicio de Cloud Run iba a resolver. El resultado de ese cambio fue pagar por capacidad reservada en un servicio donde nadie esperaba esa capacidad en tiempo real, mientras el problema que de verdad afectaba a los usuarios siguió sin tocarse hasta que se identificó la dependencia lenta y se trabajó sobre ella directamente, con reintentos acotados y un timeout más estricto del lado del endpoint móvil.
Qué revisamos antes de decidir si vale la pena actuar sobre un cold start
Antes de tocar la configuración de un servicio por un cold start reportado o sospechado, confirmamos primero que exista de verdad alguien esperando esa respuesta de forma síncrona y dentro de una ventana de tiempo donde un arranque adicional se note; revisamos el patrón real de tráfico del servicio, porque un servicio con tráfico sostenido casi nunca llega a escalar a cero y el cold start deja de ser un problema práctico aunque exista en teoría; separamos la lentitud reportada del cold start como posibles causas distintas, porque confundir ambas lleva a gastar presupuesto en instancias mínimas sin tocar el problema real, como nos pasó en el caso anterior; y, antes de reservar capacidad de forma permanente, revisamos si el arranque se puede acortar en origen -reduciendo el tamaño de la imagen, postergando la carga de dependencias que no son necesarias para la primera solicitud, o agrupando las lecturas de secretos en una sola llamada- porque esa corrección ataca la causa del arranque lento en vez de pagar, mes tras mes, por esconderlo detrás de instancias que nunca se apagan.