<?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>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>
