Email copiado — support@tuurt.com
Cargando experiencia
aws · 06 de octubre de 2026 · 7 min

SnapStart en AWS Lambda con Java: qué cold starts elimina y cuáles siguen pasando

SnapStart congela el arranque de la JVM en una instantánea y la reutiliza en cada entorno nuevo, pero esa instantánea también congela cualquier estado que debería ser único por ejecución. Vemos qué soluciona de verdad y qué hay que corregir con runtime hooks.

Por Equipo Tuurt

SnapStart en AWS Lambda con Java: qué cold starts elimina y cuáles siguen pasando

Java en Lambda tiene fama bien ganada de cold start lento. La razón no es ningún secreto: antes de que una función Java pueda atender la primera invocación en un entorno de ejecución nuevo, la JVM tiene que arrancar, el classloader tiene que resolver y cargar todas las clases que el código usa, los frameworks que inicializan un contexto completo al arrancar -Spring es el ejemplo más conocido, pero no el único- tienen que terminar de construir ese contexto, y los inicializadores estáticos del propio código de la aplicación tienen que correr de principio a fin. Todo eso pasa antes de que la función reciba el primer evento, y todo eso se repite cada vez que Lambda levanta un entorno de ejecución nuevo.

SnapStart ataca exactamente esa fase. Lo que no siempre queda claro, sobre todo cuando se lee como una promesa de "cold starts resueltos", es qué parte del arranque elimina de verdad y qué parte sigue dependiendo, igual que antes, del código que escribe el equipo.

Qué hace SnapStart en realidad

Cuando se publica una versión de una función con SnapStart activado, Lambda no se limita a guardar el código: ejecuta la fase de inicialización completa -arranque de la JVM, carga de clases, inicialización de frameworks, inicializadores estáticos- una sola vez, en un entorno de ejecución controlado, y toma una instantánea del estado de ese entorno una vez que la inicialización terminó: memoria y estado en disco del microVM de Firecracker sobre el que corre la función, cifrada y cacheada para reutilizarse.

Cuando llega una invocación que necesita un entorno de ejecución nuevo, Lambda no vuelve a ejecutar la fase de inicialización desde cero. En su lugar, restaura ese entorno desde la instantánea cacheada, que arranca su proceso congelado en el punto exacto donde la inicialización había terminado, con la JVM ya arrancada, las clases ya cargadas y los frameworks ya inicializados. Desde ahí, Lambda sigue directamente al handler con el evento de la invocación.

Esto significa que SnapStart no hace que la JVM arranque más rápido: hace que ese arranque no vuelva a ocurrir en cada entorno nuevo. La fase de inicialización corre una vez por versión publicada, no una vez por entorno de ejecución, que es justamente el multiplicador que hace doloroso un cold start de Java bajo tráfico variable.

Lo que SnapStart no toca

El punto que con más frecuencia se pasa por alto es que la instantánea captura un estado, y ese estado se reutiliza tal cual en cada restauración. Cualquier cosa en el código de inicialización que asuma que se ejecuta una sola vez por entorno real -no una vez y luego se clona para todos los entornos futuros- es un candidato directo a comportarse mal.

El ejemplo más citado por AWS mismo es el de material criptográfico o valores que deben ser únicos por ejecución: un número aleatorio generado durante la inicialización y guardado en una variable estática, pensado para usarse como semilla o como identificador, deja de ser único en el momento en que la misma instantánea se restaura para atender a decenas de entornos de ejecución distintos. Todos esos entornos arrancan desde el mismo punto congelado, así que todos comparten exactamente el mismo valor que se generó una sola vez, antes de que la instantánea se tomara.

Lo mismo pasa con conexiones de red abiertas durante la inicialización. Si el código abre una conexión a una base de datos o a cualquier servicio externo como parte del arranque -algo común para dejar un pool de conexiones listo antes de la primera invocación-, esa conexión queda en un estado de socket y, si aplica, de sesión TLS que fue válido en el momento exacto en que se tomó la instantánea, pero que no tiene por qué seguir siendo válido cuando esa instantánea se restaura después, posiblemente minutos u horas más tarde, en un entorno de ejecución distinto. Restaurar una conexión TCP congelada no revive la conexión del otro lado.

