<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Gaurav Sharma</title>
    <description>The latest articles on Forem by Gaurav Sharma (@gauravsharma_thetruecode).</description>
    <link>https://forem.com/gauravsharma_thetruecode</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3879660%2F99746783-8ac4-438d-805d-fe16dc8c7cb5.png</url>
      <title>Forem: Gaurav Sharma</title>
      <link>https://forem.com/gauravsharma_thetruecode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/gauravsharma_thetruecode"/>
    <language>en</language>
    <item>
      <title>Caching Is Easy. Production Caching Is Not.</title>
      <dc:creator>Gaurav Sharma</dc:creator>
      <pubDate>Mon, 20 Apr 2026 06:22:50 +0000</pubDate>
      <link>https://forem.com/gauravsharma_thetruecode/caching-is-easy-production-caching-is-not-59n9</link>
      <guid>https://forem.com/gauravsharma_thetruecode/caching-is-easy-production-caching-is-not-59n9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This post is part of the series &lt;a href="https://www.thetruecode.com/the-true-code-of-production-systems/" rel="noopener noreferrer"&gt;The True Code of Production Systems&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first time you add caching to a system, it feels like a superpower.&lt;/p&gt;

&lt;p&gt;One afternoon of work. Response times drop. Database load drops. The whole system breathes easier. You ship it, you move on, and somewhere in the back of your mind you file caching under "solved problems."&lt;/p&gt;

&lt;p&gt;That filing is the mistake.&lt;/p&gt;

&lt;p&gt;Because caching in production is not one decision. It is &lt;strong&gt;ten decisions&lt;/strong&gt;, and most teams only consciously make one of them: the performance one. The other nine happen by default, by accident, or not at all. And defaults in production have a way of becoming incidents.&lt;/p&gt;

&lt;p&gt;This article is about all ten. But before we get into them, let us look at a system where one of those defaults caused a real problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Booking System That Did Everything Right. Almost.
&lt;/h2&gt;

&lt;p&gt;A platform handles seat reservations for corporate training workshops. On a normal day it serves around two to three hundred requests per minute. The engineering team is small but experienced.&lt;/p&gt;

&lt;p&gt;Workshop availability data was cached in Redis with a TTL of sixty seconds. The reasoning was sound — availability changes only when someone books or cancels. Caching it for a minute seemed perfectly reasonable, and for months it worked exactly as designed.&lt;/p&gt;

&lt;p&gt;Then a well-known instructor announced a new batch of workshops on LinkedIn. The post got shared widely. Within minutes, several hundred users landed simultaneously to check availability and book seats.&lt;/p&gt;

&lt;p&gt;The cached availability keys for those workshops had expired &lt;strong&gt;seconds before the spike hit&lt;/strong&gt;. Every one of those hundreds of requests checked the cache, found a miss, and went directly to the database. The database — which had been handling 20–30 direct queries per minute — received several hundred simultaneous queries in a few seconds.&lt;/p&gt;

&lt;p&gt;Connection pool exhausted. Query times climbed from milliseconds to seconds. The application started timing out. Users saw errors. Some refreshed, which made it worse. &lt;strong&gt;The platform was effectively down for four minutes&lt;/strong&gt; during the highest-traffic window it had ever seen.&lt;/p&gt;

&lt;p&gt;The cache was there. Redis was running fine. The TTL was set. Everything was configured.&lt;/p&gt;

&lt;p&gt;Nobody had thought about what happens when a popular key expires at exactly the wrong moment.&lt;/p&gt;

&lt;p&gt;We will come back to this system after the ten points. By then you will know exactly what went wrong and what a one-line fix would have looked like.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Most Developers Think Caching Is
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;Cache the expensive query. Set a TTL. Use Redis. Done.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That mental model is not wrong. It is just incomplete. In production, every caching decision is simultaneously &lt;strong&gt;three other things&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;consistency decision&lt;/strong&gt; — data in cache may no longer reflect reality&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;reliability decision&lt;/strong&gt; — a cache misbehaving under load can damage the system it was meant to protect&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;cost decision&lt;/strong&gt; — the wrong caching setup charges you quietly, consistently, and across more than one bill line item&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most developers ship caching thinking only about performance. The other three dimensions show up later, usually at inconvenient moments, usually pointing back to a decision that was never consciously made.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Ten Things Production Caching Actually Requires
&lt;/h2&gt;




&lt;h3&gt;
  
  
  1. Your Caching Pattern Is a Choice. Make It Deliberately.
&lt;/h3&gt;

&lt;p&gt;Most developers use &lt;strong&gt;Cache Aside&lt;/strong&gt; without ever knowing they made a choice. The code checks the cache, finds a miss, goes to the database, stores the result, and returns it. It is the most common pattern. It works. But it is one of four — and each behaves differently in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache Aside&lt;/strong&gt; puts the application in charge. You decide when to read from cache and when to write to it. This gives you flexibility, but every invalidation is your responsibility. Miss one code path that updates the underlying data without clearing the cache, and you silently serve stale data. No error. No alert.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read Through&lt;/strong&gt; moves that responsibility elsewhere — the cache itself fetches from the database on a miss. This keeps application code clean but creates a &lt;strong&gt;cold start problem&lt;/strong&gt;: every fresh deployment begins with an empty cache, and until it warms up, your database absorbs full traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write Through&lt;/strong&gt; writes to both cache and database on every write. Your cache is always in sync — but every write now has to complete in two places before returning to the caller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write Behind&lt;/strong&gt; writes to cache immediately and updates the database asynchronously. Writes are very fast. But if the cache node goes down before the async write completes, &lt;strong&gt;that data is gone&lt;/strong&gt;. Unless you have consciously decided that some data loss is acceptable, this pattern is not the right one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Before you deploy, ask:&lt;/strong&gt; What is my consistency requirement? Can users tolerate stale data, and if so, for how long? Which pattern actually matches that requirement?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  2. Cache Invalidation: Why the Joke Is Not Actually a Joke
&lt;/h3&gt;

