Practical Performance Tuning for Serverless Java on AWS

2026-07-2824 min read

Java is one of the most widely used languages in the enterprise, yet it accounts for only a single-digit percentage of AWS Lambda functions. Vadym Kazulkin's thesis is that the gap is caused by two measurable properties — cold start latency and memory footprint — and that both are now tractable enough that a Java shop should not rule out serverless. His talk walks through a baseline measurement of roughly three seconds of cold start, then reduces it to approximately 700 milliseconds using AWS SnapStart plus priming, and finally compares that against a GraalVM native image.

Kazulkin is an AWS Serverless Hero and Head of Development at ip.labs GmbH, where the production system he draws examples from runs about 200 Lambda functions. He presented this 49-minute talk at InfoQ Dev Summit Munich 2025; InfoQ published the recording and transcript on June 15, 2026.

These notes report what Kazulkin presented and measured. Where I add background that an intermediate engineer needs but that the talk assumed, the text says so explicitly.

What You Will Learn

  • What an AWS Lambda cold start is, which phases it contains, and why AWS's definition of "warm start" differs from the Java community's.
  • How AWS SnapStart takes and restores a microVM snapshot, and why the restore is faster than a fresh initialization.
  • What priming is, how to implement it with the org.crac pre-snapshot hook, and why it roughly halves the SnapStart cold start in Kazulkin's benchmark.
  • Which Lambda configuration knobs matter: memory-to-CPU ratio, JIT tiered compilation level, HTTP client choice, CPU architecture, and artifact size.
  • Why SnapStart benchmarks get faster over successive invocations, and how the snapshot cache hierarchy explains that.
  • The concrete limitations of SnapStart: deployment latency, 14-day snapshot expiry, non-idempotent static state, and pricing uncertainty.
  • Where GraalVM ahead-of-time compilation wins, what it costs in build infrastructure and dependency compatibility, and why its future is currently unclear.

Why Java Is Under-Represented on Lambda

Amazon ships its own OpenJDK distribution, Amazon Corretto, and patches it well beyond the usual long-term-support window — Java 8 is supported until 2030. Lambda's managed runtimes cover long-term-support versions only, from Java 8 through Java 21. Kazulkin notes that Java 21 arrived on Lambda about two months after its general release, and at the time of the talk Java 25 had been out for three weeks with Lambda support expected within a month or two.

So the runtime story is healthy. The adoption story is not: Kazulkin cites two published sources putting Java at a single-digit share of Lambda functions, with Python and Node.js dominating. He qualifies both as somewhat dated and expects the current number to be better, while still showing a gap. His explanation is that the two things Java is historically weakest at — process startup time and resident memory — are exactly the two things a function-as-a-service platform bills and measures.

The Reference Application

Everything in the talk is measured against a deliberately small application: an API Gateway managed REST API in front of two Lambda functions, createProduct and getProductById, backed by DynamoDB. Kazulkin's team prefers to stay fully serverless, which is why the datastore is DynamoDB rather than a managed relational database; he returns to the relational option at the end.

When a request arrives through API Gateway, what the function actually receives is a JSON document containing query parameters, path parameters, the request body, and headers. The Lambda runtime deserializes it into an APIGatewayProxyRequestEvent. A handler implements RequestHandler<I, O> with a single handleRequest method, reads the id path parameter, queries DynamoDB, and returns an APIGatewayProxyResponseEvent carrying an HTTP status code and the serialized product.

The baseline configuration is worth recording precisely, because every later number is relative to it:

Setting Value
Memory 1 GB (approximately half a vCPU)
Architecture x86_64
HTTP client Apache (the AWS SDK default)
Deployment size 14 MB
Compilation option Tiered compilation stopped at level 1
Test One hour, ~100 cold starts and ~100,000 warm starts

An important detail: this was a freshly deployed application. Kazulkin later shows that this materially penalizes the SnapStart numbers.

What a Cold Start Actually Is

Lambda implements the function-as-a-service model, and a single execution environment handles exactly one request at a time. There is no in-process concurrency of the kind a Tomcat thread pool provides. Scaling is therefore horizontal by definition: if ten concurrent requests arrive and only five environments exist, AWS starts five more rather than queueing, because it does not want to add latency.

Kazulkin's analogy is a database connection pool. An environment is checked out for a request, returned to the pool, and reused. New environments must be created when the function is invoked for the first time, when new code is deployed (which invalidates every existing environment), or when concurrency rises. Environments are destroyed when concurrency falls — AWS pays for idle capacity even though it does not bill you for it — and when the fleet needs security patching. AWS does not publish the lifetime, but in Kazulkin's experience no environment survives more than several hours. There is no permanently warm pool.

