Ivan Vukićević
← Writing

What actually fixed our Lambda cold starts

awslambdaperformance

Cold starts are a JVM problem before they are an AWS problem. A Spring Boot function that takes four seconds to reach its first request is not slow because Lambda is slow — it is slow because it is building an application context inside a request.

Where the time went

Measuring with AWS_LAMBDA_LOG_LEVEL=DEBUG and the INIT_START log line, on a 1024 MB function:

PhaseTimeWhat it is
Runtime init280 msJVM boot
Spring context2,900 msComponent scan, bean graph, auto-config
First handler call340 msLazy JIT, connection setup

The application context dominated. No amount of provisioned concurrency makes that number smaller — it just pays it in advance, continuously, whether or not traffic arrives.

The three changes

Kill the component scan. Explicit @Import of the twelve beans a function needs, rather than scanning a package tree built for a monolith. Context build dropped to 900 ms.

Move initialisation above the handler. Anything constructed in a static block or field initialiser runs during the init phase, which AWS does not bill at the same rate and which runs at full CPU regardless of the memory setting.

public class ShipmentHandler implements RequestHandler<SQSEvent, Void> {
 
    // Built during INIT, not during the first invocation.
    private static final ApplicationContext CONTEXT = buildContext();
    private static final ShipmentService SERVICE = CONTEXT.getBean(ShipmentService.class);
 
    @Override
    public Void handleRequest(SQSEvent event, Context context) {
        event.getRecords().forEach(record -> SERVICE.handle(parse(record)));
        return null;
    }
}

Raise the memory to lower the cost. Lambda scales CPU with memory. Going from 1024 MB to 1792 MB cut init time enough that the per-invocation bill went down — the function was billed for far less wall-clock time.

What we did not do

We never enabled provisioned concurrency. After the three changes, p99 cold start was under a second on a workload that tolerates it fine. Provisioned concurrency is a real tool, but reach for it when you have run out of things to make faster, not before.