&lt;p&gt;The two hardest things in computer science are cache invalidation and naming things. Most people chuckle and move on. They should sit with it longer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTL-based invalidation&lt;/strong&gt; is what most systems use. Simple, easy to reason about, no inter-service coordination needed. The downside: TTL is a blunt instrument. Set it too long — users interact with stale data. Set it too short — you hammer the database repeatedly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event-based invalidation&lt;/strong&gt; is more precise. When the underlying data changes, you immediately delete or update the cache key. The challenge is coverage: &lt;strong&gt;every single code path&lt;/strong&gt; that can modify data must also trigger the invalidation. If you have five endpoints that update a user's profile and handle only four of them, you have a stale data bug that will appear random.&lt;/p&gt;

&lt;p&gt;The situation that quietly destroys production systems is &lt;strong&gt;mixing both approaches across services with no shared strategy&lt;/strong&gt;. Service A uses TTL. Service B uses events. Service C was written by a contractor six months ago. The cache becomes a state that no single person can fully reason about.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask yourself:&lt;/strong&gt; Who owns cache invalidation in my system? Is there an actual strategy, or is each service doing its own thing independently?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  3. The Cache Stampede: When Your Protection Collapses All at Once
&lt;/h3&gt;

&lt;p&gt;This one catches even experienced teams off guard.&lt;/p&gt;

&lt;p&gt;A popular cache key expires. At that exact moment, your system is handling high traffic. One thousand requests check the cache. All one thousand see a miss. &lt;strong&gt;All one thousand go directly to the database&lt;/strong&gt; to fetch the data and rebuild the cache. Your database — which the cache was there to protect — absorbs a spike it was never provisioned to handle alone.&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;cache stampede&lt;/strong&gt; (also called a &lt;em&gt;thundering herd&lt;/em&gt;). The irony: the more effective your cache, the worse the stampede when it fails.&lt;/p&gt;

&lt;p&gt;Three ways to protect against it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mutex / locking&lt;/strong&gt; — Only one request rebuilds a key at a time; others wait. Prevents the database spike but risks a queue buildup if the rebuild is slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probabilistic early expiration&lt;/strong&gt; — Before the TTL expires, the system starts refreshing the key using a probability function based on remaining TTL and rebuild cost. Hot keys effectively never go fully cold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background refresh&lt;/strong&gt; — A dedicated worker keeps popular keys warm by refreshing them proactively before they expire. The application never experiences a true miss.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask yourself:&lt;/strong&gt; What is peak concurrent traffic on my most accessed cache key? What happens to my database if that key expires right now, at this traffic level?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  4. Some Things Should Never Be Cached
&lt;/h3&gt;

&lt;p&gt;Knowing what &lt;strong&gt;not&lt;/strong&gt; to cache is equally important and almost never discussed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transactional or financial data&lt;/strong&gt; — account balances, order statuses, payment confirmations. If a user sees a balance that was accurate 30 seconds ago and makes a financial decision based on it, no performance gain justifies that. If stale data can cause a user to take a wrong action with real consequences, it should not be cached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Highly personalised responses&lt;/strong&gt; — the risk here is not performance. If your cache key does not capture every dimension that makes a response unique (user ID, role, tenant, locale, feature flags), you can serve one user's data to a completely different user. This has happened at companies of every size. The incident report always traces back to a cache key that was not specific enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Legally or contractually sensitive content&lt;/strong&gt; — terms and conditions, regulated pricing, compliance documentation. Serving an outdated version is not just a UX problem. Depending on the industry, it can carry legal weight.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask yourself:&lt;/strong&gt; If this cached value is served 60 seconds after it was written, what is the worst realistic outcome for the user receiving it?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  5. Your Eviction Policy Is a Decision, Not a Default
&lt;/h3&gt;

&lt;p&gt;Every cache has a memory ceiling. When it fills up, something gets removed. The question is whether that was a deliberate engineering choice or something that just happened because nobody changed the default.&lt;/p&gt;

&lt;p&gt;In Redis, the default eviction policy is &lt;code&gt;noeviction&lt;/code&gt; — when memory is full, &lt;strong&gt;Redis stops accepting writes and returns errors&lt;/strong&gt;. That is almost certainly not the behaviour you want under load. Many teams discover this only when they are already in an incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common strategies:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Removes&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LRU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Least recently accessed key&lt;/td&gt;
&lt;td&gt;Most general workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;LFU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Least frequently accessed key&lt;/td&gt;
&lt;td&gt;Workloads where long-term frequency matters more than recency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TTL-based&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Key closest to expiry&lt;/td&gt;
&lt;td&gt;Protecting long-lived data from short-lived displacement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask yourself:&lt;/strong&gt; Have you explicitly configured your eviction policy? When your cache fills up at peak load, what should be protected and what should go?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  6. The Cold Start Problem Nobody Prepares For
&lt;/h3&gt;

&lt;p&gt;You deploy a new version of your application. The new instance comes up with a &lt;strong&gt;completely empty cache&lt;/strong&gt;. For the first several minutes, every request is a miss. Every request goes to the database.&lt;/p&gt;

