<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ranjith kumar Ramakrishnan</title>
    <description>The latest articles on DEV Community by Ranjith kumar Ramakrishnan (@ranjith-ramakrishnan).</description>
    <link>https://dev.to/ranjith-ramakrishnan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4051974%2F33650b89-60b1-44d5-b1b3-99bff5a91c41.jpg</url>
      <title>DEV Community: Ranjith kumar Ramakrishnan</title>
      <link>https://dev.to/ranjith-ramakrishnan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ranjith-ramakrishnan"/>
    <language>en</language>
    <item>
      <title>AWS Lambda with Spring Boot and SnapStart: Killing the JVM Cold Start</title>
      <dc:creator>Ranjith kumar Ramakrishnan</dc:creator>
      <pubDate>Mon, 03 Aug 2026 19:43:34 +0000</pubDate>
      <link>https://dev.to/ranjith-ramakrishnan/aws-lambda-with-spring-boot-and-snapstart-killing-the-jvm-cold-start-143a</link>
      <guid>https://dev.to/ranjith-ramakrishnan/aws-lambda-with-spring-boot-and-snapstart-killing-the-jvm-cold-start-143a</guid>
      <description>&lt;p&gt;Every Java developer who has tried to put a Spring Boot application on AWS Lambda has met the same wall: the cold start. The first request after a scale-up sits there for two, three, sometimes four seconds while the JVM boots, the classes load, and the Spring context wires itself together. For a batch job nobody notices. For an API a user is waiting on, it is the reason a lot of teams quietly concluded that "Java doesn't belong on Lambda" and either rewrote services in a faster-starting runtime or stayed on containers.&lt;/p&gt;

&lt;p&gt;That conclusion is now out of date. Between &lt;strong&gt;AWS Lambda SnapStart&lt;/strong&gt; and the &lt;strong&gt;CRaC&lt;/strong&gt; (Coordinated Restore at Checkpoint) mechanism it builds on, you can run a real Spring Boot service on Lambda with cold starts in the low hundreds of milliseconds instead of multiple seconds — while keeping your existing Spring codebase, build, and team. This article walks through how it works, wires it up with working code, and covers the handful of gotchas that will bite you if you treat it as a magic switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem, precisely
&lt;/h2&gt;

&lt;p&gt;A JVM cold start is three costs stacked on top of each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Runtime init&lt;/strong&gt; — the JVM process starts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework init&lt;/strong&gt; — Spring Boot builds the application context: component scanning, bean creation, auto-configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First-request cost&lt;/strong&gt; — classes not touched during init get loaded and JIT-compiled on the first real request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a trivial handler, (1) dominates. For a Spring Boot service, (2) is usually the biggest single cost and the one that makes people give up. Building an application context is genuinely expensive work, and doing it on the critical path of a user request is the mistake.&lt;/p&gt;

&lt;p&gt;The insight behind SnapStart is simple: &lt;strong&gt;do that expensive work once, snapshot the initialized process, and restore from the snapshot instead of redoing the work.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What SnapStart actually does
&lt;/h2&gt;

&lt;p&gt;When you enable SnapStart on a Java function, AWS runs your initialization once, ahead of time, and takes a &lt;strong&gt;Firecracker microVM snapshot&lt;/strong&gt; of the fully initialized process — JVM warmed up, Spring context built, beans created. Instead of cold-starting from zero on each new execution environment, Lambda &lt;strong&gt;restores&lt;/strong&gt; that snapshot. The expensive framework init has already happened and is frozen into the snapshot, so it doesn't recur on the request path.&lt;/p&gt;

&lt;p&gt;For a typical Spring Boot function this turns a multi-second cold start into a restore often in the &lt;strong&gt;200–400 ms&lt;/strong&gt; range — the difference between "unusable for interactive APIs" and "fine for the great majority of workloads."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqgkjxvwmmtrftgpg9mc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftqgkjxvwmmtrftgpg9mc.png" alt=" " width="681" height="487"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1: API Gateway → SnapStart-enabled Spring Boot function → DynamoDB / EventBridge. Initialization (JVM + Spring context + priming) happens once and is snapshotted; execution environments restore from the snapshot, and CRaC hooks close stale state before checkpoint and refresh it after restore.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Enabling it
&lt;/h2&gt;

