<?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: Prism Infoways Private Limited</title>
    <description>The latest articles on DEV Community by Prism Infoways Private Limited (@prisminfoways).</description>
    <link>https://dev.to/prisminfoways</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%2F4060001%2F144d3634-5ae1-4a49-89af-f02b2a0a28a5.png</url>
      <title>DEV Community: Prism Infoways Private Limited</title>
      <link>https://dev.to/prisminfoways</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prisminfoways"/>
    <language>en</language>
    <item>
      <title>Idempotency Keys Explained: How to Make API Retries Safe (With Node.js and Redis Code)</title>
      <dc:creator>Prism Infoways Private Limited</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:00:24 +0000</pubDate>
      <link>https://dev.to/prisminfoways/idempotency-keys-explained-how-to-make-api-retries-safe-with-nodejs-and-redis-code-27n4</link>
      <guid>https://dev.to/prisminfoways/idempotency-keys-explained-how-to-make-api-retries-safe-with-nodejs-and-redis-code-27n4</guid>
      <description>&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%2F9y6l4ol7ugto5rlwbr1d.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%2F9y6l4ol7ugto5rlwbr1d.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
A customer taps "Pay" on a slow mobile connection. The request reaches your server, the card is charged, and then the response gets lost on the way back. The app sees a timeout and does what every well-behaved client does: it retries.&lt;/p&gt;

&lt;p&gt;Now the customer has been charged twice.&lt;/p&gt;

&lt;p&gt;Nothing in that story is a bug in the usual sense. The server worked. The client worked. The network did what networks do. The problem is that the client cannot tell the difference between "the request never arrived" and "the request succeeded but I never heard back." Retrying is the only sensible move, so the server has to make retrying safe.&lt;/p&gt;

&lt;p&gt;That is what idempotency keys are for. This post covers how they work, the race condition most first implementations miss, and working Node.js code you can adapt.&lt;/p&gt;
&lt;h2&gt;
  
  
  What idempotency actually means
&lt;/h2&gt;

&lt;p&gt;An operation is idempotent if doing it once has the same effect as doing it many times.&lt;/p&gt;

&lt;p&gt;Some HTTP methods are idempotent by design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /orders/42&lt;/code&gt; reads data. Call it a hundred times and nothing changes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PUT /users/7&lt;/code&gt; with a full body sets the user to that state. Repeating it gives the same state.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DELETE /sessions/abc&lt;/code&gt; removes the session. The second call finds nothing to remove, and the end state is the same.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;POST&lt;/code&gt; is the odd one out. &lt;code&gt;POST /payments&lt;/code&gt; creates a new payment every time it runs. Two calls, two payments. The same goes for sending an email, placing an order, or issuing a refund.&lt;/p&gt;

&lt;p&gt;You can't make these operations naturally idempotent, so you add a mechanism that lets the server recognise a repeat.&lt;/p&gt;
&lt;h2&gt;
  
  
  How idempotency keys work
&lt;/h2&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The client generates a unique ID (a UUID is fine) for each logical operation, not for each HTTP attempt.&lt;/li&gt;
&lt;li&gt;It sends that ID in a header, usually &lt;code&gt;Idempotency-Key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The server checks whether it has seen that key before.&lt;/li&gt;
&lt;li&gt;If not, it processes the request and stores the response against the key.&lt;/li&gt;
&lt;li&gt;If it has, it skips the work and returns the stored response.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;POST&lt;/span&gt; &lt;span class="nn"&gt;/payments&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="na"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7f3c2a9e-1b4d-4c8a-9e2f-5a6b7c8d9e0f&lt;/span&gt;

&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ord_812"&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;p&gt;The first request charges the card. Every retry with the same key gets the same response back, and the card is charged once.&lt;/p&gt;
&lt;h2&gt;
  
  
  The naive version and its race condition
&lt;/h2&gt;

&lt;p&gt;Most first attempts look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/payments&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&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;cached&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`idem:&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="s2"&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;cached&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&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;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;chargeCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// slow&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`idem:&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&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;It handles the easy case: a retry that arrives after the first request has finished.&lt;/p&gt;

&lt;p&gt;It fails the hard case. Suppose the retry arrives while the first request is still inside &lt;code&gt;chargeCard&lt;/code&gt;. Both requests run the &lt;code&gt;get&lt;/code&gt;, both find nothing, and both charge the card. The check and the write are two separate steps, and anything can happen between them.&lt;/p&gt;

&lt;p&gt;This is not rare. Aggressive client timeouts, double-clicks, and load balancer retries all produce two copies of a request within milliseconds of each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safer Express middleware with Redis
&lt;/h2&gt;

&lt;p&gt;The fix is to claim the key atomically before doing any work. Redis &lt;code&gt;SET&lt;/code&gt; with the &lt;code&gt;NX&lt;/code&gt; option does this: it writes only if the key doesn't exist, and tells you whether it won.&lt;/p&gt;

&lt;p&gt;The middleware below also does two more things. It scopes keys to the user, so one customer can't collide with another. And it stores a hash of the request, so the same key reused with a different body is rejected instead of silently returning the wrong response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&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="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ioredis&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;LOCK_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// how long a request may stay "processing"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RESULT_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// how long a finished result is kept&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&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;crypto&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalUrl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;idempotency&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&lt;/span&gt;&lt;span class="dl"&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="o"&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;res&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key header is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&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;redisKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`idem:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&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="nx"&gt;key&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="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Atomic claim: only one request can win this.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claimed&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;redisKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;LOCK_SECONDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NX&lt;/span&gt;&lt;span class="dl"&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;claimed&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&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;raw&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redisKey&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;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;processing&lt;/span&gt;&lt;span class="dl"&gt;"&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;res&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Request is already in progress. Retry shortly.&lt;/span&gt;&lt;span class="dl"&gt;"&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;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;hash&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key was already used with a different request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Finished earlier: replay the stored response.&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// We own the key. Capture the response when the handler sends it.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sendJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Server failed: release the key so the client can retry.&lt;/span&gt;
        &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redisKey&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&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="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nx"&gt;redisKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;EX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nx"&gt;RESULT_SECONDS&lt;/span&gt;
          &lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&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="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sendJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/payments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;idempotency&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="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;chargeCard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payment&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;What each status code tells the client:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;201 (or whatever the handler returned):&lt;/strong&gt; done, whether this was the first attempt or a replay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;409:&lt;/strong&gt; the original request is still running. Wait and retry with the same key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;422:&lt;/strong&gt; the key was reused for a different request. This is a client bug, so don't retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5xx:&lt;/strong&gt; the attempt failed and the key was released. Safe to retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Redis is not enough: the database version
&lt;/h2&gt;

&lt;p&gt;The middleware has one gap. The business write (the payment row) and the idempotency record live in two different systems. If the process crashes after the payment commits but before Redis is updated, the lock expires and a retry runs the handler again.&lt;/p&gt;

&lt;p&gt;For most endpoints that window is acceptable. For money, it isn't. The stronger pattern is to store the key in the same database, in the same transaction, as the work itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;idempotency_keys&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;       &lt;span class="nb"&gt;BIGINT&lt;/span&gt;      &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;idem_key&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;request_hash&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt;        &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;response_code&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;response_body&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;    &lt;span class="n"&gt;TIMESTAMPTZ&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idem_key&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;Inside one transaction, insert the key row, do the work, and save the response. The primary key makes the claim atomic: a second insert with the same key fails. And because everything commits or rolls back together, you can never end up with a payment that has no idempotency record.&lt;/p&gt;

