Java Serverless on AWS Lambda · Java on Lambda: Runtime Fundamentals

SnapStart, Uniqueness and Priming

How enabling SnapStart made every environment generate the same 'random' IDs, and how to use snapshots safely.

Scenario

Example app: a Java order service enables Lambda SnapStart and cold starts drop from 4 seconds to under 500 ms. The service generates order reference codes with a java.util.Random created in a static field, and opens a database connection during initialisation.

What you see in production

Duplicate-key errors appear: different execution environments generate identical order references. After a few hours, requests in freshly restored environments also fail with stale database connections.

The naive code

public class OrderHandler implements RequestHandler<Map<String, Object>, String> {
    private static final Random RANDOM = new Random();                // seeded once, before the snapshot
    private static final Connection DB = Database.connect();          // captured in the snapshot

    @Override
    public String handleRequest(Map<String, Object> input, Context ctx) {
        String reference = "ORD-" + Integer.toHexString(RANDOM.nextInt());
        save(DB, reference, input);
        return reference;
    }
}

Reproduce it

Three environments are restored from the same snapshot; each takes its random generator's state from it:

Restoring the same snapshot three times
import java.util.*;

public class Main {
    public static void main(String[] args) {
        long snapshotSeed = 20261019L;                    // state captured in the snapshot

        System.out.println("state restored from snapshot:");
        for (int env = 1; env <= 3; env++) {
            Random restored = new Random(snapshotSeed);    // every environment resumes from identical state
            System.out.println("  env " + env + " first reference: ORD-" + Integer.toHexString(restored.nextInt()));
        }

        System.out.println("re-seeded after restore (or SecureRandom / UUID):");
        for (int env = 1; env <= 3; env++) {
            Random afterRestore = new Random(snapshotSeed + env * 7919L);   // stands in for fresh entropy per environment
            System.out.println("  env " + env + " first reference: ORD-" + Integer.toHexString(afterRestore.nextInt()));
        }
    }
}

Output

state restored from snapshot:
  env 1 first reference: ORD-92b01e76
  env 2 first reference: ORD-92b01e76
  env 3 first reference: ORD-92b01e76
re-seeded after restore (or SecureRandom / UUID):
  env 1 first reference: ORD-8a286a13
  env 2 first reference: ORD-c395231d
  env 3 first reference: ORD-18fc3d21

Run this on your computer — see “Get Started” for setup.

Root cause

SnapStart runs your initialisation once when you publish a version, takes a snapshot of the memory and disk state of the initialised environment, and restores new environments from that snapshot. Anything computed during initialisation is therefore identical in every environment: random generator state, generated IDs, cached timestamps, and open network connections (which are dead after restore). Temporary credentials cached at init may also expire.

The fix

  • Do not generate unique values during initialisation; use java.security.SecureRandom or UUID.randomUUID() (SnapStart-aware for Java), or create generators after restore.
  • Use runtime hooks (the CRaC API, org.crac.Core.getGlobalContext().register(resource)) to refresh state: beforeCheckpoint() to close connections, afterRestore() to re-establish them and re-seed.
  • Prime the code path before the snapshot: in beforeCheckpoint(), exercise serialization, class loading and SDK calls (with dummy or read-only requests) so those classes are loaded and initialised in the snapshot.
  • Remember SnapStart works with published versions and aliases, not $LATEST, and is not compatible with provisioned concurrency, Amazon EFS or ephemeral storage larger than 512 MB.
public class OrderHandler implements RequestHandler<Map<String, Object>, String>, Resource {
    private Connection db;

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

    @Override
    public void beforeCheckpoint(org.crac.Context<? extends Resource> context) throws Exception {
        primeSerialization();            // load and initialise hot classes before the snapshot
        if (db != null) db.close();
    }

    @Override
    public void afterRestore(org.crac.Context<? extends Resource> context) throws Exception {
        db = Database.connect();         // fresh connection per restored environment
    }

    @Override
    public String handleRequest(Map<String, Object> input, Context ctx) {
        String reference = "ORD-" + UUID.randomUUID();
        save(db, reference, input);
        return reference;
    }
}

Trade-offs

  • Snapshots are taken at publish time, so deployments take longer.
  • Priming with real calls must not cause side effects; use read-only or dummy operations.
  • For Python and .NET, SnapStart has extra charges for caching and restoring snapshots; for Java managed runtimes it is free.

Detect it in production

  • Restore Duration in REPORT logs instead of Init Duration confirms SnapStart is active.
  • Duplicate unique values or collisions across environments.
  • Connection errors on the first request after restore.

Key points

  • SnapStart restores identical memory into many environments.
  • Generate unique values after restore; refresh connections with CRaC hooks.
  • Prime hot paths before the snapshot; use published versions.

Further reading: Improving startup performance with Lambda SnapStart · Handling uniqueness with Lambda SnapStart

Practice questions

Why can SnapStart cause different execution environments to generate the same 'random' values?

Answer

Initialisation runs once before the snapshot; every environment is restored from the same memory, so a random generator seeded during init resumes from identical state everywhere. Generate unique values after restore or use SecureRandom/UUID.

What are CRaC runtime hooks used for with SnapStart?

Answer

beforeCheckpoint runs before the snapshot (close connections, prime code paths), and afterRestore runs when an environment is restored (re-establish connections, refresh credentials, re-seed generators).

What is priming, and why does it help?

Answer

Exercising the main code paths during initialisation (before the snapshot) so classes are loaded and initialised, and some code is already compiled, in the snapshot; restored environments then serve the first request faster. Priming must avoid side effects.

Open in the app to mark lessons complete, take notes and track your progress.