El mecanismo para corregirlo: runtime hooks

AWS no deja este problema sin salida: expone una API de hooks, basada en el proyecto CRaC (Coordinated Restore at Checkpoint) de la JVM, con dos puntos de extensión que el código puede registrar explícitamente.

import org.crac.Core;
import org.crac.Resource;

public class DatabaseConnectionManager implements Resource {

    private Connection connection;

    public DatabaseConnectionManager() {
        Core.getGlobalContext().register(this);
    }

    public Connection getConnection() {
        return connection;
    }

    @Override
    public void beforeCheckpoint(org.crac.Context<? extends Resource> context) throws Exception {
        if (connection != null) {
            connection.close();
            connection = null;
        }
    }

    @Override
    public void afterRestore(org.crac.Context<? extends Resource> context) throws Exception {
        connection = DriverManager.getConnection(url, usuario, contrasena);
    }
}

beforeCheckpoint corre justo antes de que Lambda tome la instantánea, y es el lugar para cerrar cualquier recurso que no deba quedar congelado en un estado a medias: la conexión se cierra de forma limpia antes del congelamiento, no después. afterRestore corre inmediatamente después de restaurar cada entorno de ejecución nuevo desde la instantánea, y es el lugar para volver a abrir esa conexión, ahora sí, de forma independiente en cada entorno. El mismo patrón aplica a cualquier valor que deba regenerarse por entorno: un identificador único, una clave derivada, un token de corta duración obtenido de un servicio externo durante la inicialización.

El costo de este mecanismo es que deja de ser automático. SnapStart no adivina qué parte del estado de inicialización es seguro compartir entre entornos y qué parte no lo es: ese análisis lo tiene que hacer el equipo que escribe el código, revisando cada inicializador estático y cada recurso abierto durante el arranque, y decidiendo caso por caso si necesita un hook o si de verdad puede quedar congelado sin problema -una tabla de constantes cargada desde un archivo de configuración, por ejemplo, no necesita ningún hook, porque ese estado es idéntico sin importar cuántas veces se restaure.

Lo que sigue igual después de activar SnapStart

SnapStart elimina el costo de repetir la inicialización en cada entorno nuevo. No toca nada de lo que pasa después de que el handler recibe el evento: el tiempo que tarda la lógica de negocio en ejecutarse, las llamadas a servicios externos que hace cada invocación, las consultas a una base de datos, la serialización de la respuesta. Un handler que hace un trabajo costoso en cada invocación sigue haciendo ese mismo trabajo costoso, instantánea o no.

Tampoco elimina la primera instantánea en sí: la fase de inicialización sigue teniendo que correr completa, al menos una vez, cuando se publica la versión, porque de ahí sale el estado que se va a congelar. Y una inicialización mal diseñada -que carga de más, que abre recursos que nunca se usan, que hace trabajo innecesario antes del primer evento- sigue siendo una inicialización mal diseñada: SnapStart reduce cuántas veces se paga ese costo, no cuánto cuesta cada vez que corre.

Qué revisamos antes de activar SnapStart en una función Java

Antes de dar por cerrada la activación de SnapStart en una función, revisamos cada inicializador estático y cada recurso abierto durante el arranque -conexiones de base de datos, clientes HTTP con estado de sesión, pools de hilos, cualquier valor generado con una fuente de aleatoriedad- para decidir si necesita un hook de beforeCheckpoint y afterRestore o si es seguro que quede congelado en la instantánea; confirmamos que ningún identificador o material criptográfico pensado como único por ejecución se genere únicamente durante la inicialización sin un hook que lo regenere tras la restauración; y separamos, en cualquier medición de latencia que hagamos después, qué parte de la mejora viene de no repetir el arranque y qué parte sigue dependiendo del propio código del handler, porque confundir ambas lleva a asumir que SnapStart resolvió un problema de rendimiento que en realidad sigue viviendo dentro de la lógica de negocio.

aws lambda java serverless
← Volver al blog