&lt;p&gt;If your handler calls an external provider such as a payment gateway, pass your key through to them as well. Most payment APIs accept an idempotency key of their own, which protects the one step your database transaction can't cover.&lt;/p&gt;

&lt;h2&gt;
  
  
  The client side: retrying with the same key
&lt;/h2&gt;

&lt;p&gt;The server can only recognise a retry if the client sends the same key. Generate it once per operation, outside the retry loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&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;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// once per operation, NOT per attempt&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&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;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.example.com/payments&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Idempotency-Key&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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;

      &lt;span class="c1"&gt;// Anything except "still processing" or a server error is final.&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;500&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &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="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Network error or timeout: fall through and retry.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Exponential backoff: 0.5s, 1s, 2s, 4s&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment request failed after retries&lt;/span&gt;&lt;span class="dl"&gt;"&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;In a web or mobile app, create the key when the user opens the checkout screen or taps the button, and keep it until the operation finishes. That way a double-tap or an app restart mid-request still reuses the same key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Generating a new key on every retry.&lt;/strong&gt; This is the most common one, and it defeats the whole mechanism. Each attempt looks like a brand-new request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check-then-set without atomicity.&lt;/strong&gt; A separate &lt;code&gt;GET&lt;/code&gt; followed by &lt;code&gt;SET&lt;/code&gt; leaves the race condition shown earlier. Use &lt;code&gt;SET NX&lt;/code&gt; or a unique constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not scoping keys to the user.&lt;/strong&gt; A global key namespace lets one client replay another client's response if keys collide or are guessed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring the request body.&lt;/strong&gt; If you don't compare a request hash, a client that reuses a key by mistake gets back a response for a completely different request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching server errors.&lt;/strong&gt; If you store a 500, the client can never recover with that key. Store successes and client errors; release the key on server errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No expiry.&lt;/strong&gt; Keys kept forever grow without limit. 24 hours is a common retention window, since retries after that are almost never the same logical operation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A lock that outlives nothing.&lt;/strong&gt; If the "processing" marker has no TTL and the process crashes, the key is stuck in 409 forever. Always give the lock an expiry longer than your slowest request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist before you ship
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Every non-idempotent write endpoint (payments, orders, emails, refunds) requires an &lt;code&gt;Idempotency-Key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The key is claimed atomically before any work starts.&lt;/li&gt;
&lt;li&gt;Keys are scoped per user or per API client.&lt;/li&gt;
&lt;li&gt;The request hash is stored and compared on replay.&lt;/li&gt;
&lt;li&gt;In-progress requests return 409, mismatched bodies return 422.&lt;/li&gt;
&lt;li&gt;Server errors release the key; successes are stored with a TTL.&lt;/li&gt;
&lt;li&gt;Clients generate the key once per operation and reuse it on every retry.&lt;/li&gt;
&lt;li&gt;For money, the key is written in the same transaction as the business data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Idempotency is one of those things nobody notices when it works and everybody notices when it doesn't. It costs a few dozen lines of code up front, and it removes a whole class of duplicate-charge and duplicate-order bugs that are painful to clean up afterwards.&lt;/p&gt;

&lt;p&gt;If you're building payment flows, order systems, or any API where a retry must not repeat the work, the team at &lt;a href="https://prisminfoways.com/" rel="noopener noreferrer"&gt;Prism Infoways&lt;/a&gt; designs and builds backends with this kind of reliability built in from the start.&lt;/p&gt;

&lt;p&gt;How do you handle retries in your APIs? Share your approach in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>node</category>
      <category>backend</category>
    </item>
    <item>
      <title>Caching Strategies Explained: Cache-Aside vs Write-Through vs Write-Behind (With Redis Code)</title>
      <dc:creator>Prism Infoways Private Limited</dc:creator>
      <pubDate>Mon, 05 Oct 2026 05:24:00 +0000</pubDate>
      <link>https://dev.to/prisminfoways/caching-strategies-explained-cache-aside-vs-write-through-vs-write-behind-with-redis-code-1gd0</link>
      <guid>https://dev.to/prisminfoways/caching-strategies-explained-cache-aside-vs-write-through-vs-write-behind-with-redis-code-1gd0</guid>
      <description>&lt;p&gt;A slow database query is one of the most common reasons a web app feels sluggish. Caching fixes it by keeping the answer in memory, so the next request skips the database. But the wrong caching strategy creates a new problem: users see stale prices, old profiles, or missing updates.&lt;/p&gt;

&lt;p&gt;This guide explains the four caching strategies used in production: cache-aside, read-through, write-through, and write-behind. Each one comes with Node.js and Redis code, clear trade-offs, and the failure modes to plan for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Four Caching Strategies at a Glance
&lt;/h2&gt;

&lt;p&gt;Cache-aside is the right default for most web apps. The other three solve specific problems it cannot.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Main risk&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache-aside&lt;/td&gt;
&lt;td&gt;App checks the cache, falls back to the database, then fills the cache&lt;/td&gt;
&lt;td&gt;Read-heavy data such as products, articles, API responses&lt;/td&gt;
&lt;td&gt;Stale data until the TTL expires or the key is deleted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Read-through&lt;/td&gt;
&lt;td&gt;The cache layer loads from the database itself on a miss&lt;/td&gt;
&lt;td&gt;Same as cache-aside, with cleaner application code&lt;/td&gt;
&lt;td&gt;Needs a library or provider that supports it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write-through&lt;/td&gt;
&lt;td&gt;Every write updates the database and the cache together&lt;/td&gt;
&lt;td&gt;Data that is read right after it is written, such as profiles and settings&lt;/td&gt;
&lt;td&gt;Slower writes, and memory spent on data nobody reads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write-behind&lt;/td&gt;
&lt;td&gt;Writes go to the cache first and reach the database later in batches&lt;/td&gt;
&lt;td&gt;High-volume counters such as views, likes, and analytics events&lt;/td&gt;
&lt;td&gt;Data loss if the cache fails before the flush&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1. Cache-Aside (Lazy Loading)
&lt;/h2&gt;

&lt;p&gt;In cache-aside, your application code manages the cache. It checks the cache first, reads the database only on a miss, and stores the result for next time.&lt;/p&gt;

&lt;p&gt;The examples below use the &lt;code&gt;redis&lt;/code&gt; npm client. &lt;code&gt;db&lt;/code&gt; stands in for your own data layer.&lt;br&gt;
&lt;/p&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;createClient&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;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="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;REDIS_URL&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&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;getProduct&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="p"&gt;{&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;`product:&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="c1"&gt;// 1. Try the cache first&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Cache miss: read from the database&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Store it for next time, with a TTL&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;EX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&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;product&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&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;updateProduct&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="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&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;product&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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="nx"&gt;changes&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product:&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="c1"&gt;// invalidate, don't update&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;product&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;Notice that the write path deletes the key instead of updating it. If two requests update the same product at once, a delete can never leave an older value behind. The next read simply rebuilds the entry from the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only data that is actually requested gets cached, so memory stays small.&lt;/li&gt;
&lt;li&gt;If Redis goes down, the app still works. It just reads from the database.&lt;/li&gt;
&lt;li&gt;It works with any database and any cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first request for each key is slow, because it always misses.&lt;/li&gt;
&lt;li&gt;Data can be stale until the TTL expires if you forget to invalidate on a write.&lt;/li&gt;
&lt;li&gt;Caching logic gets repeated across your code unless you wrap it in a helper.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Read-Through
&lt;/h2&gt;