&lt;p&gt;In a low-traffic system, barely noticeable. In a high-traffic system — or one with a database already near capacity — those first few minutes can look exactly like an incident. By the time someone traces it to the deployment, the cache has warmed up. The post-mortem notes it as "transient."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Until the next deployment.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Three approaches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cache warming on startup&lt;/strong&gt; — Pre-populate your most-accessed keys before the new instance takes live traffic. Requires knowing your hot keys, which your observability setup should already surface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradual traffic shifting&lt;/strong&gt; — Old instances keep serving traffic with warm caches while new instances slowly build up state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sticky sessions during rollout&lt;/strong&gt; — Routes users to consistent instances temporarily, limiting how many cold instances are simultaneously exposed to real traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask yourself:&lt;/strong&gt; What does your system look like in the 5 minutes immediately after a fresh deployment? Have you ever deliberately tested it?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  7. Distributed Caching Is Not Just Single-Node Caching at Bigger Scale
&lt;/h3&gt;

&lt;p&gt;When you move to a distributed cache cluster, the rules change in ways that are easy to miss.&lt;/p&gt;

&lt;p&gt;Consider a write: your application updates cache node 1. Replication to node 2 is asynchronous and hasn't completed. Another request, routed to node 2, reads that key and gets the &lt;strong&gt;old value&lt;/strong&gt;. Two users, the same request, nearly the same moment — different responses.&lt;/p&gt;

&lt;p&gt;This is not a malfunction. It is the expected behaviour of an eventually consistent distributed system. The problem surfaces when the application is designed assuming &lt;strong&gt;strong consistency&lt;/strong&gt; and the cache is delivering &lt;strong&gt;eventual consistency&lt;/strong&gt;. That mismatch does not produce errors. It produces &lt;strong&gt;silent incorrectness&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Redis Cluster uses asynchronous replication. Under normal conditions, replication lag is milliseconds and practically invisible. But in failure scenarios — a node going down, a network partition, a failover — &lt;strong&gt;writes that were acknowledged can be lost&lt;/strong&gt; before they propagate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask yourself:&lt;/strong&gt; Has your application been designed knowing that cache reads across nodes may not always be consistent? What actually happens to your users if they are not?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  8. Security Gaps in Caching Are Invisible Until They Are Not
&lt;/h3&gt;

&lt;p&gt;Here is how it goes wrong. You cache a response containing data belonging to a specific user. A second user sends a request that generates the &lt;strong&gt;same cache key&lt;/strong&gt;. They receive the first user's cached response — their personal data, their account details, their private information — served silently to someone who should never have seen it.&lt;/p&gt;

&lt;p&gt;This is a data breach that produces &lt;strong&gt;no exception, no error log, and no anomaly in performance metrics&lt;/strong&gt;. The cache is working exactly as designed. The design is the problem.&lt;/p&gt;

&lt;p&gt;The fix requires rigorous cache key scoping. Every dimension that makes a response unique must be part of the key: user ID, tenant ID, permission level, role, locale, feature flags. Leaving any out is not a minor oversight to patch in the next sprint. It is a live security incident waiting for the right traffic pattern.&lt;/p&gt;

&lt;p&gt;The second concern: what lives in your cache &lt;strong&gt;at rest&lt;/strong&gt;. Session tokens, access tokens, PII embedded in cached API responses. Most teams apply strict access controls to their databases. Not all of them apply the same rigour to their cache infrastructure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask yourself:&lt;/strong&gt; Are your cache keys scoped precisely enough that no response can ever be served to the wrong user? If your cache infrastructure were accessed by someone who shouldn't have it, what would they find?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  9. If You Are Not Measuring Your Cache, You Do Not Know If It Is Working
&lt;/h3&gt;

&lt;p&gt;A cache you cannot observe is either working fine or silently failing — and you have no way to tell which.&lt;/p&gt;

&lt;p&gt;Three numbers tell you almost everything:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hit rate&lt;/strong&gt; — percentage of requests served directly from cache. A high, stable hit rate means the cache is doing its job. A rate slowly declining over days or weeks signals that data volatility has increased, TTLs have drifted, or a deployment changed behaviour upstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Miss rate&lt;/strong&gt; — how often requests fall through to the database. A sudden spike means a stampede may be in progress, an invalidation pipeline has broken, or a deployment started cold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eviction rate&lt;/strong&gt; — tells you whether your cache is sized correctly. A rising eviction rate means your working set is larger than your allocated memory. Data is being pushed out before it can be reused. Your hit rate follows downward. Your database load follows upward.&lt;/p&gt;

&lt;p&gt;Together, these three numbers tell a continuous story. Without them, you are managing critical infrastructure entirely on faith.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask yourself:&lt;/strong&gt; Can you pull up a live view of your cache hit rate, miss rate, and eviction rate right now? If not, that is the first thing to fix.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  10. The Cost Is Real, and It Compounds Quietly
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Under-provisioned cache:&lt;/strong&gt; High eviction rates reduce hit rate → more database load → more compute needed → higher costs across multiple services.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over-provisioned cache:&lt;/strong&gt; You pay for memory that sits idle. Managed Redis on any major cloud provider bills idle capacity at the same rate as active capacity.&lt;/p&gt;

&lt;p&gt;The right size comes from understanding your &lt;strong&gt;working set&lt;/strong&gt; — the total data your application actually reads within a given time window. If your working set is 15 GB and your cache is 4 GB, you are not caching 15 GB. You are repeatedly evicting and re-fetching 11 GB of it, paying for database round trips on every cycle.&lt;/p&gt;

&lt;p&gt;The other cost that accumulates quietly: &lt;strong&gt;data transfer&lt;/strong&gt;. If your application instances and cache cluster live in different availability zones, you pay for cross-zone traffic on every cache read. On a high-traffic system with a high hit rate, that is an enormous number of reads. The per-request cost is small. The monthly total is not.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ask yourself:&lt;/strong&gt; Have you sized your cache from a working set analysis or from a number someone estimated at the start of the project? Do you know what your cross-zone cache traffic costs per month?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Back to the Booking System
&lt;/h2&gt;

&lt;p&gt;Remember the platform that went down for four minutes? The cache was there. Redis was running. The TTL was set.&lt;/p&gt;