Creating an environment proceeds in four steps. The first three are the cold start.

flowchart TD
  A["Deploy: function artifact uploaded to S3"] --> B["Download code into
Firecracker microVM"] B --> C["Start Java runtime
(Amazon Corretto)"] C --> D["Run static initializer block:
class loading, DI wiring,
annotation processing, JIT"] D --> E["Invoke handler method
(AWS calls this the warm start)"] B -.-> F["Steps 1-3 = cold start"] C -.-> F D -.-> F

Everything reachable from the static initializer block is loaded during step three. In the reference application the static block constructs the ProductDao, which pulls in the DynamoDB client and its transitive initialization. With a framework such as Spring, annotation processing happens here too.

A terminology warning that Kazulkin flags explicitly: AWS calls step four the "warm start", meaning simply the execution of the handler in an already-running environment. Java engineers usually reserve "warm" for the point where the JIT compiler has reached peak performance. Those are different things, and the AWS meaning is used throughout the talk.

The Baseline Numbers

Warm starts are excellent. Ignoring the maximum, getProductById returns in about 7 milliseconds at the 90th percentile — a DynamoDB GetItem plus JSON handling. Kazulkin consistently reads p90 rather than the mean, on the grounds that it describes the experience of the overwhelming majority of requests without being dominated by outliers.

Cold starts are the problem: roughly 3 seconds, with the p90 at about 3.2 seconds and the range extending to 4 seconds. In his one-hour test cold starts were under 0.1% of invocations, which sounds reassuring, but he immediately qualifies it in two ways. First, he only exercised one function; a page that fans out across several functions multiplies the probability that at least one of them is cold. Second, he gives 2% to 3% as the figure he considers normal in practice for a real application.

That framing matters commercially. Kazulkin invokes the widely cited Google finding that users abandon pages that take more than a second, and observes that if 2% to 3% of your logins take three seconds, that is a business impact. He is careful to scope it: for an asynchronous, internal, or event-driven workload, cold starts may be irrelevant. The concern is public-facing synchronous traffic.

AWS SnapStart

SnapStart is AWS's managed answer. It is available for managed runtimes only — Java from version 11 onward, with Python and .NET added at the end of the previous year — and explicitly not for container-image-packaged functions, which is how a GraalVM native image is usually shipped. You cannot combine the two.

Conceptually it resembles checkpoint/restore technology. Kazulkin references CRaC (Coordinated Restore at Checkpoint), the OpenJDK project for restoring a JVM from a checkpoint, alongside the older Linux container checkpoint/restore tooling that has existed for over a decade. AWS built something similar but its own. (The transcript names "CRI-O" here; from context the intended reference is the CRIU-style checkpoint/restore lineage rather than the container runtime of that name.)

SnapStart splits the Lambda lifecycle into two phases:

sequenceDiagram
  participant Dev as Deployment
  participant AWS as AWS SnapStart
  participant Inv as Invocation
  Dev->>AWS: Deploy function with SnapStart enabled
  AWS->>AWS: Start microVM, download code, start JVM
  AWS->>AWS: Run static initializer block
  AWS->>AWS: Run pre-snapshot hook (priming)
  AWS->>AWS: Snapshot entire microVM (OS + JVM + loaded classes)
  AWS->>AWS: Store chunks across three availability zones
  Inv->>AWS: First request arrives
  AWS->>Inv: Restore snapshot, resume execution, run handler

At deployment time AWS starts a container, downloads the code, starts the JVM, and runs the static initialization — everything the cold start would do except invoking your handler, which it cannot do because it has no payload. It then captures a complete snapshot of the microVM: the Linux operating system, the JVM, and every class loaded so far. At invocation time the snapshot is restored and execution resumes.

The bet is simply that restoring a snapshot is faster than re-initializing from scratch. Enabling it is a boolean in your infrastructure-as-code — one extra line — so the cost of testing the hypothesis is near zero.

Priming: Making the Snapshot Contain More

The snapshot only helps for work that happened before it was taken, and the JVM loads classes lazily. In the reference application the static block builds the DynamoDB client, but the code path inside getProductById — constructing a GetItemResponse, running the Jackson ObjectMapper — is not touched until the first real request. That is why the warm start maximum in the baseline was around 1,500 milliseconds despite a 7-millisecond p90: the first invocation in each environment pays for the remaining class loading.

Kazulkin gives two concrete costs that he encourages you to reproduce in an IDE:

  • Initializing the Apache HTTP client once takes roughly half a second. Apache's client does a large amount of instantiation and caching up front, which is sensible for a long-lived Tomcat process and expensive for a function.
  • new ObjectMapper() takes 300 to 500 milliseconds the first time, depending on available CPU, and about 1 millisecond the second time, because of the singletons initialized on first use.

Priming is deliberately triggering that work before the snapshot is taken. The mechanism is the pre-snapshot hook. (There is also a post-restore hook, which Kazulkin does not use.) You add the org.crac dependency, implement its Resource interface, register the handler with Core.getGlobalContext().register(this), and put the warm-up logic in beforeCheckpoint.

His implementation is one meaningful line: call productDao.getProductById("0") and discard the result. That single call forces the Apache HTTP client, the JSON marshaller and unmarshaller, and the whole DynamoDB request/response class chain into the snapshot.

Two properties of SnapStart make this safe and cheap. Network connections established before the snapshot — the HTTPS connection to DynamoDB, or a JDBC connection to Postgres — are restored for you without any code on your part. And the priming call happens at deployment time, not on the request path.

Kazulkin is honest about the limits of the technique. It requires domain knowledge of what your dependencies do lazily. If your function computes A + B with no I/O, there is nothing to prime — and equally, you probably do not have a cold start problem. AWS publishes a SnapStart priming guide describing the common patterns; the general rule is to preload as many lazily loaded classes as you can that are not already reachable from the static initializer.

Measured effect

Reading the p90 in each case:

Configuration Cold start p90
Baseline (no SnapStart) ~3.2 s
SnapStart, no priming ~2 s
SnapStart with priming ~1 s

Enabling the checkbox alone bought roughly 40%. Priming approximately halved what was left. Kazulkin's framing is that the first improvement costs one line of configuration and the second costs one line of code, which is an unusually good return for a latency optimization.

Priming also flattened the warm-start maximum, for the reason described above. He points out that this particular benefit does not require SnapStart at all — you could make the same fake call from the static initializer block and get the same warm-start improvement. SnapStart is what converts it into a cold-start improvement.

Configuration Knobs Worth Testing

Kazulkin presents these as things to measure rather than universal settings.

Memory, and therefore CPU. On Lambda you configure memory only; CPU is allocated proportionally. Less memory is cheaper but slower. His specific guidance is that increasing memory buys more CPU up to about 1.8 GB, at which point a second core appears — and if your application cannot use a second core, paying past that point buys nothing.

Tiered compilation. The baseline used tiered compilation stopped at level 1. The reasoning: by default HotSpot's tiered compilation waits for a method to be invoked on the order of 10,000 times before applying aggressive JIT optimization. A short-lived Lambda environment will usually be destroyed before that threshold is reached, so you pay for JIT compiler threads consuming CPU without receiving the optimized code. Capping at level 1 frees that CPU for useful work, which measured better.

HTTP client. The AWS SDK offers alternatives to the Apache client: an AWS native client and a plain URL-connection-based client, both lighter to initialize. Kazulkin's experience is that once you are priming properly the choice stops mattering much, because everything is preloaded either way.

Architecture. ARM (Graviton) is cheaper per unit of compute, but in his tests it produced larger cold starts. He presents this as a price/performance decision rather than a clear win, and as something to measure for your own workload.

Artifact Size Is a First-Class Latency Concern

Kazulkin measured three artifacts:

Artifact Size
Hello World, no dependencies 130 KB
Reference application 14 MB
Application with tracing and more 50 MB

Cold start rises with size in every configuration, including SnapStart with priming — a larger snapshot takes longer to capture, store, and restore, and the code itself takes longer to fetch from S3.

His warning is aimed at habits carried over from long-lived servers: in a Tomcat deployment that starts once and runs for weeks, an extra dependency costs nothing perceptible, so teams add them freely. In serverless every dependency is paid for on every cold start. Check that test-scope dependencies are not being packaged, and include only what the function actually needs.

He adds a trend observation from his own benchmarking: AWS SDK dependency sizes have grown across recent versions, and moving from Spring Boot 3.2 to 3.4 increased his artifact size by about 10% with no other change. Size regression is something to watch during routine upgrades.

The Snapshot Cache, and Why Your Benchmark Warms Up

This section explains a benchmarking trap that would otherwise make SnapStart look worse than it is.

The snapshot is stored across three availability zones in what Kazulkin describes as a tiered low-latency cache. The snapshot is split into 512 KB chunks. Those chunks live at different distances from the execution environment: for frequently invoked functions, on the instance where the Lambda runs; for less frequently invoked ones, in a shared cache fleet further away; and ultimately in S3 as the source of truth. He compares it directly to a CPU cache hierarchy — L1, L3, RAM — where proximity determines latency. None of this is configurable, and AWS optimizes it for its own storage costs as well as your latency. AWS additionally traces execution paths, so that if your handler branches on the payload it can asynchronously preload the chunks your traffic actually needs rather than the whole snapshot.

The consequence is that immediately after a deployment the cache is cold, and performance improves over the first invocations as chunks are placed and rearranged. Kazulkin re-analyzed his SnapStart-with-priming run by discarding the first 30 of the 100 measured cold starts:

Sample Cold start p90
All 100 executions, primed ~1.2 s
Last 70 executions, primed ~650–700 ms

He quotes both 650 and 700 milliseconds for the trimmed sample at different points; the honest reading is "roughly 700 milliseconds". Whether the trimmed figure is the fairer one depends on your deployment cadence. If a function is deployed and then serves traffic for weeks — accumulating tens of thousands of cold starts — the 30 slower ones after each deploy are statistically irrelevant, and the trimmed number better describes production. His practical instruction is to keep measuring past the first handful of invocations, because stopping early systematically understates SnapStart.

For calibration, he puts Node.js and Python cold starts at roughly 300 to 400 milliseconds. Java at 700 milliseconds is not equal, but it is a different conversation from three seconds.

For a deeper treatment of Lambda internals and the snapshot cache, he recommends Mike Danilov's InfoQ San Francisco talk "AWS Lambda Under the Hood".

Profiling to Find More Priming Opportunities

AWS released a Lambda profiler extension for Java, built on the open-source async-profiler, which produces flame graphs of what happens inside an invocation. Kazulkin used it to answer a question he could not previously attack: the deserialization of the API Gateway payload into APIGatewayProxyRequestEvent happens in AWS's code before his handler runs, so it seemed out of reach.

The flame graph showed him what executed before his handler, and revealed that one AWS serializer entry point (serializeFor) was public and therefore callable. He constructed a minimal fake API Gateway request — a GET with a single id=0 parameter — and ran it through the serializer inside beforeCheckpoint. That produced a further 20% cold start reduction, and he mentions reaching 25% in a later iteration.

He is explicit that this is a lot of code for the benefit, that the same technique extends to POST handlers by faking a body, and that whether it is worth it is a judgement call. The transferable lesson is the method: profile the initialization path, find the expensive lazily initialized work, and see whether any of it is reachable from a pre-snapshot hook.

Architecture And Data Flow

The following diagram summarizes where each optimization acts.

flowchart LR
  Client["Client"] --> APIGW["API Gateway"]
  APIGW --> LambdaEnv["Lambda execution environment
(one request at a time)"] LambdaEnv --> DDB["DynamoDB"] subgraph Deploy["Deployment time"] Artifact["Artifact in S3
(size drives cold start)"] Snap["SnapStart snapshot
+ priming hook"] Artifact --> Snap Snap --> Cache["Chunked cache
instance / fleet / S3"] end Cache -->|restore| LambdaEnv Artifact -->|cold start path
without SnapStart| LambdaEnv

The two paths into the execution environment are the point. Without SnapStart, every new environment walks the full download-start-initialize sequence. With SnapStart, it restores a pre-initialized image whose contents you control through the priming hook and whose retrieval latency is governed by a cache you cannot configure but which warms with use.

GraalVM Native Image

The alternative attacks the problem at compile time. GraalVM performs ahead-of-time compilation under a closed-world assumption: it computes everything reachable — classes, methods, fields — and produces a native executable containing only that. The resulting binary is smaller, needs less memory because there is no JIT compiler, and starts nearly instantly. Those three properties map directly onto what serverless charges for, so AOT is a natural fit.

AWS does not offer a managed GraalVM runtime, but Lambda supports custom runtimes. You ship a ZIP named function.zip containing a bootstrap file that is the native image. (Note that this packaging path is why SnapStart and GraalVM are mutually exclusive: SnapStart supports managed runtimes only.)

Kazulkin's measurements used GraalVM 23, with GraalVM 25 testing underway. Against SnapStart with priming, the native image showed lower cold starts — and he flags fairly that the GraalVM numbers are for all 100 executions while the best SnapStart figures came from the trimmed sample. Beyond the p90, GraalVM gave more predictable cold starts, better warm starts, and notably better tail values at p99.9 and maximum. It is, in his words, a valid and powerful technology.

The costs are real:

Dependency compatibility. Because the world is closed, anything resolved reflectively at runtime must be declared in advance or you get a ClassNotFoundException in production. GraalVM publishes a compatibility page covering frameworks — Quarkus, Helidon, Spring, Micronaut are all supported — but your dependencies' dependencies also have to be ready. Kazulkin singles out logging as especially painful: he had to hand-declare SLF4J metadata, and he gave up on Log4j after three hours before version 2.25.0 shipped GraalVM support several months ago.

Configuration generation. The GraalVM tracing agent can generate the required metadata by observing a test run and recording what is actually loaded. This works well only to the extent your tests exercise every code path; untested paths silently produce missing entries.

Build infrastructure is yours. This is Kazulkin's main reason for preferring SnapStart as a default. AWS manages snapshot creation, storage, and restore. With GraalVM you own the pipeline. In his setup, building one native image required a Lambda function with 6 GB of memory and took up to 3 minutes — comparable to SnapStart's snapshot creation time, but on your bill and in your maintenance scope.

Upgrade fragility. A routine dependency bump can pull in something that is not GraalVM-ready and break the build or, worse, the runtime.

The Project Leyden Complication

Kazulkin's recommendation would ordinarily be straightforward: start with SnapStart because it is fully managed and accept slightly higher cold and warm starts; choose GraalVM if every millisecond matters and you are willing to do more work.

He qualifies that with "at least it was my suggestion until September 17". On that date — the day Java 25 was released — GraalVM announced that it is detaching from the Java ecosystem release train and focusing on non-Java GraalVM languages rather than Native Image, pointing users toward Project Leyden.

Project Leyden is an OpenJDK effort, shipping with Java 25, that aims to improve startup time, time to peak performance, and footprint using an AOT cache and class-data-sharing cache. Kazulkin's assessment is that it is not equivalent to a GraalVM native image and delivers worse numbers, and that the community — himself included — does not yet understand what the announcement means for Native Image's future support. He argues that deprecating something of this importance requires far more advance notice than was given. Treat this as an open question at the time of the talk rather than a settled outcome.

Trade-offs And Limitations

SnapStart adds deployment latency. Taking and securing a snapshot across availability zones costs roughly two to two and a half minutes per function. Snapshots for multiple functions are taken in parallel, so deploying ten functions does not cost ten times as long — but every deployment pays the penalty. Kazulkin suggests you may not want SnapStart enabled in a staging environment where fast iteration matters more than cold start.

Snapshots expire after 14 days without invocation. AWS deletes them to save storage. He expects this restriction to be lifted for Java if and when AWS starts charging for it.

Pricing may change. SnapStart shipped free for Java. When AWS added Python and .NET support it introduced charges for snapshot cache storage and for the restore phase. Java remains free at the time of the talk, which Kazulkin attributes to reluctance to antagonize Java developers who have had it for free — but he notes AWS observed teams enabling it on every staging function and never invoking them, which is pure cost to AWS. Plan for the possibility that Java SnapStart becomes billable.

Snapshotted state must be idempotent across restores. This is the sharpest correctness hazard. Anything captured in the snapshot is frozen at snapshot time and then replicated into every restored environment. Two concrete anti-patterns Kazulkin calls out:

  • Calling System.currentTimeMillis() in a static initializer. Every restored environment will believe it started at the snapshot moment.
  • Time-based caches. If you populate a cache at initialization with an eight-hour TTL, you have no idea when the snapshot will be restored — it may be restored long after the entries would logically have expired, or before. His advice is not to rely on such caches, and to design a mitigation if you must have one.

The same reasoning extends to anything that must be unique per environment, such as seeded randomness or identifiers generated once at startup.

SnapStart does not improve steady-state execution. It targets cold start. The warm-start improvement observed in the benchmark came from priming, not from snapshotting, and could be obtained without SnapStart.

Cold start relevance is workload-dependent. For asynchronous or internal workloads a three-second cold start on 2% of invocations may be entirely acceptable. The optimization effort is justified by user-facing synchronous latency, not by principle.

Benchmarks are perishable. Kazulkin measured on Java 21. AWS updates runtime minor versions and Firecracker independently, SnapStart itself may improve, and dependency sizes drift upward. He publishes his benchmark code on GitHub specifically so readers can re-measure rather than trust his numbers.

Powertools for Java is convenient and large. The library provides helpers for idempotency, logging, and tracing, but it is heavy, and given the size-versus-cold-start relationship you should measure its impact rather than adopt it reflexively.

Relational databases add their own problem. Kazulkin's team chose DynamoDB partly to avoid connection management: a Lambda function scaling out can exhaust a relational database's connection pool. He notes that Aurora DSQL, released several months before the talk, is Postgres-compatible (though not 100% compatible) and designed for serverless access patterns, which addresses that specific issue. He had not yet published measurements for it.

Practical Takeaways

  • Measure your own baseline before optimizing, and report percentiles rather than means. Kazulkin reads p90 consistently and treats maximum values separately, because the maximum in a warm-start distribution usually reveals lazy class loading rather than a latency problem.
  • Enable SnapStart first. It is one line of infrastructure-as-code and bought roughly 40% in the reference benchmark, with no code changes and no correctness risk beyond the idempotency hazards above.
  • Then add priming. Implement org.crac's Resource, register the handler in the global context, and in beforeCheckpoint call your own data access path with a throwaway argument. In the reference application one such call halved the remaining cold start by forcing the HTTP client, Jackson, and the SDK response classes into the snapshot.
  • Do not stop your benchmark after a handful of invocations. The snapshot cache fills over the first tens of executions; trimming the first 30 of 100 runs moved the primed p90 from about 1.2 seconds to roughly 700 milliseconds.
  • Treat artifact size as a latency budget. Audit for test-scope dependencies leaking into the package, and re-measure size after framework and SDK upgrades, which trend upward.
  • Set -XX:TieredStopAtLevel=1 and reason about it: a short-lived environment will not survive to the ~10,000 invocations that full tiered compilation optimizes for, so the JIT threads are pure CPU overhead.
  • Tune memory empirically. More memory means more CPU up to about 1.8 GB, where a second core appears; past that, extra memory only helps if your code is actually parallel.
  • Test ARM rather than assuming it. It is cheaper, but produced larger cold starts in Kazulkin's measurements.
  • Use the AWS Lambda profiler extension when the obvious priming is exhausted. The flame graph shows what runs before your handler, which is where the remaining opportunities are — including AWS's own payload deserialization.
  • Audit static initialization for anything time-dependent or required to be unique per environment before enabling SnapStart.
  • Choose GraalVM only when the extra ~300 milliseconds and the tail-latency difference genuinely matter to you, and only if you are prepared to own a native-image build pipeline and chase reachability metadata. Factor in the current uncertainty about Native Image's long-term support.

Key Terms

Amazon Corretto — Amazon's no-cost, production-ready distribution of OpenJDK, used by Lambda's managed Java runtimes and patched beyond the usual support windows.

AOT (ahead-of-time) compilation — Compiling to native machine code at build time rather than at run time, eliminating JIT warm-up at the cost of runtime flexibility.

Closed-world assumption — GraalVM's requirement that all reachable code be known at build time, which is why runtime reflection needs explicit configuration.

Cold start — On Lambda, the code download, runtime startup, and static initialization performed before a new execution environment can run your handler.

CRaC (Coordinated Restore at Checkpoint) — An OpenJDK project for checkpointing and restoring a running JVM. Lambda's Java SnapStart integration uses the org.crac API surface for its hooks.

Firecracker — The lightweight virtual machine monitor AWS uses to isolate Lambda execution environments.

FaaS (Function as a Service) — An execution model in which the platform runs individual functions on demand; on Lambda each environment serves exactly one request at a time.

Priming — Deliberately executing initialization work in a pre-snapshot hook so that lazily loaded classes and initialized clients are captured in the SnapStart snapshot.

Project Leyden — An OpenJDK project shipping in Java 25 that improves startup, time to peak performance, and footprint via AOT and class-data-sharing caches. Less aggressive than a GraalVM native image.

Reachability metadata — Declarations that tell GraalVM about classes used only via reflection or dynamic loading, so they are included in the native image.

SnapStart — AWS Lambda's managed checkpoint/restore feature, which snapshots an initialized microVM at deployment time and restores it at invocation time.

Tiered compilation — HotSpot's staged JIT strategy, where hot methods are progressively recompiled with more optimization. Level 1 is the fastest-compiling tier with the least optimization.

Warm start (AWS usage) — The execution of the handler in an environment that already exists. Distinct from the Java community's use of "warm" to mean JIT-optimized peak performance.


Reference: Practical Performance Tuning for Serverless Java on AWS