<?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>Forem: Dima</title>
    <description>The latest articles on Forem by Dima (@dzima).</description>
    <link>https://forem.com/dzima</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3765207%2Fe28bfa07-4193-45d6-bfca-02586848671e.PNG</url>
      <title>Forem: Dima</title>
      <link>https://forem.com/dzima</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/dzima"/>
    <language>en</language>
    <item>
      <title>Idempotent APIs in Node.js with Redis</title>
      <dc:creator>Dima</dc:creator>
      <pubDate>Wed, 11 Feb 2026 01:28:00 +0000</pubDate>
      <link>https://forem.com/dzima/idempotent-apis-in-nodejs-with-redis-5081</link>
      <guid>https://forem.com/dzima/idempotent-apis-in-nodejs-with-redis-5081</guid>
      <description>&lt;p&gt;Distributed systems lie.&lt;/p&gt;

&lt;p&gt;Requests get retried. Webhooks arrive twice. Clients timeout and try again.&lt;br&gt;
What should be a single operation suddenly runs multiple times — and now you’ve double-charged a customer or processed the same event five times.&lt;/p&gt;

&lt;p&gt;Idempotency is the fix.&lt;br&gt;
Doing it &lt;em&gt;correctly&lt;/em&gt; is the hard part.&lt;/p&gt;

&lt;p&gt;This post shows how to implement &lt;strong&gt;idempotent APIs in Node.js using Redis&lt;/strong&gt;, and how the &lt;code&gt;idempotency-redis&lt;/code&gt; package helps handle retries, payments, and webhooks safely.&lt;/p&gt;


&lt;h2&gt;
  
  
  What idempotency means for APIs
&lt;/h2&gt;

&lt;p&gt;An API operation is &lt;em&gt;idempotent&lt;/em&gt; if:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Multiple calls with the same idempotency key produce the same result — and side effects happen only once.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One execution per idempotency key&lt;/li&gt;
&lt;li&gt;Concurrent or retried requests replay the same result&lt;/li&gt;
&lt;li&gt;Failures can be replayed too&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💳 Payments&lt;/li&gt;
&lt;li&gt;🔁 Automatic retries&lt;/li&gt;
&lt;li&gt;🔔 Webhooks&lt;/li&gt;
&lt;li&gt;🧵 Concurrent requests&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Why naive solutions fail
&lt;/h2&gt;

&lt;p&gt;Common approaches break down quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In-memory locks → don’t work across instances&lt;/li&gt;
&lt;li&gt;Database uniqueness → hard to replay results&lt;/li&gt;
&lt;li&gt;Redis &lt;code&gt;SETNX&lt;/code&gt; → no result or error replay&lt;/li&gt;
&lt;li&gt;Returning &lt;code&gt;409 Conflict&lt;/code&gt; → pushes complexity to clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What you actually need is &lt;strong&gt;coordination + caching + replay&lt;/strong&gt;, shared across all nodes.&lt;/p&gt;


&lt;h2&gt;
  
  
  Using &lt;code&gt;idempotency-redis&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;idempotency-redis&lt;/code&gt; provides idempotent execution backed by Redis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One request executes the action&lt;/li&gt;
&lt;li&gt;Others wait and replay the cached result&lt;/li&gt;
&lt;li&gt;Errors are cached and replayed by default&lt;/li&gt;
&lt;li&gt;Works across multiple Node.js instances&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Basic example
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redis&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ioredis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;IdempotentExecutor&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="s1"&gt;idempotency-redis&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redis&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;Redis&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;executor&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;IdempotentExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;payment-123&lt;/span&gt;&lt;span class="dl"&gt;'&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;chargeCustomer&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;Call this five times concurrently with the same key — the function runs &lt;strong&gt;once&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  Real-world use cases
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Payments
&lt;/h3&gt;

&lt;p&gt;Payment providers and clients retry aggressively.&lt;br&gt;
Your API must never double-charge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`payment:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;paymentId&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="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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;charge&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;stripe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;charges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;saveToDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;charge&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;charge&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;If the response is lost, retries replay the cached result — no second charge.&lt;/p&gt;




&lt;h3&gt;
  
  
  Webhooks
&lt;/h3&gt;

&lt;p&gt;Webhook providers explicitly say &lt;em&gt;“events may be delivered more than once.”&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`webhook:&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;id&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="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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;processWebhook&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Duplicate delivery? Same result. One execution.&lt;/p&gt;




&lt;h3&gt;
  
  
  Retries without fear
&lt;/h3&gt;

&lt;p&gt;With idempotency in place, you can safely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable HTTP retries&lt;/li&gt;
&lt;li&gt;Retry background jobs&lt;/li&gt;
&lt;li&gt;Handle slow or flaky dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No duplicate work. No race conditions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error handling and control
&lt;/h2&gt;

&lt;p&gt;By default, errors are cached and replayed — preventing infinite retries.&lt;/p&gt;

&lt;p&gt;You can opt out selectively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&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="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;shouldIgnoreError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;retryable&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  When to use this
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;idempotency-redis&lt;/code&gt; if you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build APIs that mutate state&lt;/li&gt;
&lt;li&gt;Accept retries or webhooks&lt;/li&gt;
&lt;li&gt;Run multiple Node.js instances&lt;/li&gt;
&lt;li&gt;Care about correctness under failure&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Learn more
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📦 npm: &lt;a href="https://www.npmjs.com/package/idempotency-redis" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/idempotency-redis&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🐙 GitHub: &lt;a href="https://github.com/foreverest/idempotency-redis" rel="noopener noreferrer"&gt;https://github.com/foreverest/idempotency-redis&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’ve ever debugged a “why did this run twice?” incident — idempotency isn’t optional. It’s infrastructure.&lt;/p&gt;

</description>
      <category>idempotency</category>
      <category>redis</category>
      <category>node</category>
      <category>npm</category>
    </item>
  </channel>
</rss>