&lt;p&gt;What they had not done was think about the &lt;strong&gt;stampede&lt;/strong&gt; (point 3).&lt;/p&gt;

&lt;p&gt;The availability keys for those popular workshops all had the same sixty-second TTL, set at roughly the same time when the workshops were first published. So &lt;strong&gt;they all expired together&lt;/strong&gt;. When the traffic spike hit, every request found a cold cache simultaneously and went straight to the database.&lt;/p&gt;

&lt;p&gt;The fix was not complicated. A &lt;strong&gt;background worker refreshing availability keys for popular workshops every 45 seconds&lt;/strong&gt; would have kept those keys warm through the entire spike. The database would have seen normal traffic. Users would have seen normal response times.&lt;/p&gt;

&lt;p&gt;One decision. Not made. Four minutes down.&lt;/p&gt;

&lt;p&gt;That is what production caching actually looks like. Not a performance graph. &lt;strong&gt;A decision with a consequence.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Thing That Ties All of This Together
&lt;/h2&gt;

&lt;p&gt;Caching does not make your system faster.&lt;/p&gt;

&lt;p&gt;Done right, it does. Done wrong, it makes your system faster right up until the moment it does not. And when it fails, it tends to fail suddenly, in ways that are difficult to trace back to a decision made quietly, months earlier, on an ordinary afternoon.&lt;/p&gt;

&lt;p&gt;The engineers who build systems that hold up under real pressure are not necessarily smarter. They are more &lt;strong&gt;deliberate&lt;/strong&gt;. They treat each of these ten things as a conscious choice rather than something that gets handled by default.&lt;/p&gt;

&lt;p&gt;Make the choices. Write them down. Revisit them before you ship.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Ready Checklist
&lt;/h2&gt;

&lt;p&gt;Go through this before anything involving caching reaches production. Not as a formality — as a genuine engineering checkpoint.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Have I consciously chosen a caching pattern and do I understand its consistency trade-offs?&lt;/li&gt;
&lt;li&gt;[ ] Do I have a defined invalidation strategy with a clear owner, clear triggers, and handling for silent failures?&lt;/li&gt;
&lt;li&gt;[ ] Have I protected my hottest cache keys against a stampede event?&lt;/li&gt;
&lt;li&gt;[ ] Have I audited what I am caching and confirmed none of it is transactional, financial, or dangerous when stale?&lt;/li&gt;
&lt;li&gt;[ ] Have I explicitly configured my eviction policy rather than accepting the default?&lt;/li&gt;
&lt;li&gt;[ ] Have I planned and actually tested what happens in the first five minutes after a cold deployment?&lt;/li&gt;
&lt;li&gt;[ ] Do I understand my cache cluster's replication and consistency model, and has my application been designed with that in mind?&lt;/li&gt;
&lt;li&gt;[ ] Are my cache keys scoped precisely enough that no response can ever be served to the wrong user?&lt;/li&gt;
&lt;li&gt;[ ] Do I have live monitoring for hit rate, miss rate, and eviction rate?&lt;/li&gt;
&lt;li&gt;[ ] Have I sized my cache from a working set analysis and not from a rough estimate?&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on &lt;a href="https://www.thetruecode.com/the-true-code-of-production-systems/caching-is-easy-production-caching-is-not/" rel="noopener noreferrer"&gt;The True Code&lt;/a&gt; — a series on production-critical engineering, stack-agnostic, with enough depth to actually change how you think.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>redis</category>
      <category>backend</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Idempotency Is Not an API Thing: A Conversation Between Two Engineers</title>
      <dc:creator>Gaurav Sharma</dc:creator>
      <pubDate>Wed, 15 Apr 2026 04:52:13 +0000</pubDate>
      <link>https://forem.com/gauravsharma_thetruecode/idempotency-is-not-an-api-thing-a-conversation-between-two-engineers-15ii</link>
      <guid>https://forem.com/gauravsharma_thetruecode/idempotency-is-not-an-api-thing-a-conversation-between-two-engineers-15ii</guid>
      <description>&lt;p&gt;The junior engineer has been writing production code for three years.&lt;br&gt;
He knows what idempotency means. Or at least he thinks he does.&lt;br&gt;
He has used idempotency keys.&lt;br&gt;
He has read the Stripe documentation.&lt;br&gt;
He has nodded confidently in architecture reviews when someone said "make sure it's idempotent."&lt;br&gt;
He is reasonably sure he understands it.&lt;br&gt;
The senior engineer is about to ask one question that will change that.&lt;/p&gt;




&lt;h2&gt;
  
  
  The conversation starts with a definition that isn't quite right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So idempotency. It's basically when you send an idempotency key with an API request, right? The server checks if it's seen that key before. If yes, it returns the cached response. If no, it processes it fresh. That way, retries don't cause duplicate operations."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"That's one way to implement idempotency in an API. But it's not what idempotency is.&lt;/p&gt;

&lt;p&gt;Let me try something simpler. Think about an elevator button. When you're waiting for a lift and you press the button, it lights up. Now you press it again. And again. Does the lift come faster?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"No. It's already been called. Pressing it again doesn't change anything."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Exactly. That button is idempotent. You can press it once or a hundred times. The result is the same: the lift is coming. The extra presses don't create extra lifts. They don't undo the first press. They just do nothing on top of what's already been done.&lt;/p&gt;

&lt;p&gt;That's what idempotency means as a concept. An operation is idempotent if you can run it multiple times and the result is exactly the same as running it once.&lt;/p&gt;

&lt;p&gt;It's a property of an operation. Not a key. Not a header. Not something specific to APIs or HTTP. Just an answer to one question: if this runs again, does anything go wrong?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"But in practice, the way you make something idempotent is with idempotency keys. Right? That's the pattern."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"In APIs, that's one tool. But let me show you why the concept is bigger than that.&lt;/p&gt;