&lt;p&gt;Enabling SnapStart is configuration, not a rewrite. In SAM it is a property on the function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;OrderApiFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;java21&lt;/span&gt;
      &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;com.example.OrderApiHandler::handleRequest&lt;/span&gt;
      &lt;span class="na"&gt;CodeUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;target/order-api.jar&lt;/span&gt;
      &lt;span class="na"&gt;MemorySize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1024&lt;/span&gt;
      &lt;span class="na"&gt;Timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
      &lt;span class="na"&gt;AutoPublishAlias&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;live&lt;/span&gt;          &lt;span class="c1"&gt;# SnapStart requires a version/alias&lt;/span&gt;
      &lt;span class="na"&gt;SnapStart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ApplyOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishedVersions&lt;/span&gt;    &lt;span class="c1"&gt;# valid values: PublishedVersions | None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here. &lt;code&gt;ApplyOn: PublishedVersions&lt;/code&gt; tells Lambda to snapshot the initialized environment when you publish a version. And SnapStart &lt;strong&gt;only&lt;/strong&gt; applies to published versions and aliases that point to them — the &lt;code&gt;$LATEST&lt;/code&gt; version does not use SnapStart at all. The &lt;code&gt;AutoPublishAlias: live&lt;/code&gt; line ensures every deployment publishes a new version and moves the &lt;code&gt;live&lt;/code&gt; alias to it, so you are always invoking a snapshotted version rather than &lt;code&gt;$LATEST&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Activation is where the interesting part &lt;em&gt;begins&lt;/em&gt;, because snapshotting a running process brings two Java-specific problems that you, not AWS, must solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the CRaC dependency
&lt;/h2&gt;

&lt;p&gt;Everything below uses the CRaC API. Add its dependency to your build. Two coordinates are in common use and both expose the same &lt;code&gt;org.crac&lt;/code&gt; interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Common in the Spring / Lambda ecosystem --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.github.crac&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;org-crac&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.1.3&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Alternatively, the coordinate used in AWS's own Lambda docs --&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!--
&amp;lt;dependency&amp;gt;
  &amp;lt;groupId&amp;gt;org.crac&amp;lt;/groupId&amp;gt;
  &amp;lt;artifactId&amp;gt;crac&amp;lt;/artifactId&amp;gt;
  &amp;lt;version&amp;gt;1.4.0&amp;lt;/version&amp;gt;
&amp;lt;/dependency&amp;gt;
--&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recent Spring Boot versions integrate with CRaC and will drive lifecycle callbacks for some managed resources automatically, so check what your Spring version already does before hand-rolling everything below. The pattern shown here is the explicit version, which works regardless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 1: state frozen into the snapshot goes stale
&lt;/h2&gt;