&lt;p&gt;Read-through gives the same result as cache-aside, but the cache layer does the loading. Your code asks the cache for a key and never talks to the database directly.&lt;/p&gt;

&lt;p&gt;You can get the same clean call sites with one small wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;readThrough&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;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadFn&lt;/span&gt;&lt;span class="p"&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;cached&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadFn&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;value&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;EX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ttl&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;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage: one line per cached query&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readThrough&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`product:&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="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findById&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="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trade-offs match cache-aside. The gain is consistency: every cached query uses the same TTL rules, key format, and error handling.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Write-Through
&lt;/h2&gt;

&lt;p&gt;Write-through updates the cache on every write, in the same operation as the database. Reads then almost always hit the cache, and they return fresh data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Database first, so a failed write never reaches the cache&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;changes&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`user:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;EX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&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;profile&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;Use it when users read data straight after changing it. A profile page that shows the old name after a save is the classic bug this prevents.&lt;/p&gt;

&lt;p&gt;It has two costs. Every write now does two operations, so writes get slower. You also cache records that may never be read, so always set a TTL to clear them out.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Write-Behind (Write-Back)
&lt;/h2&gt;

&lt;p&gt;Write-behind sends each write to the cache only and saves it to the database later, in batches. The user gets an instant response, and the database sees one write instead of hundreds.&lt;/p&gt;

&lt;p&gt;A page-view counter is a good fit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;recordView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Fast path: touch the cache only&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;incr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`post:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:views`&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sAdd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dirty:post-views&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;postId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Background worker: flush changed counters every 5 seconds&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;flushViews&lt;/span&gt;&lt;span class="p"&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;ids&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sPop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dirty:post-views&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;for &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;id&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&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;views&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`post:&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;:views`&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;views&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;views&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flushViews&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A post that gets 1,000 views in five seconds now costs one database write, not 1,000.&lt;/p&gt;

&lt;p&gt;The risk is data loss. Anything still waiting in the cache disappears if Redis crashes before the flush. Keep write-behind for data where losing a few seconds is acceptable, and never use it for orders or payments.&lt;/p&gt;

&lt;p&gt;For heavier workloads, replace the simple set with a durable queue such as Redis Streams or Kafka. The worker can then retry a failed batch instead of dropping it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache Invalidation and TTLs: Keeping Data Fresh
&lt;/h2&gt;

&lt;p&gt;Every cached key needs two things: a TTL, and a plan for what happens when the source data changes. Skip either one and users will see stale data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always set a TTL.&lt;/strong&gt; It is your safety net when an invalidation is missed. A key with no expiry can stay wrong forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pick the TTL from the business rule.&lt;/strong&gt; Ask how stale this data may be. A blog post can be minutes old. A stock count at checkout cannot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete on write.&lt;/strong&gt; When a record changes, delete its key in the same code path, as in the cache-aside example.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use predictable key names.&lt;/strong&gt; A format like &lt;code&gt;product:42&lt;/code&gt; or &lt;code&gt;user:7:orders&lt;/code&gt; makes it easy to find what to delete.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add jitter to TTLs.&lt;/strong&gt; A small random offset stops thousands of keys from expiring in the same second.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// 300 to 359 seconds, so keys expire at different times&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Three Cache Failures to Plan For
&lt;/h2&gt;

&lt;p&gt;Most caching bugs in production are one of three patterns. Each has a simple, well-known fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cache stampede
&lt;/h3&gt;

&lt;p&gt;A popular key expires, and hundreds of requests miss at the same moment. All of them run the same slow query, and the database stalls.&lt;/p&gt;

&lt;p&gt;The fix is a short lock, so only one request rebuilds the value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getWithLock&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;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadFn&lt;/span&gt;&lt;span class="p"&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;cached&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Only one request wins the lock&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gotLock&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`lock:&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;NX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;EX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;gotLock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Someone else is rebuilding: wait briefly, then retry&lt;/span&gt;
    &lt;span class="c1"&gt;// (add a retry limit in production)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&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;getWithLock&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;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;loadFn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;try&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;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadFn&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;EX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ttl&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;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`lock:&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="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&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;h3&gt;
  
  
  Cache penetration
&lt;/h3&gt;

&lt;p&gt;Requests keep asking for IDs that do not exist, such as &lt;code&gt;/product/999999&lt;/code&gt;. Nothing is ever cached, so every request reaches the database. Bots and scrapers cause this often.&lt;/p&gt;

&lt;p&gt;The fix is to cache the "not found" result for a short time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;)&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;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;null&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;EX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// remember the miss&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&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;h3&gt;
  
  
  Cache avalanche
&lt;/h3&gt;

&lt;p&gt;A large share of keys expire together, or the cache restarts empty. The database suddenly takes the full traffic load.&lt;/p&gt;

&lt;p&gt;TTL jitter, shown in the previous section, solves the first case. For the second, warm the most important keys at startup before sending traffic to the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Choose the Right Caching Strategy
&lt;/h2&gt;

&lt;p&gt;Start with cache-aside and a TTL, then add another strategy only where a specific problem appears.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mostly reads, and slightly stale data is fine?&lt;/strong&gt; Use cache-aside. This covers product pages, articles, search results, and most API responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Users read data right after changing it?&lt;/strong&gt; Add write-through for those records, such as profiles, settings, and carts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Thousands of small writes per minute?&lt;/strong&gt; Use write-behind, but only where losing a few seconds of data is acceptable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Many cached queries across a large codebase?&lt;/strong&gt; Wrap them in a read-through helper so every query follows the same rules.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Some things should not be cached at all:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queries that already run in a few milliseconds. The cache adds complexity without a visible gain.&lt;/li&gt;
&lt;li&gt;Values that must be exact at the moment of use, such as payment status or stock at checkout.&lt;/li&gt;
&lt;li&gt;Data that is unique to each request and rarely repeated. The hit rate will be too low to matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measure before and after. Track your cache hit rate and your slowest queries, and cache the few that cause most of the load.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which caching strategy is best?
&lt;/h3&gt;

&lt;p&gt;Cache-aside is the best default for most web applications. It is simple, it survives a cache outage, and it only stores data that users actually request.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between cache-aside and read-through?
&lt;/h3&gt;

&lt;p&gt;The difference is who loads the data on a miss. In cache-aside, your application code queries the database and fills the cache. In read-through, the cache layer or a helper does it for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between write-through and write-behind?
&lt;/h3&gt;

&lt;p&gt;Write-through saves to the database immediately, as part of the same request. Write-behind saves to the cache first and updates the database later in batches. Write-through is safer, and write-behind is faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  What TTL should I use for cached data?
&lt;/h3&gt;

&lt;p&gt;There is no universal number. Start from how stale the data is allowed to be, then test. Many teams use minutes for content pages and hours for settings that rarely change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Redis the only option for caching?
&lt;/h3&gt;