&lt;p&gt;You have a SQL job that runs every night at midnight. Its job is to take orders from a staging table and insert them into the main orders table. No API. No HTTP. No key anywhere.&lt;/p&gt;

&lt;p&gt;If that job runs twice tonight, what happens?"&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The junior pauses.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"If it just does a plain INSERT... every order gets inserted twice. Duplicate rows."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Right. So the job is not idempotent. And nobody wrote an idempotency key into it. Because developers don't usually think about SQL jobs the way they think about APIs.&lt;/p&gt;

&lt;p&gt;Which means most SQL jobs in most systems are quietly not idempotent. And nobody realises it until the job runs twice. Which it will, eventually."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;He lets that sit for a moment.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"This is what I want us to talk through today. Not idempotency as an API feature. Idempotency as a discipline. A way of thinking about every operation you build, regardless of what kind it is. Because more operations can run more than once than most engineers think."&lt;/p&gt;




&lt;h2&gt;
  
  
  When does something run more than once?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Okay. I get the concept. But when would something actually run more than once by accident? I'd expect that to be pretty rare."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"It's actually one of the most common things in production. Let me walk through some everyday situations.&lt;/p&gt;

&lt;p&gt;Your app calls an external payment API. The network is slow. After 30 seconds, your app gets no response and assumes it failed. So it retries. But the first call actually did succeed. The payment went through. Now it goes through again.&lt;/p&gt;

&lt;p&gt;Your scheduled Azure Function is set to run at 2 AM every night. But last night's run is still going because it hit a slow database query. At 2 AM tonight, a new run starts. Now two instances are running at the same time, both doing the same work.&lt;/p&gt;

&lt;p&gt;A developer runs a data migration script on a Friday to fix a production issue. On Monday, a second developer, not knowing about Friday's run, runs the same script again to double-check. The script runs twice.&lt;/p&gt;

&lt;p&gt;A message arrives in Azure Service Bus. Your consumer picks it up, starts processing it, and then the server crashes halfway through. Service Bus doesn't hear back from the consumer. After a few minutes, it assumes the message was never processed. So it puts the message back and delivers it again to a new consumer instance.&lt;/p&gt;

&lt;p&gt;A Kubernetes pod is running a background job. The cluster decides to move the pod to a different node. The pod is killed mid-job. Kubernetes starts it fresh on a new node. The job runs again from the beginning."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So these aren't edge cases. These are just... normal production situations."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Completely normal. Every one of those things happens regularly. And in every one of those situations, if your operation isn't idempotent, you get problems.&lt;/p&gt;

&lt;p&gt;Duplicate rows in the database. A customer charged twice. The same email sent twice. A counter that's off by one or ten. Or the worst kind: silent data corruption that nobody notices for three days, until a customer calls support."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"And the customer doesn't know any of this happened. They just see the wrong charge on their statement."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Exactly. And your support team has to investigate manually, trace through logs, figure out what ran when, and apologise. All of that cost comes from one missing design decision made before the feature was built."&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenario 1: The payment API, and what happens when the key isn't enough
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Let's start where you're most familiar. A REST API for placing an order and charging a card. You said you'd use an idempotency key. Walk me through how that works."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"The client, meaning the front end or the calling service, generates a unique ID before making the request. Usually a UUID, something like '7f3d2c1a-...'. It sends that in the request header. When the server receives the request, it checks a table: have I seen this key before? If yes, return the response I stored last time. If no, process the order and save the response against this key."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Good. That's the right idea. Now let me walk through one specific failure scenario and I want you to tell me what happens.&lt;/p&gt;

&lt;p&gt;Your server receives the request. It checks the key: not seen before, so it starts processing. It calls the payment gateway. The gateway charges the card successfully. But then, before your server can write the order record to the database and save the idempotency key, the server crashes. Power cut, out of memory, doesn't matter. It just dies.&lt;/p&gt;

&lt;p&gt;The client gets a timeout. No response. What does the client do?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"It retries. With the same idempotency key."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Your server restarts. The new request arrives. It checks the key table. Does the key exist?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"No. Because we never saved it. We crashed before that step."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So what does the server do?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"It treats the request as new. It calls the payment gateway again."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The junior goes quiet for a second.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;"The customer gets charged twice."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Yes. And no error happened anywhere. Every individual step worked correctly. The payment gateway did its job. The retry logic did its job. The idempotency check did its job. But the order of operations was wrong, and the whole thing still failed the customer.&lt;/p&gt;

&lt;p&gt;This is the gap most developers miss. The key only works if you save it as part of the same operation as the work. Not before. Not after. Together. If your database write and your key storage are not in the same transaction, there's a window where a crash leaves you in the worst possible state: work done, but no record of it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So the idempotency key is only as safe as the transaction around it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Yes. And there's a second problem that's just as common. The client sends the same key, but the server isn't crashed. It's just slow. The first request is still being processed. The client gets impatient and retries.&lt;/p&gt;

&lt;p&gt;Now two requests with the same key are being processed at the same time. What does your server return to the second one?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"I don't know. Maybe a 500 error? Or it just waits?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Most servers return something unhelpful there. The correct answer is a 409 Conflict. A response that says, in plain terms: 'I've already seen this key and I'm still working on it. Wait a moment and try again.'&lt;/p&gt;

&lt;p&gt;Or if your operation is asynchronous, meaning it takes a long time and runs in the background, you return a 202 Accepted with a link the client can check to see the status.&lt;/p&gt;