&lt;p&gt;A snapshot captures your process &lt;strong&gt;as it was at checkpoint time&lt;/strong&gt; — including things that must not be reused:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Open connections.&lt;/strong&gt; A DB or HTTP connection open at checkpoint is dead by the time the snapshot is restored, possibly days later. Reusing it fails on the first request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uniqueness and randomness.&lt;/strong&gt; A random seed, generated ID, or cached timestamp captured at checkpoint is &lt;strong&gt;identical across every restored environment&lt;/strong&gt;, because they all restore from the same snapshot. Seed a &lt;code&gt;Random&lt;/code&gt; at startup and every environment now shares the same sequence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CRaC solves this with lifecycle hooks that run &lt;em&gt;around&lt;/em&gt; the snapshot. &lt;code&gt;beforeCheckpoint()&lt;/code&gt; fires just before the snapshot; &lt;code&gt;afterRestore()&lt;/code&gt; fires just after each restore. You close what shouldn't be frozen and re-establish what must be fresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.zaxxer.hikari.HikariConfig&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.zaxxer.hikari.HikariDataSource&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.crac.Context&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.crac.Core&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;org.crac.Resource&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;javax.sql.DataSource&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.security.SecureRandom&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Owns anything unsafe to freeze into a snapshot: the connection pool
 * (closed before checkpoint, reopened after restore) and the RNG
 * (re-seeded after restore so every restored environment differs).
 * Priming of the request path also happens in beforeCheckpoint().
 */&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SnapshotSafeResources&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;volatile&lt;/span&gt; &lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;volatile&lt;/span&gt; &lt;span class="nc"&gt;SecureRandom&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderApiHandler&lt;/span&gt; &lt;span class="n"&gt;handlerForPriming&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SnapshotSafeResources&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderApiHandler&lt;/span&gt; &lt;span class="n"&gt;handlerForPriming&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;handlerForPriming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;handlerForPriming&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dataSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildPool&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;random&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SecureRandom&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;Core&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getGlobalContext&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// hook into the CRaC lifecycle&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;beforeCheckpoint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1) Prime the hot path so its classes are loaded/JIT-warmed into&lt;/span&gt;
        &lt;span class="c1"&gt;//    the snapshot (see Gotcha 2). Best-effort; must not fail init.&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;Priming&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warm&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;handlerForPriming&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RuntimeException&lt;/span&gt; &lt;span class="n"&gt;ignored&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* priming is an optimization */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// 2) Close the pool so no dead connection is frozen into the snapshot.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isClosed&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;close&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;afterRestore&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Context&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;?&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Every restored environment gets a live pool and a fresh RNG seed.&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dataSource&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildPool&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;random&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SecureRandom&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// do NOT reuse a frozen seed&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;DataSource&lt;/span&gt; &lt;span class="nf"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;SecureRandom&lt;/span&gt; &lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;   &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;HikariDataSource&lt;/span&gt; &lt;span class="nf"&gt;buildPool&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;HikariConfig&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;HikariConfig&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setJdbcUrl&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"JDBC_URL"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setUsername&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DB_USER"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPassword&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DB_PASSWORD"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMaximumPoolSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HikariDataSource&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One constraint to respect: &lt;code&gt;beforeCheckpoint()&lt;/code&gt; counts toward the initialization time limit, and &lt;code&gt;afterRestore()&lt;/code&gt; must finish within &lt;strong&gt;10 seconds&lt;/strong&gt; or Lambda throws a &lt;code&gt;SnapStartTimeoutException&lt;/code&gt;. Keep the hooks lean — prime what matters, don't try to warm everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotcha 2: a cold snapshot is still a cold-ish start
&lt;/h2&gt;

&lt;p&gt;SnapStart removes framework-init cost, but a class never loaded during init still loads and JIT-compiles the first time a real request touches it. A snapshot of a context that never served a request is "built but never exercised," and the first restored request pays the exercising cost.&lt;/p&gt;