&lt;p&gt;No. Memcached and Valkey work with the same patterns. A small in-process LRU cache is enough for a single server, and a CDN is the right cache for full HTTP responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Caching is one of the cheapest ways to make a web app faster, as long as the strategy fits the data. Use cache-aside by default, write-through where freshness matters, and write-behind only for high-volume writes you can afford to lose. Then set TTLs, add jitter, and protect hot keys from a stampede.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://prisminfoways.com/" rel="noopener noreferrer"&gt;Prism Infoways&lt;/a&gt;, we build and scale web applications for businesses worldwide. If your app is slowing down as traffic grows, our &lt;a href="https://prisminfoways.com/our-services/web-services" rel="noopener noreferrer"&gt;web development&lt;/a&gt; and &lt;a href="https://prisminfoways.com/our-services/cloud-computing" rel="noopener noreferrer"&gt;cloud computing&lt;/a&gt; teams can help you find the bottleneck and fix it. You can &lt;a href="https://prisminfoways.com/contact" rel="noopener noreferrer"&gt;reach us here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Which caching strategy do you use in production, and what problems did it cause? Share your experience in the comments.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>redis</category>
      <category>backend</category>
      <category>performance</category>
    </item>
    <item>
      <title>Rate Limiting Algorithms Explained: Token Bucket vs. Sliding Window (With Code)</title>
      <dc:creator>Prism Infoways Private Limited</dc:creator>
      <pubDate>Fri, 25 Sep 2026 07:25:27 +0000</pubDate>
      <link>https://dev.to/prisminfoways/rate-limiting-algorithms-explained-token-bucket-vs-sliding-window-with-code-46mj</link>
      <guid>https://dev.to/prisminfoways/rate-limiting-algorithms-explained-token-bucket-vs-sliding-window-with-code-46mj</guid>
      <description>&lt;p&gt;Almost every production API needs rate limiting eventually — to stop abuse, protect against traffic spikes, and keep one noisy client from degrading service for everyone else. Most tutorials tell you &lt;em&gt;to&lt;/em&gt; add rate limiting, far fewer explain the actual tradeoffs between the different algorithms you could use. Picking the wrong one for your situation means either blocking legitimate traffic unnecessarily or failing to actually protect your system under load.&lt;/p&gt;

&lt;p&gt;Here's a practical walkthrough of the main approaches, with working code for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixed Window Counter
&lt;/h2&gt;

&lt;p&gt;The simplest approach: count requests in fixed time windows (e.g., per minute), reset the counter when the window ends.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FixedWindowLimiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_seconds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_requests&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window_seconds&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;  &lt;span class="c1"&gt;# key -&amp;gt; (window_start, count)
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;window_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;window_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_requests&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; This has a well-known edge case — the "boundary burst" issue. If a client sends max requests at the very end of one window, and max requests again at the very start of the next window, they've effectively sent 2x their limit in a short burst spanning the boundary, and the algorithm never notices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When it's still fine:&lt;/strong&gt; For rough, low-stakes limiting where occasional boundary bursts don't matter much, fixed window is simple and cheap. Don't reach for something more complex than your actual requirement needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sliding Window Log
&lt;/h2&gt;

&lt;p&gt;Keeps a timestamp for every request in a rolling window, and counts how many fall within the last N seconds from &lt;em&gt;now&lt;/em&gt; — not from a fixed boundary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SlidingWindowLogLimiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_seconds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_requests&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window_seconds&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;  &lt;span class="c1"&gt;# key -&amp;gt; deque of timestamps
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;logs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="c1"&gt;# Drop timestamps outside the current window
&lt;/span&gt;        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; This is accurate — no boundary burst problem — but it costs memory proportional to the number of requests per client per window, which can get expensive at high request volumes or with many distinct clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sliding Window Counter (The Practical Middle Ground)
&lt;/h2&gt;

&lt;p&gt;Approximates the sliding window log's accuracy without storing every timestamp, by blending the current and previous fixed-window counts, weighted by how far into the current window you are.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SlidingWindowCounterLimiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_seconds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_requests&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window_seconds&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;windows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;  &lt;span class="c1"&gt;# key -&amp;gt; {current_start, current_count, previous_count}
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;current_start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt;

        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;current_start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;current_start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;previous_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;current_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;current_start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;current_start&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;current_start&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;current_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;current_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;previous_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;previous_count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;windows&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;

        &lt;span class="n"&gt;elapsed_in_current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;current_start&lt;/span&gt;
        &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;elapsed_in_current&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;estimated_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;previous_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;current_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;estimated_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;current_count&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this is often the best default:&lt;/strong&gt; It's memory-efficient (just two counters per client, not a log of every request), and it smooths out the boundary burst problem well enough for the vast majority of real use cases. This is what most production rate limiters (including the one built into many API gateways) actually use under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  Token Bucket
&lt;/h2&gt;