&lt;p&gt;The point is: an idempotency key isn't just a deduplication trick. It changes what your system has to communicate in every possible situation. Seen the key and done the work: return the stored result. Seen the key and still working: say so. Never seen the key: do the work. Each case needs a clear, intentional answer."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"I've never thought about the 'still working' case. I just assumed the system would either have done it or not."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Most developers haven't. And one more thing. How long do you keep the keys?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"I... haven't really decided. Until the row gets cleaned up, I suppose."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"That's the answer most systems give. Which means some systems keep them forever until the database gets large, and some delete them too early, which means a retry that comes in four hours later looks like a brand new request.&lt;/p&gt;

&lt;p&gt;Stripe keeps idempotency keys for 30 days because that's long enough to cover any realistic retry window for a payment. Most internal systems don't need 30 days. But they need a number. A deliberate decision. Not a default that nobody chose."&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenario 2: The nightly SQL job that nobody worries about
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Let's go back to the SQL job example. You said most of them aren't idempotent. How do you actually fix that?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"First, let's be very concrete about the problem. Imagine your company runs an Azure Data Factory pipeline every night at midnight. It reads from a staging table where raw transaction data lands throughout the day, and it inserts those transactions into a clean fact table that the reporting team uses.&lt;/p&gt;

&lt;p&gt;On a normal night, it runs once. Everything is fine. But one night there's a network blip halfway through and the pipeline fails. The on-call engineer sees the alert and reruns it manually. Now the pipeline runs again from the beginning. What happens to the rows that already got inserted in the first partial run?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"They get inserted again. Duplicate rows in the fact table."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"And the reporting team runs their reports the next morning not knowing any of this happened. The numbers are wrong. Maybe slightly wrong, maybe very wrong depending on how far the first run got. And tracing it back is painful.&lt;/p&gt;

&lt;p&gt;The fix is to change the question the job asks. Instead of 'insert this data', it should ask 'make this data exist.' There's a big difference.&lt;/p&gt;

&lt;p&gt;A plain INSERT says: add this row, I don't care if it's already there. An upsert, or a MERGE in SQL terms, says: if this row already exists, update it to match. If it doesn't exist, create it. Either way, when you're done, the data looks exactly like it should.&lt;/p&gt;

&lt;p&gt;Run that job once: correct state. Run it ten times: same correct state. The job is now idempotent."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"But to do a MERGE, you need some way to recognise whether a row already exists. Like a unique ID to match on."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Exactly. And this is where the design conversation starts. Idempotency requires identity. To know whether you've already done something, you need a reliable way to recognise that thing when you see it again.&lt;/p&gt;

&lt;p&gt;For an order, that's probably an order ID from the source system. For a transaction, maybe a combination of the transaction reference and the date. For an event log, maybe a hash of the key fields.&lt;/p&gt;

&lt;p&gt;The point is: if your data model has no natural key, idempotency becomes much harder. This is a design decision you make early. And if you don't make it deliberately, production will eventually make it for you, in the worst possible way."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So idempotency isn't just about the job. It starts with the data model."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Yes. And there's a trap I see often that looks safe but isn't. A developer writes something like: check if this row already exists, and if not, insert it. Sounds fine. But what if two instances of the job run at the same time? Both do the check. Both find no existing row. Both try to insert. You get duplicate rows anyway.&lt;/p&gt;

&lt;p&gt;The check-then-insert pattern only works if exactly one thing is running at a time, which you often can't guarantee. The database's own uniqueness constraint, combined with an upsert operation, is the only way to get a guarantee that holds under any conditions."&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenario 3: The console app that runs every 15 minutes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"What about a background worker? Like a console app or a WebJob that runs on a schedule?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Good example. Let's say you have an Azure WebJob that runs every 15 minutes. Its job is to pick up new customer records, call an external enrichment API to add extra details, and write the enriched records to Azure Blob Storage.&lt;/p&gt;

&lt;p&gt;Two problems can happen here, and neither of them feels like a bug at first.&lt;/p&gt;

&lt;p&gt;Problem one: the job takes 16 minutes. A slow response from the enrichment API. By the time it finishes, the next scheduled run has already started. Now two instances are running at the same time, processing the same batch of records. Neither knows the other exists."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"They'd both write to the same blobs. One would overwrite the other."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Maybe. Or they'd both call the enrichment API for the same customer, getting billed twice for that API call. Or one finishes first and marks the record as done, but then the second finishes and marks it done again with slightly different data because the API returned something different the second time around.&lt;/p&gt;

&lt;p&gt;Problem two: the job processes a record, writes the blob, then crashes before marking that record as done. Next run, it picks up the same record again and runs the whole thing over."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"For the second problem, if the blob gets overwritten with the same data, isn't that fine?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Only if the enrichment API always returns identical data for the same input. If it returns a price, a stock level, a timestamp, anything that can change between calls, then the second write might have different data. Now your system has processed the same record twice and stored two different results, one of which got silently overwritten.&lt;/p&gt;

&lt;p&gt;You might never notice. Until someone asks why customer records from a specific period look inconsistent."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So how do you stop the overlap problem? The two instances running at the same time?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"A distributed lease. Before the job starts its work, it tries to claim a lock on a shared resource. In Azure, you can use a Blob Storage lease for this. Think of it like a physical key to a room. Only one person can hold the key at a time. The job picks up the key before it starts. If another instance tries to start and finds the key already taken, it simply exits. It doesn't fight. It doesn't wait. It just walks away.&lt;/p&gt;

&lt;p&gt;When the first job finishes, it releases the key. The next scheduled run picks it up normally.&lt;/p&gt;