&lt;p&gt;The fix is &lt;strong&gt;priming&lt;/strong&gt;: run a representative request during initialization, &lt;em&gt;inside &lt;code&gt;beforeCheckpoint()&lt;/code&gt;&lt;/em&gt; (as wired above), so the hot classes are loaded and warmed into the snapshot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Priming: exercise the real request path before the snapshot is taken so
 * its classes are loaded and JIT-warmed. Best-effort — a priming failure
 * must never fail initialization.
 */&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Priming&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;Priming&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;

    &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;warm&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderApiHandler&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;APIGatewayProxyRequestEvent&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;APIGatewayProxyRequestEvent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setHttpMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPath&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setBody&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{\"orderId\":\"PRIMING\",\"amount\":1}"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;handleRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// run the hot path once&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Priming is the single highest-leverage thing you can do to get from "SnapStart works" to "SnapStart is fast." It is also the step most tutorials omit — which is why people sometimes report disappointing numbers. For Spring Boot specifically, driving a full application-context invocation this way (or via an &lt;code&gt;ApplicationRunner&lt;/code&gt; at startup) is what warms the framework's request mapping, not just your own code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together in the handler
&lt;/h2&gt;

&lt;p&gt;The handler wires the pieces: build the CRaC-managed resources at init (which registers the checkpoint hooks and arranges priming), and keep the request method idempotent because SnapStart does nothing for delivery semantics (more on that next).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.Context&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.RequestHandler&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderApiHandler&lt;/span&gt;
        &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RequestHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;APIGatewayProxyRequestEvent&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;APIGatewayProxyResponseEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Static init runs ONCE, before the snapshot is taken. Constructing the&lt;/span&gt;
    &lt;span class="c1"&gt;// resources registers the CRaC hooks and sets up priming for checkpoint.&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderApiHandler&lt;/span&gt; &lt;span class="no"&gt;INSTANCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderApiHandler&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;SnapshotSafeResources&lt;/span&gt; &lt;span class="no"&gt;RESOURCES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SnapshotSafeResources&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;INSTANCE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="no"&gt;ORDERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;RESOURCES&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;APIGatewayProxyResponseEvent&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;APIGatewayProxyRequestEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderJson&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBody&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="c1"&gt;// Idempotent completion: safe to call twice for the same order.&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;confirmationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ORDERS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;APIGatewayProxyResponseEvent&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withStatusCode&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withBody&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{\"confirmationId\":\""&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;confirmationId&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"\"}"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What SnapStart does NOT fix
&lt;/h2&gt;

&lt;p&gt;Being honest about the boundaries is what separates a usable mental model from a fragile one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is not exactly-once anything.&lt;/strong&gt; SnapStart is about startup latency; it does nothing for the correctness of your side effects. If your handler is triggered by an at-least-once source (EventBridge, SQS) and does something with side effects, you still need idempotency — a separate concern. Here is the idempotent completion referenced above, using a conditional write so a duplicate delivery returns the original result instead of acting twice:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;package&lt;/span&gt; &lt;span class="nn"&gt;com.example&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;software.amazon.awssdk.services.dynamodb.DynamoDbClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;software.amazon.awssdk.services.dynamodb.model.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.Map&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.UUID&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DynamoDbClient&lt;/span&gt; &lt;span class="n"&gt;dynamo&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ORDER_TABLE"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SnapshotSafeResources&lt;/span&gt; &lt;span class="n"&gt;resources&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dynamo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DynamoDbClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="cm"&gt;/**
     * Complete an order exactly once. If the same orderId is completed twice
     * (a retry or duplicate delivery), the conditional write fails and we
     * return the ALREADY-stored confirmation rather than creating a new one.
     */&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;confirmationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"CONF-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;dynamo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateItem&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UpdateItemRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OrderId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AttributeValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromS&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateExpression&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SET #s = :done, ConfirmationId = :cid"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;conditionExpression&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"attribute_not_exists(ConfirmationId)"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expressionAttributeNames&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"#s"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Status"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;":done"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AttributeValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromS&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Completed"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                    &lt;span class="s"&gt;":cid"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;  &lt;span class="nc"&gt;AttributeValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromS&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;confirmationId&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;confirmationId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// first time&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ConditionalCheckFailedException&lt;/span&gt; &lt;span class="n"&gt;already&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;GetItemResponse&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamo&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getItem&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;GetItemRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"OrderId"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AttributeValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromS&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;projectionExpression&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ConfirmationId"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;item&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ConfirmationId"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// replay: return stored&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(For at-least-once sources where you would rather not add a dedicated table, I wrote about a lighter Parameter-Store approach to exactly-once processing &lt;a href="https://dev.to/ranjith-ramakrishnan/exactly-once-lambda-processing-on-at-least-once-event-sources-without-adding-a-table-de9"&gt;in a previous article&lt;/a&gt;; SnapStart and idempotency solve orthogonal problems and you often need both.)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It does not shrink a genuinely heavy application.&lt;/strong&gt; If your context builds forty seconds of work, the restore is fast but the checkpoint build still has to happen and counts against init limits. SnapStart rewards lean initialization.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It has restore cost that scales with snapshot size.&lt;/strong&gt; Large snapshots restore more slowly; disciplined dependencies help.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It requires versioned invocation.&lt;/strong&gt; You give up &lt;code&gt;$LATEST&lt;/code&gt; for published versions/aliases — good practice anyway, but a change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Benchmarks — measure your own
&lt;/h2&gt;

&lt;p&gt;Numbers vary enormously with application size, memory, and priming quality, so &lt;strong&gt;measure your own&lt;/strong&gt;. The honest comparison, on the same memory and payload, is three configurations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cold start without SnapStart,&lt;/li&gt;
&lt;li&gt;restore with SnapStart but &lt;strong&gt;no&lt;/strong&gt; priming,&lt;/li&gt;
&lt;li&gt;restore with SnapStart &lt;strong&gt;and&lt;/strong&gt; priming.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice the third is where Spring Boot on Lambda becomes genuinely viable for interactive workloads — but the only number that matters for your decision is the one from your own function. Watch the &lt;code&gt;Init Duration&lt;/code&gt; and first-request durations in the Lambda logs, run each configuration enough times to see the distribution rather than one lucky sample, and decide on data.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for something else
&lt;/h2&gt;

&lt;p&gt;SnapStart makes Spring Boot on Lambda viable; it is not always the right answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;strong&gt;latency-critical, high-volume&lt;/strong&gt; interactive services, provisioned concurrency or a container deployment may edge it out, at higher cost.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;greenfield&lt;/strong&gt; functions, a natively-fast runtime (Quarkus/GraalVM native image) starts in tens of milliseconds without a snapshot, at the cost of a more constrained build.&lt;/li&gt;
&lt;li&gt;For &lt;strong&gt;event-driven, latency-tolerant&lt;/strong&gt; work — most background and integration workloads — SnapStart-enabled Spring Boot is often exactly right: you keep your framework, team, and governance perimeter, and pay almost nothing for the JVM's startup reputation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;SnapStart, with CRaC hooks and priming, retires the oldest objection to Java on Lambda. Hold the mental model precisely: &lt;strong&gt;snapshot the initialized process, restore instead of rebuild, close what goes stale, refresh what must be unique, and prime the request path so the snapshot is warm&lt;/strong&gt; — and keep your side effects idempotent, because SnapStart never promised otherwise. Do that, and a Spring Boot service on Lambda starts fast enough for real users, inside the framework and governance perimeter your team already runs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Ranjith Kumar Ramakrishnan (&lt;a href="https://orcid.org/0009-0007-4924-3264" rel="noopener noreferrer"&gt;ORCID: 0009-0007-4924-3264&lt;/a&gt;), an enterprise systems architect focused on AI-integrated, serverless architectures for public-safety-critical regulatory systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>java</category>
      <category>serverless</category>
      <category>springboot</category>
    </item>
    <item>
      <title>Exactly-Once Lambda Processing on At-Least-Once Event Sources — Without Adding a Table</title>
      <dc:creator>Ranjith kumar Ramakrishnan</dc:creator>
      <pubDate>Tue, 28 Jul 2026 19:58:13 +0000</pubDate>
      <link>https://dev.to/ranjith-ramakrishnan/exactly-once-lambda-processing-on-at-least-once-event-sources-without-adding-a-table-de9</link>
      <guid>https://dev.to/ranjith-ramakrishnan/exactly-once-lambda-processing-on-at-least-once-event-sources-without-adding-a-table-de9</guid>
      <description>&lt;p&gt;Every AWS engineer eventually meets this bug.&lt;/p&gt;

&lt;p&gt;A Lambda function does something with a side effect — it transitions a record's status, sends a notification, posts a ledger entry, expires an authorization. It works perfectly in testing. Then one day, in production, it does that thing &lt;em&gt;twice&lt;/em&gt;. A customer gets two emails. A record gets processed twice. A status flips, then flips again.&lt;/p&gt;

&lt;p&gt;You didn't write a loop. You didn't deploy twice. So what happened?&lt;/p&gt;

&lt;h2&gt;
  
  
  At-least-once is the default, and it will find you
&lt;/h2&gt;

&lt;p&gt;The event source delivered the same logical event more than once. This isn't a rare edge case or a misconfiguration — it's the documented contract of nearly every AWS event source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge&lt;/strong&gt; guarantees at-least-once delivery. Occasional duplicate deliveries are expected behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQS&lt;/strong&gt; standard queues are explicitly at-least-once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled rules&lt;/strong&gt; can fire more than once around a boundary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic retries&lt;/strong&gt; re-invoke your handler after a transient error — even if the first invocation actually succeeded but the response was lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent invocations&lt;/strong&gt; can pick up the same event from two directions at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a handler that's purely a read, none of this matters. For a handler with &lt;strong&gt;side effects&lt;/strong&gt;, every one of these is a duplicate waiting to happen. And in regulated, financial, or safety-relevant systems, a duplicated side effect isn't a cosmetic glitch — it's a doubled notification, a double-processed expiration, a contradictory state change that someone downstream has to untangle.&lt;/p&gt;

&lt;p&gt;The fix is idempotency: make sure the same logical event produces its side effect &lt;strong&gt;exactly once&lt;/strong&gt;, no matter how many times it's delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual answer, and a lighter one
&lt;/h2&gt;

&lt;p&gt;The standard way to add idempotency on AWS is a DynamoDB table with a conditional write and a TTL. It works well and it's the right tool at high throughput. But it also means provisioning a table, managing its capacity, wiring up TTL, and adding a dependency — for what is often a very low-volume concern (lifecycle jobs, scheduled processors, moderate event streams).&lt;/p&gt;

&lt;p&gt;There's a lighter primitive already sitting in every AWS account: &lt;strong&gt;SSM Parameter Store&lt;/strong&gt;. It has strong per-parameter consistency, it needs nothing provisioned, and — crucially — &lt;code&gt;PutParameter&lt;/code&gt; with &lt;code&gt;Overwrite: false&lt;/code&gt; is &lt;strong&gt;atomic&lt;/strong&gt;. That single property is enough to build an idempotency ledger: two racing invocations both try to create the same claim, exactly one wins, and the loser gets &lt;code&gt;ParameterAlreadyExists&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I wrapped this into a tiny, dependency-light library: &lt;a href="https://www.npmjs.com/package/ssm-idempotency-guard" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;ssm-idempotency-guard&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;ssm-idempotency-guard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The whole thing in one call
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createIdempotencyGuard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Outcome&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ssm-idempotency-guard&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Create once per container, reuse across invocations.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createIdempotencyGuard&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// A STABLE, DETERMINISTIC key for the logical unit of work.&lt;/span&gt;
  &lt;span class="c1"&gt;// The same event must always produce the same key.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;detail-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;effectiveDate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;outcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;guard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runExactlyOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ---- your side-effecting work ----&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expireRegistration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expired&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;recordId&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;outcome&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;Outcome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SKIPPED_COMPLETED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;duplicate delivery ignored:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retries, duplicate deliveries, and concurrent invocations for the same key will not run your work twice. That's the entire idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;Under the hood, &lt;code&gt;runExactlyOnce&lt;/code&gt; does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Claim the key.&lt;/strong&gt; It writes a small ledger entry to Parameter Store under a name derived from your key, using &lt;code&gt;Overwrite: false&lt;/code&gt; so the creation is atomic. If the key is already there and marked completed, it returns &lt;code&gt;SKIPPED_COMPLETED&lt;/code&gt; and your work never runs. If another invocation holds a fresh in-flight claim, it returns &lt;code&gt;SKIPPED_IN_FLIGHT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run your work&lt;/strong&gt; — but only if the claim was won.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complete or release.&lt;/strong&gt; On success it marks the key completed so later duplicates are suppressed. On failure it releases the claim, so a legitimate retry can re-process rather than being silently blocked.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two design choices worth calling out, because they're the difference between a toy and something you can put in production:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Crash recovery.&lt;/strong&gt; If an invocation claims a key and then dies before completing, that claim would block the event forever. So claims carry a timestamp and an in-flight TTL (default 15 minutes). After that window, a stale claim can be taken over. Set the TTL comfortably above your worst-case handler duration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure semantics.&lt;/strong&gt; &lt;code&gt;runExactlyOnce&lt;/code&gt; biases toward &lt;em&gt;exactly-once on success&lt;/em&gt; and &lt;em&gt;at-least-once on failure&lt;/em&gt; — it releases the claim if your work throws. If your side effect isn't naturally retry-safe, you can use the lower-level &lt;code&gt;claim&lt;/code&gt; / &lt;code&gt;complete&lt;/code&gt; / &lt;code&gt;release&lt;/code&gt; primitives and mark completion only after the side effect is durably committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a good key
&lt;/h2&gt;

&lt;p&gt;The idempotency key is the whole contract, and it has exactly one rule: it must be &lt;strong&gt;deterministic&lt;/strong&gt; — derivable purely from the event, identical every time the same logical event arrives.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"RegistrationExpiry:REG-4821:2026-03-01"
"InvoicePosted:INV-99123"
"CylinderRequal:RIN-2049:2026-06-15"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Combine the event type, the target record, and whatever discriminator makes each logical occurrence unique. Avoid anything that varies per &lt;em&gt;delivery&lt;/em&gt; — a receipt timestamp, a message ID — because then every duplicate looks brand new and deduplication does nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IAM you'll need
&lt;/h2&gt;

&lt;p&gt;Scoped to your prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ssm:GetParameter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ssm:PutParameter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ssm:DeleteParameter"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ssm:*:*:parameter/idempotency/*"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When to reach for something else
&lt;/h2&gt;

&lt;p&gt;Be honest about the boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Very high throughput&lt;/strong&gt; (thousands of writes/sec against one key space) — Parameter Store has account-level throughput limits; DynamoDB with conditional writes and TTL is the better fit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Already all-in on DynamoDB&lt;/strong&gt; — the same conditional-write pattern applies there; this library exists for the common case where you'd rather not add a table.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For lifecycle automation, scheduled processors, and moderate event-driven workloads — which is a huge share of real serverless systems — the zero-infrastructure version is often exactly right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The package is MIT-licensed and open source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;a href="https://www.npmjs.com/package/ssm-idempotency-guard" rel="noopener noreferrer"&gt;&lt;code&gt;ssm-idempotency-guard&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/ranjith-ramakrishnan/ssm-idempotency-guard" rel="noopener noreferrer"&gt;github.com/ranjith-ramakrishnan/ssm-idempotency-guard&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo includes a runnable Lambda example and a scheduled cleanup sweeper, plus a test suite that proves exactly-once under duplicate delivery and one-executes-one-skips under concurrency — using an in-memory fake, so it runs with no AWS account.&lt;/p&gt;

&lt;p&gt;If you've fought the duplicate-side-effect bug, I'd genuinely like to hear how you handled it — drop a comment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Ranjith Kumar Ramakrishnan (&lt;a href="https://orcid.org/0009-0007-4924-3264" rel="noopener noreferrer"&gt;ORCID: 0009-0007-4924-3264&lt;/a&gt;), an enterprise systems architect focused on AI-integrated, serverless architectures for public-safety-critical regulatory systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambda</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