&lt;p&gt;A different mental model entirely: each client has a "bucket" that holds tokens, refilled at a steady rate. Each request consumes one token; if the bucket is empty, the request is rejected. This naturally allows short bursts (up to the bucket's capacity) while enforcing a steady average rate over time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenBucketLimiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;refill_rate_per_second&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refill_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;refill_rate_per_second&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;  &lt;span class="c1"&gt;# key -&amp;gt; (tokens, last_refill_time)
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_refill&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_refill&lt;/span&gt;
        &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;refill_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why teams choose this:&lt;/strong&gt; Token bucket is the natural fit when you genuinely want to allow bursts — a client that's been idle should be able to send a quick flurry of requests, as long as their &lt;em&gt;average&lt;/em&gt; rate over time stays within budget. This maps well onto real usage patterns (a user rapidly clicking through a UI after being idle, for instance) better than a strict per-window count does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leaky Bucket (The Opposite Philosophy)
&lt;/h2&gt;

&lt;p&gt;Where token bucket allows bursts, leaky bucket smooths them out — requests go into a queue (the "bucket"), and are processed at a constant, fixed rate, regardless of how bursty the incoming traffic is.&lt;/p&gt;

&lt;p&gt;This is less commonly implemented as a simple in-memory limiter and more often realized through an actual request queue with a fixed-rate worker pulling from it — useful when you need to protect a downstream system that genuinely can't handle bursts at all, even brief ones (a legacy system with a hard concurrency limit, for example).&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing Between Them
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Best fit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Simple, low-stakes limiting, don't want to over-engineer&lt;/td&gt;
&lt;td&gt;Fixed window counter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need accuracy and boundary bursts genuinely matter&lt;/td&gt;
&lt;td&gt;Sliding window log (if volume is manageable) or sliding window counter (if not)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Want to allow natural bursts for idle-then-active clients&lt;/td&gt;
&lt;td&gt;Token bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protecting a downstream system that can't handle any burst at all&lt;/td&gt;
&lt;td&gt;Leaky bucket / queue-based smoothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Building a general-purpose API rate limiter with good default behavior&lt;/td&gt;
&lt;td&gt;Sliding window counter&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  A Practical Note on Distributed Systems
&lt;/h2&gt;

&lt;p&gt;Every example above assumes a single in-memory store. The moment you're running multiple API server instances behind a load balancer, in-memory counters per instance won't give you an accurate global rate limit — a client could get &lt;code&gt;max_requests&lt;/code&gt; allowed on &lt;em&gt;each&lt;/em&gt; instance. For distributed rate limiting, the same algorithms apply, but the counters need to live in a shared store (Redis is the common choice), typically implemented with atomic operations (&lt;code&gt;INCR&lt;/code&gt; + &lt;code&gt;EXPIRE&lt;/code&gt;, or a Lua script for the more complex sliding window variants) to avoid race conditions between instances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;There's no universally "correct" rate limiting algorithm — the right choice depends on whether you're optimizing for memory efficiency, accuracy, or burst tolerance, and what your downstream system can actually handle. Understanding the tradeoffs behind each approach, rather than reaching for whatever the first tutorial you find implements, is what makes the difference between rate limiting that actually protects your system and rate limiting that just adds complexity without solving the real problem.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Prism Infoways builds and audits backend systems and APIs, including rate limiting and abuse-protection strategies, for growing products. More at &lt;a href="https://prisminfoways.com/" rel="noopener noreferrer"&gt;prisminfoways.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>webdev</category>
      <category>api</category>
      <category>programming</category>
    </item>
    <item>
      <title>JWT vs. Session-Based Authentication: A Practical Guide for Choosing the Right One</title>
      <dc:creator>Prism Infoways Private Limited</dc:creator>
      <pubDate>Wed, 09 Sep 2026 05:19:39 +0000</pubDate>
      <link>https://dev.to/prisminfoways/jwt-vs-session-based-authentication-a-practical-guide-for-choosing-the-right-one-3alp</link>
      <guid>https://dev.to/prisminfoways/jwt-vs-session-based-authentication-a-practical-guide-for-choosing-the-right-one-3alp</guid>
      <description>&lt;p&gt;Every new backend project eventually hits the same fork in the road: JWT or sessions? Half the tutorials online will tell you JWT is the modern, scalable choice. The other half will tell you sessions are simpler and more secure. Both are right in different contexts, and picking wrong tends to bite you months later, once you're deep into a specific auth flow and the tradeoffs suddenly matter.&lt;/p&gt;

&lt;p&gt;Here's an actual practical framework for choosing, not just a definitions list.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Difference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Session-based auth&lt;/strong&gt; stores session state on the server. When a user logs in, the server creates a session record (typically in a database or in-memory store like Redis), and sends the client a session ID in a cookie. On every request, the server looks up that ID to know who the user is.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client                          Server
  |-- POST /login ---------------&amp;gt;|
  |                                | creates session, stores in Redis
  |&amp;lt;-- Set-Cookie: sessionId=xyz --|
  |-- GET /profile (cookie) -----&amp;gt;|
  |                                | looks up sessionId in Redis
  |&amp;lt;-- user data ------------------|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JWT (JSON Web Token) auth&lt;/strong&gt; is stateless. When a user logs in, the server issues a signed token containing the user's claims (ID, roles, expiry). The client sends this token on every request, and the server verifies the signature — no database lookup needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client                          Server
  |-- POST /login ---------------&amp;gt;|
  |                                | verifies credentials, signs JWT
  |&amp;lt;-- { token: "eyJhbGc..." } ---|
  |-- GET /profile (Bearer token)-&amp;gt;|
  |                                | verifies signature, reads claims
  |&amp;lt;-- user data ------------------|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one difference — state stored server-side vs. encoded in the token itself — is the root of almost every tradeoff between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Sessions Win
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Instant revocation.&lt;/strong&gt; If you need to log a user out immediately — forcibly, from the server side, say after a password change or a security incident — sessions handle this trivially: delete the session record, and the next request fails. With JWT, the token is valid until it expires, full stop, unless you build a separate revocation mechanism (more on that below).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smaller client payload.&lt;/strong&gt; A session cookie is just an ID — a few bytes. A JWT carrying several claims can be several hundred bytes to a few KB, sent on every single request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simpler mental model for traditional web apps.&lt;/strong&gt; If you're building a server-rendered app where the browser and backend are tightly coupled, sessions are the more direct fit — this is what they were designed for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sensitive data stays server-side.&lt;/strong&gt; Session data never leaves your server. A JWT's payload, by contrast, is base64-encoded, not encrypted — anyone with the token can decode and read the claims (they can't forge a valid signature, but they can read what's inside).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where JWT Wins
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Statelessness for horizontal scaling.&lt;/strong&gt; Since the token carries everything needed to verify identity, any server instance can validate it without a shared session store. This matters a lot once you're running multiple backend instances behind a load balancer — sessions need a shared store (Redis, typically) to work across instances; JWT doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cross-domain and mobile-friendly.&lt;/strong&gt; Cookies get complicated across domains and don't map cleanly onto native mobile clients. A bearer token in an &lt;code&gt;Authorization&lt;/code&gt; header works the same way regardless of client type or domain — which is why most public APIs and mobile backends default to token-based auth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Natural fit for microservices.&lt;/strong&gt; If you have multiple services that need to verify a user's identity independently, a JWT signed by a central auth service lets each downstream service verify the token locally, without calling back to a central session store on every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Part Most Tutorials Skip: JWT Revocation
&lt;/h2&gt;

&lt;p&gt;The single biggest practical problem with JWT is the one most getting-started guides gloss over: &lt;strong&gt;how do you log someone out before the token naturally expires?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few real approaches, each with tradeoffs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short expiry + refresh tokens.&lt;/strong&gt; Issue short-lived access tokens (5-15 minutes) alongside a longer-lived refresh token stored server-side. To "log out," you revoke the refresh token; the access token still works until it naturally expires, but that window is small.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Access token: short-lived, stateless&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accessToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;ACCESS_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;15m&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Refresh token: longer-lived, stored server-side so it CAN be revoked&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;refreshToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;REFRESH_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;7d&lt;/span&gt;&lt;span class="dl"&gt;'&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;refreshTokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;refreshToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;revoked&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A denylist for the rare "must revoke immediately" case.&lt;/strong&gt; For genuinely urgent revocations (compromised account), maintain a small, fast-lookup denylist (Redis, with a TTL matching the token's remaining life) of tokens or user IDs that should be rejected even if their signature is valid. This reintroduces a bit of the statefulness you were trying to avoid, but only for the exceptional case, not every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Version/generation numbers.&lt;/strong&gt; Store a &lt;code&gt;tokenVersion&lt;/code&gt; field on the user record. Include it as a claim in the JWT. On password change or forced logout, increment the version — any token issued before that increment fails validation on next check against the user record. This needs one lookup per request (or a cached version check), which is a smaller cost than a full session lookup.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Decision Guide
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Lean toward&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Traditional server-rendered web app, single domain&lt;/td&gt;
&lt;td&gt;Sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public API consumed by third parties&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile app + backend&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microservices needing independent auth verification&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need instant, guaranteed logout/revocation&lt;/td&gt;
&lt;td&gt;Sessions (or JWT + denylist)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Horizontally scaled backend, no shared session store set up&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Already running Redis/shared cache for other reasons&lt;/td&gt;
&lt;td&gt;Sessions become much simpler, tradeoff shrinks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Honest Middle Ground
&lt;/h2&gt;

&lt;p&gt;In practice, a lot of production systems end up as a hybrid: short-lived JWTs for stateless verification across services, backed by a server-side refresh-token record that gives you a real revocation point. You get most of JWT's scaling benefits without fully giving up the ability to say "this user is logged out, right now."&lt;/p&gt;