&lt;p&gt;One run at a time. Clean."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So in this case, the solution isn't making the operation idempotent. It's preventing the duplication from happening at all."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Right. And that's an important distinction. Idempotency means tolerating duplication. Prevention means eliminating it. Both are valid. Often you want both: prevent where you can, tolerate where you can't. The blob lease prevents concurrent runs. The upsert write tolerates the occasional restart where the same record gets processed twice."&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenario 4: Message queues and the guarantee that surprises people
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Message queues. I know at-least-once delivery means a message might arrive more than once. So idempotency matters there."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Yes. But I want to make sure the 'at-least-once' part is clear, because a lot of developers hear it and think 'that's an edge case, it rarely happens.'&lt;/p&gt;

&lt;p&gt;It's not an edge case. Azure Service Bus guarantees that a message will be delivered. It does not guarantee it will only be delivered once. The reason is: to know that a message was truly processed, Service Bus needs the consumer to send back an acknowledgement. If the consumer processes the message and then crashes before sending that acknowledgement, Service Bus has no idea the work was done. So it re-delivers the message. It has to. The alternative is losing the message entirely, which is worse.&lt;/p&gt;

&lt;p&gt;So duplicates aren't a bug. They're the price of reliability. And your consumer has to be built with that in mind."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Doesn't Service Bus have duplicate detection built in though? I've seen a setting for it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"It does. And this is a common source of false confidence. Service Bus can detect if the exact same message is delivered twice within a time window, based on the message ID. That covers broker-level duplicates, situations where the broker itself sends the same message twice.&lt;/p&gt;

&lt;p&gt;But it doesn't cover the scenario I just described. If your consumer crashes after processing but before acknowledging, Service Bus delivers the message again, but from its perspective, that's a legitimate re-delivery of a message that was never confirmed, not a duplicate. The duplicate detection won't catch it.&lt;/p&gt;

&lt;p&gt;Your consumer needs to handle it."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So how do you make a consumer handle it correctly?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"The cleanest approach depends on what your consumer does.&lt;/p&gt;

&lt;p&gt;If your consumer is writing to a database and there's a natural business key on the record, like an order ID, just use an upsert. Write the record if it doesn't exist, update it if it does. Processing the same message ten times leaves you with exactly one record in the correct state. No extra infrastructure needed.&lt;/p&gt;

&lt;p&gt;If your consumer has side effects beyond a database write, like sending an email or calling a payment gateway, you need to track what you've already done. Before processing a message, check whether that message has already been processed successfully. Azure Cache for Redis works well here: store the message's business ID with a short expiry. If it's already there, skip the processing and just acknowledge the message. Simple check before every action.&lt;/p&gt;

&lt;p&gt;The key choice is which ID to track. Service Bus gives every message its own message ID, which is an infrastructure concept. But your message also contains a business concept, an order number, a customer ID, a transaction reference. Use that as your deduplication key. It's the thing that actually means something to your system, and it survives across retries and redeliveries in a way that infrastructure IDs sometimes don't."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"How do you decide which approach to use?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Ask one question: what is the cost if this runs twice?&lt;/p&gt;

&lt;p&gt;If the cost is nothing, the operation is naturally safe, just use an upsert and move on.&lt;/p&gt;

&lt;p&gt;If the cost is money, like a payment, or trust, like a notification, or anything the customer will notice, then you need the explicit check.&lt;/p&gt;

&lt;p&gt;The answer to that question tells you exactly how much effort to invest."&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenario 5: Azure Functions, serverless, and why state can't live in memory
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"What about Azure Functions? The HTTP-triggered kind."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Azure Functions are a great example of why understanding idempotency as a concept matters more than knowing any specific implementation.&lt;/p&gt;

&lt;p&gt;Here's what makes Functions different. A regular web app might run as one or two instances. You might even be able to pretend it's a single server in some situations. A Function can scale to dozens or hundreds of instances in seconds. If 500 users all click the same button at the same time, 500 separate Function instances could all be handling those requests simultaneously.&lt;/p&gt;

&lt;p&gt;Each instance is completely isolated. It has no memory of what other instances are doing. It doesn't know if another instance is already processing the exact same request that came in twice due to a network retry."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So you can't store 'have I seen this request before' in memory. Because memory is per-instance."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Exactly. The only place where truth can live is somewhere that all instances can read from and write to. A database. Azure Table Storage. Redis. Something external and shared.&lt;/p&gt;

&lt;p&gt;The logic is the same as what we discussed before: when a request arrives, check a shared store for that idempotency key. If it exists, return the stored result without doing the work again. If it doesn't exist, do the work, save the result and the key to the shared store, and return.&lt;/p&gt;

&lt;p&gt;The extra question with Functions is concurrency. What if two instances receive the same request at almost the same moment, both check the store, both find no key, and both start processing?&lt;/p&gt;

&lt;p&gt;You let the database handle that. If you're using Azure Table Storage, it has optimistic concurrency built in. Only one write will succeed when two try to insert the same key at the same time. The second one gets a conflict error. At that point your Function catches the conflict, re-reads the stored result from the first instance, and returns it. Clean."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So the Function itself doesn't need to be complicated. The data layer does the heavy lifting."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Yes. And that's the general principle in serverless: compute is cheap and disposable. Data is where guarantees live. Your idempotency design has to be in the data layer, not the compute layer. Functions just execute whatever logic you give them. They don't remember anything between invocations unless you build that memory into storage."&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this all matters beyond just preventing duplicates
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So we've gone through APIs, SQL jobs, console apps, queues, and Functions. I understand the problem better now. But what's the bigger payoff? Why invest in this properly?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Let me ask you something. When you're testing a feature, what makes testing hard?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Writing tests for all the different things that can go wrong. Error cases, edge cases, unexpected sequences of events."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Right. Now think about retry scenarios specifically. If you have an operation that isn't idempotent, you need test cases for: what if the client retried once? What if it retried three times? What if two retries overlapped? What if the first attempt half-succeeded and then the retry came in?&lt;/p&gt;

&lt;p&gt;Each of those is a separate test scenario. Each one requires setup, assertions, and maintenance.&lt;/p&gt;