&lt;p&gt;Don't pick JWT just because it's the trendier answer in tutorials, and don't pick sessions just because they're the "classic" choice. Pick based on whether you actually need statelessness (multi-instance scaling, cross-service verification, mobile/API clients) or whether instant revocation and simplicity matter more for your specific app. Most projects know the answer once they ask the question honestly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Prism Infoways builds and audits authentication systems and backend architecture for growing products. If you're deciding on an auth strategy for a new build or fixing issues in an existing one, check out &lt;a href="https://prisminfoways.com/" rel="noopener noreferrer"&gt;prisminfoways.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>security</category>
      <category>backend</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Native vs Cross-Platform App Development in 2026: Which Actually Saves You More Money?</title>
      <dc:creator>Prism Infoways Private Limited</dc:creator>
      <pubDate>Sat, 29 Aug 2026 15:29:36 +0000</pubDate>
      <link>https://dev.to/prisminfoways/native-vs-cross-platform-app-development-in-2026-which-actually-saves-you-more-money-2661</link>
      <guid>https://dev.to/prisminfoways/native-vs-cross-platform-app-development-in-2026-which-actually-saves-you-more-money-2661</guid>
      <description>&lt;p&gt;If you're planning a mobile app in 2026, you'll hit this question in your first client or product meeting: should you build native (separate iOS and Android codebases) or cross-platform (one codebase for both)?&lt;br&gt;
The answer used to be simple — "native for anything serious, cross-platform for MVPs." That advice is outdated. Both Flutter and React Native shipped major architectural upgrades this year, and the gap that used to justify going native by default has narrowed a lot. This post breaks down what's actually different in 2026, what it costs, and how to decide without getting stuck in framework-war noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native vs Cross-Platform: The Real Difference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Native app development&lt;/strong&gt; means writing separate code for each platform — Swift/SwiftUI for iOS, Kotlin/Jetpack Compose for Android. Two codebases, two teams (or one team working twice), full access to every platform API on day one.&lt;br&gt;
&lt;strong&gt;Cross-platform development&lt;/strong&gt; means writing one codebase that compiles down to both platforms. In 2026, this space is really a two-horse race:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flutter&lt;/strong&gt; (Google) — compiles to native ARM code, draws every pixel itself through its own rendering engine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Native&lt;/strong&gt; (Meta) — renders using real native UI components, written in JavaScript/TypeScript with a React-based architecture
A third option, Kotlin Multiplatform, is gaining traction for teams that want to share business logic while keeping fully native UI layers — worth knowing about, but still a niche choice for most product teams right now.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Changed in 2026
&lt;/h2&gt;

&lt;p&gt;Both major frameworks matured significantly this year, and it's worth understanding what actually changed before you pick a side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flutter&lt;/strong&gt; now runs on its Impeller rendering engine by default (replacing the older Skia backend), which pre-compiles shaders and removes the first-run stutter Flutter was previously known for. Independent 2026 benchmarks put it at roughly 58–60 FPS on heavy, animation-rich UIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Native&lt;/strong&gt; completed its shift to the "New Architecture" — Fabric renderer, JSI bridge, and TurboModules are now the default for new projects, and bridgeless mode has eliminated the old JS-to-native communication bottleneck. React Native tends to cold-start faster (around 200ms) and uses somewhat less battery in recent test suites, since it renders through real native components rather than its own engine.&lt;/li&gt;
&lt;li&gt;Flutter currently holds a larger share of the cross-platform market (roughly 46% vs React Native's 35–38%, per recent industry estimates), but React Native still draws from a much larger JavaScript developer pool, which matters more than market share when you're hiring.
The honest takeaway from nearly every 2026 benchmark: for most business apps, the performance difference between the two is no longer the deciding factor. The decision should come down to your team, your UI complexity, and your platform roadmap — not which framework "wins" on paper.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost Comparison: What You're Actually Paying For
&lt;/h2&gt;

&lt;p&gt;This is where the decision usually gets made in practice. Rough ranges for the Indian market in 2026:&lt;br&gt;
&lt;strong&gt;App Type&lt;/strong&gt;    &lt;strong&gt;Native (iOS + Android)&lt;/strong&gt;  Cross-Platform (Flutter/RN)&lt;br&gt;
Simple MVP (5–8 screens)  ₹6–10 lakh  ₹3–5 lakh&lt;br&gt;
Mid-complexity app (e-commerce, booking, on-demand) ₹12–25 lakh ₹7–15 lakh&lt;br&gt;
Complex app (fintech, real-time, heavy animation)   ₹25 lakh+ ₹18–30 lakh&lt;br&gt;
Cross-platform development typically saves 30–60% compared to building separate native apps, mainly because you're paying for one codebase and one QA cycle instead of two. That gap shrinks as app complexity grows — a highly complex app with platform-specific requirements can eat into cross-platform's cost advantage, sometimes to the point where native makes more sense anyway.&lt;br&gt;
Other cost factors people forget to budget for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance&lt;/strong&gt;: one codebase is cheaper to maintain long-term, full stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hiring&lt;/strong&gt;: React Native developers are easier to find (and often cheaper) than Flutter developers in most Indian tech hubs, simply due to pool size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-to-market&lt;/strong&gt;: faster launch means faster revenue or faster investor traction — sometimes worth more than the raw development cost difference.
When Native Actually Makes Sense
Don't default to cross-platform just because it's cheaper. Go native when:&lt;/li&gt;
&lt;li&gt;Your app is graphics- or camera-heavy (AR/VR, real-time video processing, complex custom animations)&lt;/li&gt;
&lt;li&gt;You need day-one access to brand-new OS features (Apple/Google often ship platform-specific APIs to native SDKs first)&lt;/li&gt;
&lt;li&gt;You're building for a single platform only, at least initially&lt;/li&gt;
&lt;li&gt;Your product is performance-critical in a way that can't tolerate any abstraction layer — think trading apps, high-precision health devices, or hardware-heavy IoT companion apps&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When Cross-Platform Is the Smarter Call
&lt;/h2&gt;

&lt;p&gt;Cross-platform wins for the majority of business apps in 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need to launch on iOS and Android simultaneously with a limited budget&lt;/li&gt;
&lt;li&gt;Your UI is standard (forms, lists, dashboards, e-commerce flows, content apps) rather than exotic&lt;/li&gt;
&lt;li&gt;You're validating a product idea and need to move fast and iterate&lt;/li&gt;
&lt;li&gt;Your team already knows JavaScript/React (favor React Native) or you want pixel-perfect design consistency across platforms and possibly web/desktop too (favor Flutter)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Quick Decision Framework
&lt;/h2&gt;

&lt;p&gt;Ask yourself these three questions, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Do you need the app on both platforms within the same budget cycle?&lt;/strong&gt; If yes, cross-platform is almost always the right call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is your UI graphics-heavy or does it need bleeding-edge OS features?&lt;/strong&gt; If yes, lean native — or at least prototype the riskiest screen in both approaches before committing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does your team already have strength in JavaScript, Dart, or native Swift/Kotlin?&lt;/strong&gt; Existing skill often matters more than theoretical framework advantages — a team that knows React will ship a better React Native app than a mediocre Flutter app, and vice versa.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Bottom Line
&lt;/h2&gt;

&lt;p&gt;In 2026, "native vs cross-platform" isn't a performance debate anymore — both paths are production-grade. It's a budget, timeline, and team-fit decision. Most growing businesses building their first or second app will get to market faster and cheaper with Flutter or React Native, and can always invest in native modules later for any screen that genuinely needs it.&lt;br&gt;
The wrong move is picking a framework based on what's trending on social media instead of what your app, budget, and team actually need.&lt;/p&gt;

&lt;p&gt;Choosing between native and cross-platform for your next app? &lt;a href="https://prisminfoways.com/" rel="noopener noreferrer"&gt;Prism Infoways&lt;/a&gt; builds both — from Flutter and React Native MVPs to fully native iOS/Android apps — and can help you scope the right approach before you spend a rupee on development. &lt;a href="https://prisminfoways.com/contact" rel="noopener noreferrer"&gt;Get a free consultation&lt;/a&gt; and see what fits your project.&lt;/p&gt;

</description>
      <category>mobileapp</category>
      <category>flutter</category>
      <category>reactnative</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why Every Growing Business Needs a Web Development Partner, Not Just a Website Vendor in 2026</title>
      <dc:creator>Prism Infoways Private Limited</dc:creator>
      <pubDate>Wed, 19 Aug 2026 05:50:12 +0000</pubDate>
      <link>https://dev.to/prisminfoways/why-every-growing-business-needs-a-web-development-partner-not-just-a-website-vendor-in-2026-59db</link>
      <guid>https://dev.to/prisminfoways/why-every-growing-business-needs-a-web-development-partner-not-just-a-website-vendor-in-2026-59db</guid>
      <description>&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%2F5l795h02n7yac2egzkk9.jpg" 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%2F5l795h02n7yac2egzkk9.jpg" alt=" " width="800" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Launching a new website is never as simple as people think. Getting the design right, keeping load times fast, ensuring mobile responsiveness, and nailing SEO — juggling all of this while still focusing on your core business is genuinely hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes Businesses Make Online
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Focusing only on design&lt;/strong&gt;&lt;br&gt;
 — A website should look good, but if it doesn't rank on Google, that design isn't doing much for your business.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Ignoring the mobile-first approach&lt;/strong&gt;&lt;br&gt;
 — Over 70% of traffic today comes from mobile. If your site is slow or broken on mobile, users bounce almost instantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Treating security as an afterthought&lt;/strong&gt;&lt;br&gt;
 — Cyber threats are increasing every year. Security needs to be built into the architecture from day one, not patched in later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Bolting SEO on at the end&lt;/strong&gt;&lt;br&gt;
 — SEO isn't something you add after launch; it should be baked into development from the start — clean code, fast load times, proper site structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Good Tech Partner Actually Does
&lt;/h2&gt;

&lt;p&gt;A strong IT partner doesn't just hand you code — they understand your business goals and build around them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scalable architecture&lt;/strong&gt; that grows with your business&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance optimization&lt;/strong&gt;&lt;br&gt;
for consistently fast load times&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SEO-friendly structure&lt;/strong&gt; to help you capture organic traffic&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ongoing support&lt;/strong&gt;&lt;br&gt;
so technical issues never block your business&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't limited to web development alone. App development, AI/ML integration, cloud infrastructure, and digital marketing are all part of the same ecosystem when you're aiming for real end-to-end digital transformation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the Right Partner
&lt;/h2&gt;

&lt;p&gt;When evaluating an IT or web development company, look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A solid &lt;strong&gt;portfolio&lt;/strong&gt; and genuine client reviews&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Whether they build &lt;strong&gt;custom solutions&lt;/strong&gt; or just rely on templates&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transparent pricing&lt;/strong&gt; and clear timelines&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Long-term support&lt;/strong&gt; after launch, not just a handoff&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Companies like &lt;a href="https://prisminfoways.com/" rel="noopener noreferrer"&gt;Prism Infoways&lt;/a&gt; operate across this entire space — web development, app development, AI solutions, cybersecurity, and digital marketing — giving businesses a single reliable partner instead of juggling multiple vendors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;If you're serious about building your digital presence in 2026, don't just think "I need a website." Find a tech partner who can grow and evolve alongside your business.&lt;/p&gt;

&lt;p&gt;Have you worked with a dedicated dev partner recently? Share your experience in the comments — what challenges did you run into?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>business</category>
      <category>startup</category>
      <category>seo</category>
    </item>
    <item>
      <title>Shopify vs WooCommerce vs Custom: How to Actually Choose for an Ecommerce Build</title>
      <dc:creator>Prism Infoways Private Limited</dc:creator>
      <pubDate>Sat, 08 Aug 2026 13:38:58 +0000</pubDate>
      <link>https://dev.to/prisminfoways/shopify-vs-woocommerce-vs-custom-how-to-actually-choose-for-an-ecommerce-build-2o2h</link>
      <guid>https://dev.to/prisminfoways/shopify-vs-woocommerce-vs-custom-how-to-actually-choose-for-an-ecommerce-build-2o2h</guid>
      <description>&lt;h2&gt;
  
  
  The Platform Question Gets Asked Backwards, Usually
&lt;/h2&gt;

&lt;p&gt;Most "Shopify vs WooCommerce vs custom" conversations start with a preference someone already has, and then look for reasons to justify it. A more useful starting point is the constraints of the actual project: catalog size, integration needs, budget, and who maintains the store after launch.&lt;br&gt;
Here's how each option actually behaves under those constraints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shopify: Optimized for Speed to Launch, Not Flexibility
&lt;/h2&gt;

&lt;p&gt;Shopify is a hosted, managed platform — you're renting infrastructure and a well-tested checkout flow in exchange for giving up some control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's the right call:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You need to launch fast and don't have in-house dev resources for ongoing platform maintenance&lt;/li&gt;
&lt;li&gt;Your catalog and business logic are fairly standard (products, variants, standard shipping/tax rules)&lt;/li&gt;
&lt;li&gt;You want PCI compliance, hosting, and uptime handled for you, not managed in-house&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it starts to hurt:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Highly custom checkout flows or non-standard business logic (complex B2B pricing tiers, unusual bundling rules) fight the platform&lt;/li&gt;
&lt;li&gt;Transaction fees stack up at scale unless you're on Shopify Payments&lt;/li&gt;
&lt;li&gt;You're paying a recurring platform fee indefinitely, on top of app costs for anything beyond core features
WooCommerce: More Flexibility, More Responsibility
WooCommerce is a WordPress plugin — self-hosted, open-source, and far more customizable at the code level, but that flexibility comes with more to manage.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it's the right call:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You want full control over hosting, plugins, and custom functionality without platform fees&lt;/li&gt;
&lt;li&gt;Your team (or dev partner) is comfortable managing WordPress security updates and plugin conflicts&lt;/li&gt;
&lt;li&gt;You want lower long-term recurring costs, accepting more upfront setup work&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it starts to hurt:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Plugin conflicts are a real, recurring maintenance cost — WooCommerce's flexibility comes from a large plugin ecosystem that doesn't always play well together&lt;/li&gt;
&lt;li&gt;You're responsible for hosting performance, security patching, and backups (or paying someone to be)&lt;/li&gt;
&lt;li&gt;Scaling a very large catalog can require more deliberate performance tuning than a hosted platform handles by default&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Custom Build: Full Control, Full Cost
&lt;/h2&gt;

&lt;p&gt;A fully custom platform means building the storefront, admin, and integrations from scratch (or from a headless commerce API like a custom backend with a decoupled frontend).&lt;br&gt;
Where it's the right call:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your business logic genuinely doesn't fit standard ecommerce patterns — complex pricing engines, unusual inventory rules, deep ERP/CRM integration&lt;/li&gt;
&lt;li&gt;You're building at a scale where platform transaction fees or plugin limitations become a real cost problem&lt;/li&gt;
&lt;li&gt;You need full control over performance and architecture (e.g., a headless setup for a highly custom frontend experience)
Where it starts to hurt:&lt;/li&gt;
&lt;li&gt;Cost and timeline are both significantly higher than the other two options&lt;/li&gt;
&lt;li&gt;You now own the full maintenance burden indefinitely — security, scaling, updates, everything&lt;/li&gt;
&lt;li&gt;It only pays off if the standard platforms genuinely can't serve your requirements — building custom because it "feels more professional" is usually a mistake
Realistic Cost and Timeline Ranges
These vary a lot by scope, but as a rough anchor for a mid-sized &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  business ecommerce build:
&lt;/h2&gt;

&lt;p&gt;Approach    Typical Cost Range  Typical Timeline&lt;br&gt;
Basic store (template-based)    Lower end, budget-friendly  2–3 weeks&lt;br&gt;
Standard store (custom design + integrations)   Mid-range   4–6 weeks&lt;br&gt;
Advanced/custom platform    Higher investment   8–12 weeks&lt;br&gt;
The biggest cost swing factor isn't usually the platform choice itself — it's the number of third-party integrations (payment gateways, shipping APIs, inventory/ERP systems) and how custom the design and business logic need to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Way to Decide
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;List your non-negotiable business logic first — pricing rules, integrations, workflows that can't be compromised on&lt;/li&gt;
&lt;li&gt;Check if Shopify or WooCommerce handles that logic natively or via a well-maintained plugin/app&lt;/li&gt;
&lt;li&gt;Only consider custom if the answer is genuinely no, not because custom sounds more impressive&lt;/li&gt;
&lt;li&gt;Factor in who maintains this after launch — a platform choice that's cheap to build but expensive to maintain isn't actually the cheap option
The right choice is almost always the one that matches your actual constraints, not the one with the most features or the most flexibility in the abstract.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This post is adapted from a longer guide covering full cost breakdowns and the development process step by step, originally published on the Prism Infoways blog: &lt;a href="https://prisminfoways.com/blog-single/ecommerce-website-development-company-gurugram-cost-process" rel="noopener noreferrer"&gt;https://prisminfoways.com/blog-single/ecommerce-website-development-company-gurugram-cost-process&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>webdev</category>
      <category>shopify</category>
      <category>wordpress</category>
    </item>
    <item>
      <title>5 Practical Ways to Cut Cloud Hosting Costs for Small Business Web Apps</title>
      <dc:creator>Prism Infoways Private Limited</dc:creator>
      <pubDate>Mon, 03 Aug 2026 06:53:53 +0000</pubDate>
      <link>https://dev.to/prisminfoways/5-practical-ways-to-cut-cloud-hosting-costs-for-small-business-web-apps-26ii</link>
      <guid>https://dev.to/prisminfoways/5-practical-ways-to-cut-cloud-hosting-costs-for-small-business-web-apps-26ii</guid>
      <description>&lt;p&gt;If you're running a small business web app on AWS or Azure, there's a good chance you're paying more than you need to — not because of bad architecture decisions, but because of defaults nobody ever revisited after the initial setup.&lt;/p&gt;

&lt;p&gt;Here are five things we've consistently found make the biggest difference when auditing cloud costs for small and mid-sized teams.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Right-size your instances (most teams over-provision by default)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It's common to spin up a t3.medium or m5.large "just to be safe" and never look back. Check actual CPU/memory utilization over a 2-week window using CloudWatch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudwatch get-metric-statistics &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--namespace&lt;/span&gt; AWS/EC2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metric-name&lt;/span&gt; CPUUtilization &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dimensions&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;InstanceId,Value&lt;span class="o"&gt;=&lt;/span&gt;i-xxxxxxxx &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start-time&lt;/span&gt; 2026-07-20T00:00:00Z &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end-time&lt;/span&gt; 2026-08-03T00:00:00Z &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--period&lt;/span&gt; 3600 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--statistics&lt;/span&gt; Average
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If average utilization is consistently under 20%, you're likely paying for capacity you don't use. Downsizing one tier often cuts compute costs by 30-40% with zero performance impact for typical CRUD apps.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Move static assets off compute instances entirely&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Serving images, CSS, and JS directly from your app server wastes compute resources on something a CDN does better and cheaper. Moving static assets to S3 + CloudFront (or equivalent) usually:&lt;/p&gt;

&lt;p&gt;Reduces origin server load&lt;br&gt;
Cuts bandwidth costs significantly at scale&lt;br&gt;
Improves page load times as a side benefit&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up auto-scaling instead of running peak capacity 24/7&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most small business apps have predictable traffic patterns — busy during business hours, quiet overnight. Running peak-capacity instances around the clock means paying full price for idle time.&lt;/p&gt;

&lt;p&gt;A basic auto-scaling group with scheduled scaling (not even reactive scaling) can cut costs meaningfully:&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="c1"&gt;# Example: scale down overnight, scale up for business hours&lt;/span&gt;
&lt;span class="na"&gt;ScheduledActions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ScheduledActionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scale-down-night&lt;/span&gt;
    &lt;span class="na"&gt;Recurrence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;22&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="na"&gt;MinSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
    &lt;span class="na"&gt;MaxSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ScheduledActionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scale-up-morning&lt;/span&gt;
    &lt;span class="na"&gt;Recurrence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
    &lt;span class="na"&gt;MinSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;MaxSize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Audit unused Elastic IPs, snapshots, and orphaned volumes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This one's boring but adds up fast. Unattached EBS volumes, old snapshots nobody deleted, and unused Elastic IPs quietly bill you every month. Run a quick audit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ec2 describe-volumes &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="nv"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;status,Values&lt;span class="o"&gt;=&lt;/span&gt;available
aws ec2 describe-addresses &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"Addresses[?AssociationId==null]"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We've seen accounts with 15-20% of their monthly bill coming from resources nobody was actively using.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reserved Instances or Savings Plans for predictable workloads&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your baseline load is stable (not bursty), on-demand pricing is the most expensive way to pay for it. A 1-year Compute Savings Plan typically saves 30-40% over on-demand for workloads you know aren't going anywhere.&lt;/p&gt;

&lt;p&gt;None of this requires a major re-architecture — it's mostly auditing what's already running and questioning defaults that were set once and forgotten. For most small business apps we've reviewed, steps 1, 2, and 4 alone typically recover 25-35% of monthly cloud spend without touching application code.&lt;/p&gt;

&lt;p&gt;Based on cloud cost audits done at &lt;a href="https://prisminfoways.com/" rel="noopener noreferrer"&gt;Prism Infoways,&lt;/a&gt; where we work with small and mid-sized businesses on cloud infrastructure and web development.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>aws</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