&lt;p&gt;If the operation is idempotent, all of those scenarios collapse into one: does the operation produce the correct result? You don't care how many times it ran. The result is always the same.&lt;/p&gt;

&lt;p&gt;That's a real, measurable reduction in test surface. Fewer tests to write. Fewer tests to maintain. Fewer bugs that come from test gaps."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"And when something goes wrong in production, what changes?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"This is where it matters most for your day-to-day life as an engineer.&lt;/p&gt;

&lt;p&gt;When something isn't idempotent and it fails, your recovery process is: investigate what ran, figure out what got into the database and what didn't, write a script to fix the inconsistency, test the script, run the fix, verify the result, and update the customer. That process takes hours. Sometimes days if the failure was subtle.&lt;/p&gt;

&lt;p&gt;When something is idempotent and it fails, your recovery process is: run it again. That's it. It takes minutes. And you can do it with confidence because you know that running it again will leave the system in the correct state, not make things worse."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So idempotency changes your 3 AM incident from 'I need to figure out what happened and carefully fix the data' to 'I just rerun the job and go back to sleep.'"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Yes. And it changes things beyond incidents too.&lt;/p&gt;

&lt;p&gt;Imagine you need to replay six months of transactions through a new processing pipeline you just built. If your pipeline is idempotent, you just run the data through and whatever already exists gets updated to match, whatever's missing gets created. No risk.&lt;/p&gt;

&lt;p&gt;If your pipeline isn't idempotent, replaying data means duplicates everywhere. You have to build cleanup logic before you can even start. What should be a straightforward migration becomes a careful, scary operation.&lt;/p&gt;

&lt;p&gt;Idempotency turns reruns from something you fear into something you can do without thinking."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"You mentioned earlier that idempotency is also a contract with your callers. Can you say more about that?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Every system you build is used by other systems. Other services call your API. Other jobs consume your queue. Other pipelines read your output.&lt;/p&gt;

&lt;p&gt;When those callers hit an error or a timeout, they have to decide: do I retry? If your operation is idempotent, the answer is always yes. Retry as many times as you need. You won't cause any harm.&lt;/p&gt;

&lt;p&gt;If your operation is not idempotent, the answer is: maybe. It depends on what stage the previous request reached. The caller now has to write complicated state-tracking logic to figure out whether it's safe to retry. Their code gets more complex because your design didn't make a guarantee.&lt;/p&gt;

&lt;p&gt;Most teams never write down which operations are idempotent and which aren't. Callers guess. When they guess right, nothing bad happens. When they guess wrong, you get an incident that looks mysterious until someone traces through logs for two hours and realises a retry caused a double charge.&lt;/p&gt;

&lt;p&gt;One sentence in your documentation, 'this endpoint is idempotent, it is safe to retry with the same key', prevents that entire category of problem. Good engineering is also good communication. They're the same thing."&lt;/p&gt;




&lt;h2&gt;
  
  
  The one question to ask before you build anything
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"If I take one thing from this conversation and apply it to every new piece of work I do, what should it be?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Before you build any operation, ask: can this run more than once?&lt;/p&gt;

&lt;p&gt;Not 'will it.' Because the answer to 'will it' is often 'probably not.' The answer to 'can it' is almost always 'yes.'&lt;/p&gt;

&lt;p&gt;Networks are unreliable. Servers crash. Deployments overlap. Developers rerun scripts. Queues redeliver messages. Schedulers fire twice. In the real world, any operation that can run once will, at some point, run more than once.&lt;/p&gt;

&lt;p&gt;So once you've accepted that, the question becomes: if it runs twice, what happens?&lt;/p&gt;

&lt;p&gt;If the answer is 'nothing bad,' you're fine.&lt;/p&gt;

&lt;p&gt;If the answer is 'duplicates,' 'double charges,' 'inconsistent state,' or 'I'm not sure,' then idempotency is not an optional nice-to-have. It's a requirement. And the time to think about it is before you build, not after production finds the problem for you."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"So it's not a pattern you add on top. It's a question you answer during design."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"Yes. And one more thing worth remembering.&lt;/p&gt;

&lt;p&gt;Most bugs in distributed systems aren't caused by failures. They're caused by successful operations that ran more than once."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;He closes his laptop.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;"A failure is visible. You get an error. An alert fires. A log entry appears. You know something went wrong and you go fix it.&lt;/p&gt;

&lt;p&gt;An operation that succeeds twice is invisible. No error. No alert. No log that says anything is wrong. Just a customer who checks their statement and finds two charges. Or a report that shows slightly wrong numbers. Or an email that went to 50,000 people twice.&lt;/p&gt;

&lt;p&gt;The damage is quiet. And you find it when someone complains, not when your monitoring catches it. Because your monitoring is watching for failures. And this wasn't a failure. It was a success. Twice."&lt;/p&gt;




&lt;p&gt;Idempotency is not something you add to an API when Stripe tells you to.&lt;br&gt;
It's not a checkbox in a design review.&lt;br&gt;
It's not something senior engineers think about and junior engineers don't.&lt;br&gt;
It's a question. One question, asked before every operation you design:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If this runs again, what happens?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Answer that question clearly, in SQL jobs, in background workers, in message consumers, in serverless Functions, in every place code runs that can run more than once, and a whole category of production incident quietly stops happening.&lt;/p&gt;

&lt;p&gt;Not because anything is perfect. But because you designed for the world as it actually is.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write at &lt;a href="https://www.thetruecode.com" rel="noopener noreferrer"&gt;thetruecode.com&lt;/a&gt; about real lessons from production systems, engineering teams, and 20 years of making things work under pressure.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Let's connect on &lt;a href="https://www.linkedin.com/in/gaurav-sharma-unfiltered" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>beginners</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
