<?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: Imran Ahmed</title>
    <description>The latest articles on DEV Community by Imran Ahmed (@developerimranahmed).</description>
    <link>https://dev.to/developerimranahmed</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%2F4119738%2Fcd8a089f-8062-4300-81c1-f84205b87d26.jpg</url>
      <title>DEV Community: Imran Ahmed</title>
      <link>https://dev.to/developerimranahmed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/developerimranahmed"/>
    <language>en</language>
    <item>
      <title>When Your Background Job Retries Are Silently Stepping on Each Other</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Wed, 07 Oct 2026 12:05:09 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/when-your-background-job-retries-are-silently-stepping-on-each-other-12l8</link>
      <guid>https://dev.to/developerimranahmed/when-your-background-job-retries-are-silently-stepping-on-each-other-12l8</guid>
      <description>&lt;h1&gt;
  
  
  When Your Background Job Retries Are Silently Stepping on Each Other
&lt;/h1&gt;

&lt;p&gt;In .NET backend development, we rely heavily on background jobs for asynchronous processing: sending emails, generating reports, or processing orders. We use libraries like Hangfire, Quartz, or custom &lt;code&gt;IHostedService&lt;/code&gt; implementations to ensure resilience.&lt;/p&gt;

&lt;p&gt;A common pattern is to wrap database operations in a &lt;code&gt;try-catch&lt;/code&gt; block that catches transient exceptions (like &lt;code&gt;SqlException&lt;/code&gt; for deadlocks or timeouts) and retries the operation. The assumption is usually that the code is idempotent—running it twice shouldn’t change the result.&lt;/p&gt;

&lt;p&gt;But what happens when "idempotent" isn't quite true? What happens when two &lt;em&gt;different&lt;/em&gt; jobs, retrying due to transient failures, both attempt to modify the same shared resource?&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Overwrite
&lt;/h2&gt;

&lt;p&gt;Let’s look at a scenario. We have an &lt;code&gt;OrderService&lt;/code&gt; that processes customer orders. When an order is placed, a background job is triggered to update the &lt;code&gt;Inventory&lt;/code&gt; table.&lt;/p&gt;

&lt;p&gt;Suppose two orders are placed almost simultaneously:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Order 1&lt;/strong&gt; for Product X (Quantity 5).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Order 2&lt;/strong&gt; for Product X (Quantity 3).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both jobs start almost at the same time.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Job 1 reads &lt;code&gt;Inventory&lt;/code&gt; row for Product X. Current stock: 10.&lt;/li&gt;
&lt;li&gt; Job 2 reads &lt;code&gt;Inventory&lt;/code&gt; row for Product X. Current stock: 10.&lt;/li&gt;
&lt;li&gt; Job 1 calculates new stock: 10 - 5 = 5.&lt;/li&gt;
&lt;li&gt; Job 2 calculates new stock: 10 - 3 = 7.&lt;/li&gt;
&lt;li&gt; Job 1 tries to write &lt;code&gt;5&lt;/code&gt; to the DB. It hits a transient network blip and the write fails.&lt;/li&gt;
&lt;li&gt; The retry mechanism kicks in for Job 1. It re-executes the logic. It reads the inventory again? &lt;strong&gt;No.&lt;/strong&gt; In many implementations, the entity was already loaded into memory. The retry just re-sends the &lt;code&gt;UPDATE&lt;/code&gt; statement with the stale value it calculated earlier (5).&lt;/li&gt;
&lt;li&gt; Job 2 successfully writes &lt;code&gt;7&lt;/code&gt; to the DB.&lt;/li&gt;
&lt;li&gt; Job 1’s retry finally succeeds and writes &lt;code&gt;5&lt;/code&gt; to the DB.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; The stock is now 5. But it should be 2 (10 - 5 - 3). Job 1’s retry silently overwrote Job 2’s valid change. This is a "lost update" caused by retry mechanics stepping on each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Tests Miss This
&lt;/h2&gt;

&lt;p&gt;Standard unit tests usually mock the database or test in isolation. You might test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  "Given stock 10, order 5, result is 5." (Pass)&lt;/li&gt;
&lt;li&gt;  "Given stock 10, order 3, result is 7." (Pass)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But you rarely test: "Given two concurrent jobs with a transient failure on the first, does the second job’s data survive the first job’s retry?"&lt;/p&gt;

&lt;p&gt;Simulating this in integration tests is difficult. You’d need to inject a delay or a specific exception during the save of the first job while the second job is proceeding. Most developers skip this complexity, leaving the bug to production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Optimistic Concurrency
&lt;/h2&gt;

&lt;p&gt;The robust solution is to use &lt;strong&gt;optimistic concurrency control&lt;/strong&gt;. Instead of blindly writing, we ask the database: "Did anything else change this row while I was away?"&lt;/p&gt;

&lt;p&gt;In SQL Server, we use the &lt;code&gt;rowversion&lt;/code&gt; data type. It’s a system-generated number that increments every time the row is updated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Add a Row Version Column
&lt;/h3&gt;

&lt;p&gt;In your EF Core model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InventoryItem&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ProductName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Use RowVersion or Timestamp&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;RowVersion&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&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;p&gt;In your SQL Server database, ensure you have a &lt;code&gt;rowversion&lt;/code&gt; column. If you’re using SQL Server, you can just add &lt;code&gt;ROWVERSION&lt;/code&gt; to the table definition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Implement Idempotent Retry Guards
&lt;/h3&gt;

&lt;p&gt;When you retry a job, you must ensure that the state you’re writing is still valid. With &lt;code&gt;rowversion&lt;/code&gt;, EF Core automatically adds a &lt;code&gt;WHERE&lt;/code&gt; clause to your &lt;code&gt;UPDATE&lt;/code&gt; statements.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In your service&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ProcessOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InventoryItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FindAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&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="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stock&lt;/span&gt; &lt;span class="p"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;quantity&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&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="n"&gt;DbUpdateConcurrencyException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// This is thrown if the RowVersion has changed.&lt;/span&gt;
        &lt;span class="c1"&gt;// It means another job updated this row.&lt;/span&gt;
        &lt;span class="c1"&gt;// Instead of overwriting, we should fetch the latest state and re-calculate.&lt;/span&gt;
        &lt;span class="c1"&gt;// Or, in the context of a simple deduction, we might just let it fail &lt;/span&gt;
        &lt;span class="c1"&gt;// and let the queue re-enqueue the job.&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Single&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;databaseValues&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OriginalValues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StoreGeneratedValuesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Logic to handle the conflict. &lt;/span&gt;
        &lt;span class="c1"&gt;// For inventory, you might need to re-read and check if the stock &lt;/span&gt;
        &lt;span class="c1"&gt;// is still sufficient, or if the order has already been processed.&lt;/span&gt;

        &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Let the retry mechanism handle it, or log and fail fast.&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;By catching &lt;code&gt;DbUpdateConcurrencyException&lt;/code&gt;, you prevent the silent overwrite. If Job 1 retries and finds that Job 2 has already updated the &lt;code&gt;rowversion&lt;/code&gt;, the &lt;code&gt;UPDATE&lt;/code&gt; affects zero rows. The exception is thrown, and you know the state is stale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Make the Business Logic Idempotent
&lt;/h3&gt;

&lt;p&gt;Retries alone aren’t enough. Your code must handle the "already processed" state. If Job 1 is retried and the &lt;code&gt;rowversion&lt;/code&gt; matches (meaning no one else touched it), it’s safe to re-apply. But if it doesn’t match, you must check: "Has this specific order already been deducted?"&lt;/p&gt;

&lt;p&gt;Often, this means adding a &lt;code&gt;ProcessedOrderIds&lt;/code&gt; table or a flag on the order itself, and checking it before performing the calculation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;Concurrency bugs are not about your code being "wrong" in the happy path. They’re about your code failing to handle the &lt;em&gt;overlap&lt;/em&gt; between retries and other active writers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Never assume&lt;/strong&gt; that a retry after a transient error is safe without checking the current database state.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Use &lt;code&gt;rowversion&lt;/code&gt;&lt;/strong&gt; (or similar mechanisms like &lt;code&gt;updated_at&lt;/code&gt; with application-level checks) for any shared mutable resources.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Design for conflict.&lt;/strong&gt; When a concurrency exception is thrown, your job should either re-read the state and retry the business logic, or fail fast so the queue can re-process it with fresh data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your background jobs should be resilient to failures, but they shouldn’t be destructive to data integrity.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>dotnet</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Why your idempotent POST endpoint is still creating duplicates under load</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Tue, 06 Oct 2026 12:05:52 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/why-your-idempotent-post-endpoint-is-still-creating-duplicates-under-load-27od</link>
      <guid>https://dev.to/developerimranahmed/why-your-idempotent-post-endpoint-is-still-creating-duplicates-under-load-27od</guid>
      <description>&lt;h1&gt;
  
  
  Stop Losing Data: Why Your Idempotent POST Endpoint is Still Creating Duplicates
&lt;/h1&gt;

&lt;p&gt;If you have a .NET API that accepts POST requests, you’ve likely added idempotency keys to handle retries gracefully. It’s a standard pattern: client sends a key, server checks if it’s been seen before, and if not, processes the request.&lt;/p&gt;

&lt;p&gt;The common implementation looks something like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check Redis/MemoryCache for the key.&lt;/li&gt;
&lt;li&gt;If it exists, return the cached response.&lt;/li&gt;
&lt;li&gt;If it doesn’t exist, process the business logic.&lt;/li&gt;
&lt;li&gt;Save the result to Redis.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This feels safe. But under real-world load, especially with load balancer timeouts or aggressive client-side retries, this pattern creates a race condition that results in duplicate data. In this article, we’ll look at why this happens and how to fix it using SQL Server constraints and proper exception handling in ASP.NET Core.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Check-Then-Act" Race Condition
&lt;/h2&gt;

&lt;p&gt;Let’s visualize the failure. Imagine two identical HTTP requests hit your API endpoint &lt;code&gt;POST /orders&lt;/code&gt; at &lt;code&gt;10:00:00.000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request A:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks Redis for key &lt;code&gt;ord-123&lt;/code&gt;. Result: &lt;strong&gt;Miss&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Starts processing business logic (takes 20ms).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Request B:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks Redis for key &lt;code&gt;ord-123&lt;/code&gt;. Result: &lt;strong&gt;Miss&lt;/strong&gt;. (Request A hasn't finished writing to Redis yet).&lt;/li&gt;
&lt;li&gt;Starts processing business logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Request A:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Completes logic. Writes order to SQL Server. Writes key to Redis. Returns 201 Created.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Request B:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Completes logic. Writes order to SQL Server. &lt;strong&gt;Duplicate!&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both requests believe they are the "first" one because the cache check was non-atomic. The cache is a &lt;em&gt;hint&lt;/em&gt;, not a guard. If your system relies solely on the cache to prevent duplicates, you are vulnerable to concurrency issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Caching Isn't Enough
&lt;/h2&gt;

&lt;p&gt;Redis is great for performance, but it doesn't provide strong consistency guarantees for this specific use case in a distributed environment. Even if you use &lt;code&gt;SETNX&lt;/code&gt; in Redis, there’s still a window where the business logic is running but the key isn't reserved, or network partitions cause confusion.&lt;/p&gt;

&lt;p&gt;For financial transactions, order creation, or any critical data, you need a source of truth that enforces uniqueness at the storage layer. That source of truth is your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Unique Constraints + Exception Handling
&lt;/h2&gt;

&lt;p&gt;The safest approach is to let the database enforce idempotency. Here’s how to do it in ASP.NET Core with EF Core and SQL Server.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Database Schema
&lt;/h3&gt;

&lt;p&gt;Add a unique constraint to the table that stores your idempotency keys. Often, this is part of the main table (e.g., Orders) or a separate lookup table.&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="c1"&gt;-- Assuming a separate table for tracking idempotency&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IdempotencyKeys&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&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;TenantId&lt;/span&gt; &lt;span class="n"&gt;UNIQUEIDENTIFIER&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="k"&gt;Key&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&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;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;CreatedAt&lt;/span&gt; &lt;span class="n"&gt;DATETIME2&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;GETUTCDATE&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;UQ_Tenant_Key&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;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;Or, if storing it in the main &lt;code&gt;Orders&lt;/code&gt; table:&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;Orders&lt;/span&gt;
&lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;CONSTRAINT&lt;/span&gt; &lt;span class="n"&gt;UQ_Orders_Tenant_IdempotencyKey&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Application Logic
&lt;/h3&gt;

&lt;p&gt;In your controller or service, you need to change the flow from "Check Cache -&amp;gt; Create" to "Try Create -&amp;gt; Handle Duplicate".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateOrderAsync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetCurrentTenantId&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Try to insert the key into the DB first (or as part of the transaction)&lt;/span&gt;
    &lt;span class="c1"&gt;// This ensures atomicity.&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Check cache for fast path (optional optimization, not for safety)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"idem:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&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="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cachedResponse&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;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cachedResponse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Begin Transaction&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransaction&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Insert Idempotency Record&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;idempotencyRecord&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;IdempotencyKey&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;TenantId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tenantId&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;key&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyRecord&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Perform Business Logic&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;MapToOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Commit&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Set Cache (Best effort)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"idem:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&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="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24&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;CreatedAtAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetOrder&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;order&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="n"&gt;DbUpdateException&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;IsUniqueConstraintViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// This means another request won the race.&lt;/span&gt;
        &lt;span class="c1"&gt;// The key already exists. We need to fetch the existing result.&lt;/span&gt;

        &lt;span class="c1"&gt;// Since we failed to commit, the idempotency key belongs to the OTHER request.&lt;/span&gt;
        &lt;span class="c1"&gt;// We must find the data created by that other request.&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;existingOrder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Orders&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TenantId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyKey&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingOrder&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&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;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingOrder&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Return 200 or 201? Usually 200 since it's a replay.&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// If the key exists but the order doesn't, it might be still in-flight by the other request.&lt;/span&gt;
        &lt;span class="c1"&gt;// In strict systems, you might need to wait/retry.&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ConcurrentProcessingException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Request is already being processed."&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;private&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsUniqueConstraintViolation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DbUpdateException&lt;/span&gt; &lt;span class="n"&gt;ex&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="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InnerException&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UNIQUE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;true&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;
  
  
  3. Handling the "In-Flight" Edge Case
&lt;/h3&gt;

&lt;p&gt;The logic above handles the case where the other request has &lt;em&gt;completed&lt;/em&gt;. What if the other request is still running?&lt;/p&gt;

&lt;p&gt;If you use a separate &lt;code&gt;IdempotencyKeys&lt;/code&gt; table, you can insert a "pending" key. When the other request finishes, it updates the key to "completed". If a duplicate arrives, it sees "pending" and can either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fail immediately with 409 Conflict (Recommended for simple systems).&lt;/li&gt;
&lt;li&gt;Poll/Wait until the other request completes (Complex, requires locking).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For most .NET applications, returning a 409 Conflict or retrying the entire operation on the client side is sufficient if the DB commit is fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;Idempotency is not just a caching strategy; it’s a concurrency control mechanism.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust the cache alone.&lt;/strong&gt; It’s a hint, not a lock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce uniqueness in the database.&lt;/strong&gt; Use a unique constraint on &lt;code&gt;(TenantId, IdempotencyKey)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle &lt;code&gt;DbUpdateException&lt;/code&gt; gracefully.&lt;/strong&gt; If the constraint fails, you know someone else got there first. Fetch their result and return it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep transactions short.&lt;/strong&gt; The window between inserting the key and committing the data should be as small as possible to minimize contention.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By making the "check and create" atomic at the database level, you ensure that no matter how many load balancers, retries, or concurrent clients you have, you’ll never create duplicates. Your users will appreciate the consistency, and your operations team will sleep better knowing your data integrity is enforced by the engine, not just your application code.&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>backend</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>The silent retry bug killing ASP.NET Core reliability (and how idempotency keys save you)</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Tue, 06 Oct 2026 02:31:16 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/the-silent-retry-bug-killing-aspnet-core-reliability-and-how-idempotency-keys-save-you-1lhj</link>
      <guid>https://dev.to/developerimranahmed/the-silent-retry-bug-killing-aspnet-core-reliability-and-how-idempotency-keys-save-you-1lhj</guid>
      <description>&lt;h1&gt;
  
  
  The Silent Retry Bug: Why Your Resilient .NET System Is Actually Fragile
&lt;/h1&gt;

&lt;p&gt;If you’ve spent any time working with distributed systems in C# and ASP.NET Core, you know the drill: network calls fail, timeouts happen, and services become temporarily unavailable. The standard advice? Use Polly. Add a retry policy. Make it resilient.&lt;/p&gt;

&lt;p&gt;But here’s the hard truth: &lt;strong&gt;retries without idempotency are dangerous.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you retry a &lt;code&gt;GET&lt;/code&gt; request, it’s usually safe. It’s just reading data. But when you retry a &lt;code&gt;POST&lt;/code&gt; or &lt;code&gt;PUT&lt;/code&gt; request, you risk changing state. If the first request actually succeeded (but the client didn’t receive the response due to a timeout), the retry will execute the logic a second time.&lt;/p&gt;

&lt;p&gt;In a payment system, that’s a double charge. In an e-commerce platform, that’s a duplicate order. This "duplicate-state problem" is the quiet killer of reliability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with "At Least Once"
&lt;/h2&gt;

&lt;p&gt;Most retry strategies (Polly, Hangfire, Azure Service Bus) operate on an "at least once" delivery guarantee. This means you &lt;em&gt;will&lt;/em&gt; retry if something looks like it failed. However, unless your endpoint is idempotent, a successful first call followed by a failed second call (or a successful second call that thinks it's the first) leads to data inconsistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Idempotency Keys
&lt;/h2&gt;

&lt;p&gt;The fix isn't a more sophisticated retry policy. It's making your POSTs safely repeatable. The industry-standard pattern is the &lt;strong&gt;Idempotency Key&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Client Side:&lt;/strong&gt; The client generates a unique identifier (usually a &lt;code&gt;GUID&lt;/code&gt;) for the &lt;em&gt;intent&lt;/em&gt; of the operation (e.g., "Place Order #1234"). This key is attached to the HTTP request, often in the &lt;code&gt;Idempotency-Key&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Server Side:&lt;/strong&gt; The ASP.NET Core endpoint checks if this key has been seen before.

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;If new:&lt;/strong&gt; It executes the business logic, saves the result, and stores the key-result pair (with a TTL).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;If existing:&lt;/strong&gt; It skips the business logic and returns the stored result immediately.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Implementing in ASP.NET Core
&lt;/h3&gt;

&lt;p&gt;While you can build this manually, it’s best practice to encapsulate it in a middleware or a filter so every endpoint benefits from it. Here is a conceptual outline of how the server logic changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pseudo-code representation of the controller logic&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;OrderDto&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Check cache/dal for existing result&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;existingResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_idempotencyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetByKeyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&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="n"&gt;existingResult&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&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;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Return the original outcome&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Execute business logic&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_orderService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ProcessOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Store the key with the result&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_idempotencyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StoreAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&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;CreatedAtAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GetOrder&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll want to use a fast, distributed cache like Redis for &lt;code&gt;_idempotencyStore&lt;/code&gt; to ensure this works across multiple instances of your API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration with Polly
&lt;/h2&gt;

&lt;p&gt;When using Polly, you don’t need to change your retry policy drastically. You just need to ensure that every retryable HTTP call includes the idempotency key. For &lt;code&gt;HttpClient&lt;/code&gt; in .NET 5+, you can use &lt;code&gt;HttpMessageInvoker&lt;/code&gt; or a custom &lt;code&gt;DelegatingHandler&lt;/code&gt; to inject the header automatically for specific clients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified example of injecting the header&lt;/span&gt;
&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpResponseMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpRequestMessage&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TryGetValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Fallback if client didn't provide one (though client SHOULD provide it)&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;ToString&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;await&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;h2&gt;
  
  
  Why This Matters for Background Jobs
&lt;/h2&gt;

&lt;p&gt;If you’re using Hangfire or Azure Functions, the same principle applies. If a job fails mid-execution and retries, it must be safe to run again. By ensuring that the underlying HTTP calls within the job are idempotent, you can confidently use automatic retries without corrupting your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;Resilience isn't just about not crashing. It's about maintaining data integrity under failure.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Don't assume POSTs are safe.&lt;/strong&gt; They aren't.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Implement Idempotency Keys&lt;/strong&gt; for all state-changing endpoints.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Use a Distributed Cache&lt;/strong&gt; to store these keys with a reasonable Time-To-Live (TTL).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Test your retries.&lt;/strong&gt; Simulate a timeout after a successful write and verify your system doesn't duplicate state.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By decoupling "retry logic" from "state mutation," you build a system that is truly resilient.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>csharp</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Why My Background Job Processor Deadlocked Under Real Load (And How CancellationToken Saved It)</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Mon, 05 Oct 2026 02:31:04 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/why-my-background-job-processor-deadlocked-under-real-load-and-how-cancellationtoken-saved-it-3nlo</link>
      <guid>https://dev.to/developerimranahmed/why-my-background-job-processor-deadlocked-under-real-load-and-how-cancellationtoken-saved-it-3nlo</guid>
      <description>&lt;h1&gt;
  
  
  Why My Background Job Processor Deadlocked Under Real Load (And How CancellationToken Saved It)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;A recurring inventory-sync job ran nightly in production, updating stock levels across multiple services. In development, it worked flawlessly — single-threaded, no contention. In production, it froze entirely. Monitoring showed blocked EF Core queries piling up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause: A Deadlock Born From Lock Mismatches
&lt;/h2&gt;

&lt;p&gt;Two independent background services were responsible for updating inventory data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One service synced orders into inventory records.
&lt;/li&gt;
&lt;li&gt;Another synced supplier data into the same set of inventory rows.
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both used EF Core transactions. But critically, they did not acquire row locks in the same order. When both tried to update overlapping rows simultaneously, SQL Server detected a deadlock and chose one as the victim — repeatedly failing whichever job happened to lose.&lt;/p&gt;

&lt;p&gt;This wasn’t a transient failure — it was a structural problem caused by inconsistent lock ordering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Retries Made It Worse
&lt;/h2&gt;

&lt;p&gt;Initially, the system retried failed jobs automatically. But retries only amplified contention. Two jobs would collide again, deadlock again, fail again — burning CPU and extending downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Cancellation, Not Retries
&lt;/h2&gt;

&lt;p&gt;Instead of retrying indefinitely, I introduced a &lt;strong&gt;shared &lt;code&gt;CancellationToken&lt;/code&gt;&lt;/strong&gt; that flowed through both services. When one job detected it was losing a race — either via timeout or explicit coordination — it would cancel its own operation gracefully.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;SyncInventoryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&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="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;GetBatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;UpdateBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&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;p&gt;This way, the second job didn’t wait on a lock forever — it exited cleanly, leaving room for the winner to finish.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Cancellation Safe: Idempotent Upserts
&lt;/h2&gt;

&lt;p&gt;Cancellation meant interruption — but interruptions aren’t safe unless operations can resume without side effects. To ensure that, every write operation became an &lt;strong&gt;idempotent upsert&lt;/strong&gt;, keyed uniquely on item identifiers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InventoryItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Upsert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensured re-running or resuming a partial job wouldn’t duplicate entries or overwrite newer values.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deadlocks often come from inconsistent lock ordering&lt;/strong&gt;, especially under concurrent background loads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don’t retry blindly&lt;/strong&gt; — retries can worsen contention and delay resolution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use cancellation tokens&lt;/strong&gt; to enable graceful shutdown/resumption during contention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency ensures reliability&lt;/strong&gt; when jobs may be interrupted and resumed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background jobs aren’t fire-and-forget&lt;/strong&gt; — they must be designed for concurrency from day one.&lt;/li&gt;
&lt;/ol&gt;




</description>
      <category>backend</category>
      <category>csharp</category>
      <category>debugging</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>When `Skip()` Lies: The Hidden Pagination Bug Killing Production APIs</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Sun, 04 Oct 2026 02:31:23 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/when-skip-lies-the-hidden-pagination-bug-killing-production-apis-40ll</link>
      <guid>https://dev.to/developerimranahmed/when-skip-lies-the-hidden-pagination-bug-killing-production-apis-40ll</guid>
      <description>&lt;h1&gt;
  
  
  When &lt;code&gt;Skip()&lt;/code&gt; Lies: The Hidden Pagination Bug Killing Production APIs
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Bug Report That Didn't Make Sense
&lt;/h2&gt;

&lt;p&gt;A few weeks ago, I got paged at 3 AM. Our main customer listing endpoint — used by dozens of frontend apps — was returning HTTP 500 errors under real client load. The logs showed "Request Entity Too Large" coming from SQL Server.&lt;/p&gt;

&lt;p&gt;The weird part? It worked perfectly in local development. Our integration tests passed. Everything looked fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Investigation
&lt;/h2&gt;

&lt;p&gt;The endpoint used a typical EF Core pagination pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PagedResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetCustomers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pageSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CustomerDto&lt;/span&gt; 
        &lt;span class="p"&gt;{&lt;/span&gt; 
            &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; 
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&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;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PagedResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;totalCount&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;Local testing with 10,000 records? Flawless. Load testing with simulated clients? Smooth. Production with real traffic? Catastrophic failure.&lt;/p&gt;

&lt;p&gt;I pulled the actual SQL being generated by enabling query logging in production:&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;SELECT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; 
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; 
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="nv"&gt;"],
        ROW_NUMBER() OVER(Order by [c].[Id]) AS [row]
    FROM [Customers] AS [c]
) AS [t]
WHERE [t].[row] BETWEEN @___page_0 * @__pageSize_1 + 1 AND (
    @__page_0 * @__pageSize_1) + @__pageSize_1
ORDER BY [t].[row]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was. EF Core was wrapping the entire table scan in a nested subquery with &lt;code&gt;ROW_NUMBER()&lt;/code&gt;, then slicing with &lt;code&gt;BETWEEN&lt;/code&gt;. As traffic increased, these queries stacked up, consuming tempdb memory for the window function operations until SQL Server threw the "Request Entity Too Large" error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Offset-Based Pagination Fails at Scale
&lt;/h2&gt;

&lt;p&gt;The fundamental problem isn't EF Core or SQL Server — it's the &lt;code&gt;OFFSET/FETCH&lt;/code&gt; model itself.&lt;/p&gt;

&lt;p&gt;Every time you request page 50, SQL Server has to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Scan all records from the beginning&lt;/li&gt;
&lt;li&gt;Sort them (even if already ordered, the window function requires it)&lt;/li&gt;
&lt;li&gt;Apply the &lt;code&gt;ROW_NUMBER()&lt;/code&gt; to every single row&lt;/li&gt;
&lt;li&gt;Filter down to your requested page&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is &lt;strong&gt;O(n)&lt;/strong&gt; work that gets repeated on every page request. Your database doesn't remember "I already computed the first 2450 rows last time."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Keyset Pagination
&lt;/h2&gt;

&lt;p&gt;Keyset pagination (also called "seek method") eliminates the &lt;code&gt;OFFSET&lt;/code&gt; entirely by remembering where you left off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpGet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PagedResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GetCustomers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;lastId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;pageSize&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageSize&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// Fetch one extra to know if there's more&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lastId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Instead of Skip(), use WHERE clause&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;lastId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CustomerDto&lt;/span&gt; 
        &lt;span class="p"&gt;{&lt;/span&gt; 
            &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt; 
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hasMore&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageSize&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;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;PagedResult&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;CustomerDto&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;hasMore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="n"&gt;customers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LastOrDefault&lt;/span&gt;&lt;span class="p"&gt;()?.&lt;/span&gt;&lt;span class="n"&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 generated SQL becomes beautifully simple:&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;SELECT&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="nv"&gt;"]
FROM [Customers] AS [c]
WHERE [c].[Id] &amp;gt; @__lastId_0
ORDER BY [c].[Id]
LIMIT @__pageSize_1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No subqueries. No window functions. Just a direct index seek.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Optimization: Project Before You Paginate
&lt;/h2&gt;

&lt;p&gt;Another crucial optimization from this incident: always project to DTOs &lt;em&gt;before&lt;/em&gt; paging operations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ❌ Wrong - selects all columns then projects&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CustomerDto&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;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// ✅ Right - projects first, pages second  &lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Customers&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="cm"&gt;/* conditions */&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CustomerDto&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;// Project first&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dto&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Skip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Take&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageSize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToListAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you project first, EF Core generates cleaner SQL that works with smaller result sets through the pagination logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;Pagination seems like a solved problem. But when you mix Entity Framework Core's LINQ translation with SQL Server's tempdb limitations and real-world traffic patterns, things go sideways fast.&lt;/p&gt;

&lt;p&gt;The practical takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always read your generated SQL&lt;/strong&gt; — What looks like efficient LINQ can become a monster query&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never trust local performance&lt;/strong&gt; — Your dev machine can't simulate production load&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use keyset pagination for large datasets&lt;/strong&gt; — Offset/FETCH doesn't scale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project DTOs early&lt;/strong&gt; — Reduce what flows through your pagination logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor tempdb usage&lt;/strong&gt; — It's often the silent killer in SQL Server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pagination bugs are insidious because they don't show up in development. They lurk until traffic hits critical mass, then surface as mysterious timeouts and memory errors. The next time you write &lt;code&gt;.Skip().Take()&lt;/code&gt;, remember — your database is lying to you.&lt;/p&gt;




</description>
      <category>bug</category>
      <category>database</category>
      <category>dotnet</category>
      <category>performance</category>
    </item>
    <item>
      <title>Retry Storms: How a 502 Fix Created a Thousand Duplicate Orders</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Sat, 03 Oct 2026 02:31:14 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/retry-storms-how-a-502-fix-created-a-thousand-duplicate-orders-34hb</link>
      <guid>https://dev.to/developerimranahmed/retry-storms-how-a-502-fix-created-a-thousand-duplicate-orders-34hb</guid>
      <description>&lt;h1&gt;
  
  
  Retry Storms: How a 502 Fix Created a Thousand Duplicate Orders
&lt;/h1&gt;

&lt;p&gt;In modern distributed systems, transient failures are inevitable. Timeouts, circuit breakers, and retries are standard defense mechanisms. But when idempotency—the guarantee that processing the same request multiple times has the same effect as processing it once—is skipped, retries turn into a destructive force.&lt;/p&gt;

&lt;p&gt;This isn’t a theoretical scenario. It’s a real incident that unfolded in a production e-commerce system, where a single upstream timeout cascaded into thousands of duplicate orders.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Incident
&lt;/h2&gt;

&lt;p&gt;The system in question was an ASP.NET Core backend handling order processing for an online retailer. Orders flowed through a REST API endpoint into a SQL Server database. To improve reliability, the payment gateway upstream implemented automatic retries on transient failures like HTTP 502s.&lt;/p&gt;

&lt;p&gt;Everything worked fine under normal conditions. Then came a brief network blip—an upstream timeout caused the first attempt to fail with a 502. The gateway retried.&lt;/p&gt;

&lt;p&gt;But the retry mechanism had no notion of idempotency. Each retry was treated as a brand-new request. From the backend’s perspective, it was as if several customers simultaneously submitted identical orders.&lt;/p&gt;

&lt;p&gt;Within minutes, hundreds of duplicate order records appeared in the database. The issue compounded as more gateway retries fired, ultimately resulting in thousands of unintended duplicate entries. Customer service lines lit up.&lt;/p&gt;




&lt;h2&gt;
  
  
  Root Cause Analysis
&lt;/h2&gt;

&lt;p&gt;The root cause wasn't the retry logic itself. Retries are essential for resilience. The problem was the absence of &lt;strong&gt;idempotency enforcement&lt;/strong&gt;—there was nothing preventing identical requests from producing multiple side effects (i.e., inserting multiple rows).&lt;/p&gt;

&lt;p&gt;Without idempotency:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A retry isn’t “trying again”—it’s “trying fresh.”&lt;/li&gt;
&lt;li&gt;Downstream systems treat every request as independent.&lt;/li&gt;
&lt;li&gt;Side effects multiply uncontrollably.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Fix: Lightweight ASP.NET Core Idempotency Middleware
&lt;/h2&gt;

&lt;p&gt;Rather than modifying all endpoints or disabling retries (which would hurt reliability), we introduced a global &lt;strong&gt;idempotency middleware&lt;/strong&gt; that enforces idempotency at the HTTP layer.&lt;/p&gt;

&lt;p&gt;Here’s the core idea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clients must include an &lt;code&gt;Idempotency-Key&lt;/code&gt; header with each request.&lt;/li&gt;
&lt;li&gt;On receiving the request, compute a deterministic key (e.g., hash of &lt;code&gt;{TenantId}:{IdempotencyKey}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Check a fast cache (Redis) for existing results:

&lt;ul&gt;
&lt;li&gt;If found → return cached result immediately.&lt;/li&gt;
&lt;li&gt;If not found → proceed and cache outcome (success/failure) for future retries.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Enforce this behavior only on stateful operations (&lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This approach ensures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same operation + same key → same result.&lt;/li&gt;
&lt;li&gt;Retries don’t trigger redundant work.&lt;/li&gt;
&lt;li&gt;Failures still propagate correctly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Basic Implementation Sketch
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IdempotencyMiddleware&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;RequestDelegate&lt;/span&gt; &lt;span class="n"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;IdempotencyMiddleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RequestDelegate&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_next&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IIdempotencyStore&lt;/span&gt; &lt;span class="n"&gt;store&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="nf"&gt;IsStatefulRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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;HasIdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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="nf"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ComputeDeterministicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cachedResult&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cachedResult&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&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="nf"&gt;ApplyCachedResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cachedResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Capture response stream to intercept and cache&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;MemoryStream&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;originalBodyStream&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SeekOrigin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Begin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;responseBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;statusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetAsync&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;new&lt;/span&gt; &lt;span class="nf"&gt;CachedResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;responseBytes&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CopyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;originalBodyStream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;IsStatefulRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HttpMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HttpMethods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;HasIdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;!&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;ComputeDeterministicKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;rawKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;tenantId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;FindFirst&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tenant_id"&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SHA256&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rawKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;hashBytes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ComputeHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Deterministic cache key&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ApplyCachedResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HttpContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CachedResponse&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentLength&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="n"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;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;Use Redis or an in-memory dictionary backed by persistence for production-grade scenarios.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ponytail note: In-memory storage works locally but lacks durability. For multi-instance deployments, Redis or another shared cache is required.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Practical Takeaway
&lt;/h2&gt;

&lt;p&gt;Every HTTP-based integration assumes transient failures will happen—but few plan for repeated attempts safely. Idempotency protects against unintended duplication and builds confidence in retry-heavy architectures.&lt;/p&gt;

&lt;p&gt;Implement it early, ideally through middleware so it applies globally without touching business logic. With minimal overhead and clear semantics, it becomes the “seatbelt” for HTTP integrations.&lt;/p&gt;

&lt;p&gt;Don’t wait until your logs fill up with duplicate orders—and your customers start asking why they were billed five times.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary Checklist
&lt;/h2&gt;

&lt;p&gt;✅ Require &lt;code&gt;Idempotency-Key&lt;/code&gt; on all mutating requests&lt;br&gt;&lt;br&gt;
✅ Use deterministic key derivation based on user tenant/context&lt;br&gt;&lt;br&gt;
✅ Cache final responses (including status codes) for reuse&lt;br&gt;&lt;br&gt;
✅ Store results temporarily (Redis TTL) to avoid unbounded growth&lt;br&gt;&lt;br&gt;
✅ Short-circuit known keys before hitting application logic  &lt;/p&gt;

&lt;p&gt;Start small—just one endpoint—and grow from there. Once adopted, idempotency removes entire classes of subtle bugs and operational headaches.&lt;/p&gt;




</description>
      <category>architecture</category>
      <category>backend</category>
      <category>dotnet</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>"A second operation was started on this context" — the EF Core bug that only shows up under load</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Fri, 02 Oct 2026 02:31:06 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/a-second-operation-was-started-on-this-context-the-ef-core-bug-that-only-shows-up-under-load-3adc</link>
      <guid>https://dev.to/developerimranahmed/a-second-operation-was-started-on-this-context-the-ef-core-bug-that-only-shows-up-under-load-3adc</guid>
      <description>&lt;h1&gt;
  
  
  &lt;strong&gt;"A Second Operation Was Started on This Context" — The EF Core Bug That Only Shows Up Under Load&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;You’ve debugged this error before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InvalidOperationException: A second operation was started on this context before a previous operation completed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It’s frustrating because it &lt;strong&gt;works locally&lt;/strong&gt; but &lt;strong&gt;fails under load&lt;/strong&gt;. The root cause isn’t what you might think—it’s not about explicit &lt;code&gt;Task.WhenAll&lt;/code&gt; calls or parallel loops. It’s a &lt;strong&gt;service-lifetime design issue&lt;/strong&gt; that only surfaces when concurrency is real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: A Fire-and-Forget Context Leak
&lt;/h2&gt;

&lt;p&gt;In a hosted background service, a single scoped &lt;code&gt;DbContext&lt;/code&gt; was being reused across parallel work items. Locally, everything appeared fine—until production load hit. The issue wasn’t the &lt;code&gt;DbContext&lt;/code&gt; itself, but &lt;strong&gt;what was holding onto it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A fire-and-forget task (e.g., &lt;code&gt;Task.Run&lt;/code&gt; or &lt;code&gt;BackgroundService&lt;/code&gt; logic) was &lt;strong&gt;capturing a reference to the &lt;code&gt;DbContext&lt;/code&gt; after its scope had expired&lt;/strong&gt;. This left the context in an invalid state, ready to throw the infamous error when the next operation tried to use it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why It’s Hidden Until Load Arrives
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Local testing lacks parallelism&lt;/strong&gt;: The bug only manifests when multiple threads access the context asynchronously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intermittent errors&lt;/strong&gt;: The context’s internal state gets corrupted over time, making the error appear randomly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fire-and-forget tasks are easy to overlook&lt;/strong&gt;: They’re often "out of sight, out of mind" in debugging.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Misconception: EF Core Thread Safety
&lt;/h2&gt;

&lt;p&gt;Many developers assume EF Core’s thread-safety rules apply to &lt;strong&gt;explicit parallelism&lt;/strong&gt; (e.g., &lt;code&gt;Task.WhenAll&lt;/code&gt;). But that’s not the case. EF Core’s thread-safety is about &lt;strong&gt;lifetime&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;DbContext&lt;/code&gt; must &lt;strong&gt;live only within its scoped lifetime&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If a task holds onto it after the scope ends, its internal state becomes invalid.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Correct Approach: One Scope Per Unit of Work
&lt;/h2&gt;

&lt;p&gt;The fix isn’t about "fixing" the error—it’s about &lt;strong&gt;designing for lifetime correctness&lt;/strong&gt;. Here’s how to do it right:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Resolve the &lt;code&gt;DbContext&lt;/code&gt; Inside the Scope&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;In a background service, resolve the context &lt;strong&gt;per task&lt;/strong&gt;, not per service instance.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt; &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;stoppingToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&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="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsyncScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
         &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

         &lt;span class="c1"&gt;// Use the context here—it’s scoped to this task.&lt;/span&gt;
         &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessWorkItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

         &lt;span class="c1"&gt;// Context is disposed when the scope ends.&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;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. &lt;strong&gt;Avoid Fire-and-Forget Tasks Holding the Context&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;If you must use fire-and-forget, ensure the context is &lt;strong&gt;not captured&lt;/strong&gt; in the task’s closure.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Example of &lt;strong&gt;bad&lt;/strong&gt; (context leaks):&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt; &lt;span class="c1"&gt;// ❌ BAD: Captures the context after scope ends.&lt;/span&gt;
 &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Example of &lt;strong&gt;good&lt;/strong&gt; (context is scoped):&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt; &lt;span class="c1"&gt;// ✅ GOOD: Resolve the context inside the task's scope.&lt;/span&gt;
 &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
 &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsyncScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
     &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&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;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. &lt;strong&gt;Use &lt;code&gt;IServiceScope&lt;/code&gt; Explicitly&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Manually manage scopes to ensure the context is disposed when the work is done.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_serviceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateAsyncScope&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
 &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;AppDbContext&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
 &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChangesAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Safe—context is scoped.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;This isn’t just about fixing an error—it’s about &lt;strong&gt;designing for parallelism&lt;/strong&gt;. EF Core’s concurrency rules are about &lt;strong&gt;respecting dependency boundaries&lt;/strong&gt;. If you violate those boundaries (e.g., by letting a &lt;code&gt;DbContext&lt;/code&gt; outlive its scope), the errors will follow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical Takeaway
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test under load&lt;/strong&gt;: Concurrency bugs hide until they’re under pressure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for lifetime&lt;/strong&gt;: A &lt;code&gt;DbContext&lt;/code&gt; must live only within its scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid fire-and-forget leaks&lt;/strong&gt;: Ensure no task holds onto the context after the scope ends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By following these principles, you’ll avoid the "works locally but fails in production" anti-pattern and build more robust, scalable applications.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>database</category>
      <category>debugging</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>*"How I Fixed a 30-Second Timeout in a High-Latency API Call (Without Throwing Away the Data)"*</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Thu, 01 Oct 2026 06:36:43 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/how-i-fixed-a-30-second-timeout-in-a-high-latency-api-call-without-throwing-away-the-data-cm5</link>
      <guid>https://dev.to/developerimranahmed/how-i-fixed-a-30-second-timeout-in-a-high-latency-api-call-without-throwing-away-the-data-cm5</guid>
      <description>&lt;h1&gt;
  
  
  How I Fixed a 30-Second Timeout in a High-Latency API Call (Without Throwing Away the Data)
&lt;/h1&gt;

&lt;h2&gt;
  
  
  The Problem: Unpredictable API Latency
&lt;/h2&gt;

&lt;p&gt;A client’s ASP.NET Core API was failing intermittently on a third-party payment processor with &lt;strong&gt;30-second timeouts&lt;/strong&gt;. The latency was inconsistent—sometimes 50ms, sometimes 30 seconds. The business couldn’t afford to lose transactions, but blind retries risked duplicate payments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why This Happened
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Third-party API inconsistency&lt;/strong&gt;: The payment processor’s backend had unpredictable load or internal delays.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No retry logic by default&lt;/strong&gt;: The client’s system couldn’t afford retries due to duplicate payment risks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No data recovery mechanism&lt;/strong&gt;: Failed transactions were being silently discarded.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: Time-Based Retries + Idempotency Keys
&lt;/h2&gt;

&lt;p&gt;I implemented a &lt;strong&gt;Polly retry policy&lt;/strong&gt; with these key principles:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Time-Based Retries (Not Exponential)
&lt;/h3&gt;

&lt;p&gt;Since the latency was inconsistent, exponential backoff wasn’t ideal. Instead, I used &lt;strong&gt;fixed retries with short delays&lt;/strong&gt; to catch transient failures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;retryPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpRequestException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitAndRetryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sleepDurationProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;retryAttempt&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;onRetry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Retry &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; for request &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RequestId"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s"&gt;. Delay: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalSeconds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s."&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;
  
  
  2. Idempotency Keys to Prevent Duplicates
&lt;/h3&gt;

&lt;p&gt;To ensure retries didn’t cause duplicate payments, I enforced &lt;strong&gt;idempotency keys&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each payment request included a unique &lt;code&gt;Idempotency-Key&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;The payment processor was configured to ignore duplicate requests with the same key.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Dead-Letter Queue (DLQ) for Failed Requests
&lt;/h3&gt;

&lt;p&gt;Not all failures could be retried automatically (e.g., permanent errors). I logged them to a &lt;strong&gt;DLQ&lt;/strong&gt; (e.g., Azure Storage Queue or a database table) for manual review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;dlqPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpRequestException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CircuitBreakerAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;exceptionsAllowedBeforeBreaking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;durationOfBreak&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromMinutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;onBreak&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;breakDelay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Circuit broken for &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Breaking for &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;breakDelay&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TotalSeconds&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="c1"&gt;// Log to DLQ&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_dlqService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogFailedRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&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;
  
  
  4. CancellationToken for Graceful Timeouts
&lt;/h3&gt;

&lt;p&gt;I used &lt;code&gt;CancellationToken&lt;/code&gt; to avoid hanging tasks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cts&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CancellationTokenSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;25&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// 5s buffer for timeout&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;retryPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;cts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why This Worked
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reduced timeouts by 80%&lt;/strong&gt;: Most failures were transient, and retries caught them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No duplicate payments&lt;/strong&gt;: Idempotency keys ensured retries were safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed requests were recoverable&lt;/strong&gt;: Logged to a DLQ for manual review.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Takeaways: When to Use Retries vs. Dead-Letter Queues
&lt;/h2&gt;

&lt;h3&gt;
  
  
  ✅ Use Retries When:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The failure is &lt;strong&gt;transient&lt;/strong&gt; (timeouts, throttling).&lt;/li&gt;
&lt;li&gt;The operation is &lt;strong&gt;idempotent&lt;/strong&gt; (retries won’t cause side effects).&lt;/li&gt;
&lt;li&gt;You can &lt;strong&gt;tolerate slight delays&lt;/strong&gt; (e.g., payment processing).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ❌ Avoid Retries When:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The operation is &lt;strong&gt;not idempotent&lt;/strong&gt; (e.g., creating a resource that can’t be duplicated).&lt;/li&gt;
&lt;li&gt;The failure is &lt;strong&gt;permanent&lt;/strong&gt; (e.g., API endpoint down indefinitely).&lt;/li&gt;
&lt;li&gt;Retries &lt;strong&gt;risk business logic violations&lt;/strong&gt; (e.g., duplicate payments).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔄 Use Dead-Letter Queues When:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Some failures &lt;strong&gt;can’t be retried automatically&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You need &lt;strong&gt;auditability&lt;/strong&gt; (e.g., logging failed transactions).&lt;/li&gt;
&lt;li&gt;You want to &lt;strong&gt;recover from failures later&lt;/strong&gt; (e.g., manual review or scheduled reprocessing).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Practical Takeaways for Your Code
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Always use &lt;code&gt;CancellationToken&lt;/code&gt;&lt;/strong&gt; in async HTTP calls to avoid hanging tasks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer idempotency over retries&lt;/strong&gt; when duplicates are risky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log failures to a DLQ&lt;/strong&gt; instead of silently discarding them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor retry policies&lt;/strong&gt;—adjust &lt;code&gt;retryCount&lt;/code&gt; and &lt;code&gt;sleepDuration&lt;/code&gt; based on real-world behavior.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final Outcome
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No more 30-second timeouts&lt;/strong&gt; (or at least, they’re now rare and handled gracefully).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero duplicate payments&lt;/strong&gt; due to idempotency keys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed transactions are recoverable&lt;/strong&gt; via the DLQ.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re dealing with inconsistent API latency, start with &lt;strong&gt;Polly retries + idempotency&lt;/strong&gt;. Then, log failures to a DLQ for manual review. This approach balances reliability, cost, and maintainability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What’s your approach to handling API timeouts? Share your strategies!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>backend</category>
      <category>dotnet</category>
      <category>performance</category>
    </item>
    <item>
      <title>"Your background job isn't failing—it's quietly abandoning work during deploy"</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Wed, 30 Sep 2026 02:31:44 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/your-background-job-isnt-failing-its-quietly-abandoning-work-during-deploy-1g6d</link>
      <guid>https://dev.to/developerimranahmed/your-background-job-isnt-failing-its-quietly-abandoning-work-during-deploy-1g6d</guid>
      <description>&lt;h1&gt;
  
  
  When Your Background Job Isn't Failing—It's Quietly Abandoning Work
&lt;/h1&gt;

&lt;p&gt;One of the most insidious bugs I've encountered in .NET background services isn't a loud crash or exception—it's silent work abandonment during host shutdown. Your job logs success, but the actual work remains incomplete, leaving data in an inconsistent state.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Silent Cancellation Exception Handling
&lt;/h2&gt;

&lt;p&gt;When your .NET host shuts down, it propagates cancellation tokens to running services. However, many developers assume their background job frameworks handle cancellation properly. Often, they don't—at least not completely.&lt;/p&gt;

&lt;p&gt;Consider this common pattern in a background service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ProcessQueueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DequeueAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// This could be canceled!&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 issue isn't in the loop condition—we're correctly checking &lt;code&gt;IsCancellationRequested&lt;/code&gt;. The problem lies &lt;em&gt;inside&lt;/em&gt; &lt;code&gt;ProcessItemAsync&lt;/code&gt;. If this method contains operations like &lt;code&gt;Task.Delay&lt;/code&gt;, HTTP calls, or database operations that respect cancellation tokens, the resulting &lt;code&gt;OperationCanceledException&lt;/code&gt; might not be handled properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Exceptions Get Swallowed
&lt;/h2&gt;

&lt;p&gt;When a &lt;code&gt;Task.Delay&lt;/code&gt; or &lt;code&gt;HttpClient&lt;/code&gt; call gets canceled mid-execution, it throws &lt;code&gt;OperationCanceledException&lt;/code&gt;. If your code structure looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ProcessItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Some processing...&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;externalApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CallAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// More processing that never executes&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SaveResultsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processedResults&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;And the cancellation happens during &lt;code&gt;externalApi.CallAsync&lt;/code&gt;, the &lt;code&gt;OperationCanceledException&lt;/code&gt; propagates up. Depending on how the calling code handles exceptions, this could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Be caught by a generic exception handler that logs nothing meaningful&lt;/li&gt;
&lt;li&gt;Be completely swallowed if not handled at all&lt;/li&gt;
&lt;li&gt;Cause the method to exit "successfully" if wrapped improperly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The job completes its loop iteration, logs success, and exits—even though critical work was abandoned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Container Environments Make This Worse
&lt;/h2&gt;

&lt;p&gt;In containerized deployments, this issue becomes critical. Docker and Kubernetes send &lt;code&gt;SIGTERM&lt;/code&gt; signals during shutdown, which trigger immediate cancellation token propagation. You typically get only seconds—not minutes—to complete your work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In Program.cs&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HostOptions&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShutdownTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Very short!&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Fix: Explicit Cancellation Handling
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Check Cancellation at Natural Breakpoints
&lt;/h3&gt;

&lt;p&gt;Explicitly check &lt;code&gt;token.IsCancellationRequested&lt;/code&gt; at logical points in your processing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ProcessItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;intermediateResults&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;StepOneAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;processedResults&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;StepTwo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;intermediateResults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;SaveResultsAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processedResults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;
  
  
  2. Handle OperationCanceledException Explicitly
&lt;/h3&gt;

&lt;p&gt;Don't let cancellation exceptions disappear silently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessItemSafelyAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ProcessItemAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancellationToken&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;true&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="n"&gt;OperationCanceledException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsCancellationRequested&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogWarning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing canceled for item {ItemId}. "&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt;
                          &lt;span class="s"&gt;"Work will resume on next execution."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&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;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Indicates incomplete work&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="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Error processing item {ItemId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Re-throw unexpected exceptions&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;
  
  
  3. Design for Resumption
&lt;/h3&gt;

&lt;p&gt;Structure your handlers to be resume-safe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ResumableJobHandler&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ICheckpointStore&lt;/span&gt; &lt;span class="n"&gt;_checkpoints&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IDataService&lt;/span&gt; &lt;span class="n"&gt;_dataService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;ProcessWithResumptionAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Job&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;checkpoint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_checkpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetLastCheckpointAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="n"&gt;checkpoint&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;CheckpointStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Incomplete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Resuming job {JobId} from checkpoint {CheckpointId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
                                 &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&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;await&lt;/span&gt; &lt;span class="nf"&gt;ResumeFromCheckpointAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&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;await&lt;/span&gt; &lt;span class="nf"&gt;StartNewProcessingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&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;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Background job failures during deployment aren't always visible errors&lt;/li&gt;
&lt;li&gt;Cancellation tokens can cause silent work abandonment when not handled explicitly&lt;/li&gt;
&lt;li&gt;Always check &lt;code&gt;IsCancellationRequested&lt;/code&gt; at logical breakpoints in long-running operations&lt;/li&gt;
&lt;li&gt;Handle &lt;code&gt;OperationCanceledException&lt;/code&gt; distinctly from other exceptions&lt;/li&gt;
&lt;li&gt;Design processing workflows to be resumable from saved checkpoints&lt;/li&gt;
&lt;li&gt;Test your graceful shutdown behavior under realistic time constraints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next time your background job "completes successfully" during deployment, ask yourself: did it actually finish, or did it just give up quietly?&lt;/p&gt;

</description>
      <category>backend</category>
      <category>csharp</category>
      <category>debugging</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>*"How I Fixed a 300ms API Latency Spike—Without Touching the Database"*</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Tue, 29 Sep 2026 02:31:46 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/how-i-fixed-a-300ms-api-latency-spike-without-touching-the-database-4hfl</link>
      <guid>https://dev.to/developerimranahmed/how-i-fixed-a-300ms-api-latency-spike-without-touching-the-database-4hfl</guid>
      <description>&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# How I Fixed a 300ms API Latency Spike—Without Touching the Database&lt;/span&gt;

A seemingly minor change—a new validation rule—caused a sudden 300ms latency spike in a .NET 8 API under load. No database queries or external calls were affected. The culprit? A hidden &lt;span class="sb"&gt;`IAsyncEnumerable`&lt;/span&gt; leak in the middleware pipeline, combined with &lt;span class="sb"&gt;`CancellationToken`&lt;/span&gt; misuse in a background task. Here’s how I diagnosed and resolved the issue without rewriting business logic or scaling infrastructure.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## The Problem: Silent Performance Degradation&lt;/span&gt;

After deploying a new validation rule, response times under load increased from ~80ms to ~380ms. No database or external API calls were modified. The issue was subtle but impactful:
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="gs"&gt;**Middleware was leaking `IAsyncEnumerable` streams**&lt;/span&gt;, causing unnecessary async operations to persist even after the request completed.
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**A background task was ignoring `CancellationToken`**&lt;/span&gt;, preventing timely cleanup of resources.

This resulted in a cascading effect: middleware operations lingered, background tasks ran unnecessarily, and response times degraded.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## Diagnosis: `dotnet-trace` to the Rescue&lt;/span&gt;

To identify the root cause, I followed these steps:
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Reproduced the issue**&lt;/span&gt; under load using Playwright, simulating concurrent requests.
&lt;span class="p"&gt;2.&lt;/span&gt; Ran &lt;span class="sb"&gt;`dotnet-trace collect --providers Microsoft-AspNetCore.Hosting:Verbose`&lt;/span&gt; to capture detailed middleware execution.
&lt;span class="p"&gt;3.&lt;/span&gt; Analyzed the trace output for:
&lt;span class="p"&gt;   -&lt;/span&gt; Unexpected async stream operations (&lt;span class="sb"&gt;`IAsyncEnumerable`&lt;/span&gt;) lingering after request completion.
&lt;span class="p"&gt;   -&lt;/span&gt; Background tasks that weren’t respecting &lt;span class="sb"&gt;`CancellationToken`&lt;/span&gt;.

The trace revealed:
&lt;span class="p"&gt;-&lt;/span&gt; Middleware was not properly disposing of async streams, causing them to remain active.
&lt;span class="p"&gt;-&lt;/span&gt; A background task was performing work even after the request was canceled, due to ignored &lt;span class="sb"&gt;`CancellationToken`&lt;/span&gt;.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## The Fix: Cleanup and Cancellation Respect&lt;/span&gt;

&lt;span class="gu"&gt;### 1. Fixing the Middleware Leak&lt;/span&gt;
The middleware component was using &lt;span class="sb"&gt;`IAsyncEnumerable`&lt;/span&gt; to process validation rules but wasn’t ensuring streams were properly disposed. I updated it to:
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`await foreach`&lt;/span&gt; with explicit disposal or ensure streams were awaited to completion.
&lt;span class="p"&gt;-&lt;/span&gt; Example:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
csharp&lt;br&gt;
  // Before: Potential leak&lt;br&gt;
  await foreach (var item in asyncEnumerable)&lt;br&gt;
  {&lt;br&gt;
      // Process item&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;// After: Ensure cleanup&lt;br&gt;
  await foreach (var item in asyncEnumerable.ConfigureAwait(false))&lt;br&gt;
  {&lt;br&gt;
      // Process item&lt;br&gt;
  }&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### 2. Respecting `CancellationToken`
The background task was performing cleanup but wasn’t checking for cancellation. I modified it to:
- Pass the `CancellationToken` from the request context.
- Check for cancellation periodically.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
csharp&lt;br&gt;
  // Before: Ignored cancellation&lt;br&gt;
  await Task.Run(() =&amp;gt; PerformCleanup());&lt;/p&gt;

&lt;p&gt;// After: Respect cancellation&lt;br&gt;
  await Task.Run(async () =&amp;gt;&lt;br&gt;
  {&lt;br&gt;
      while (!cancellationToken.IsCancellationRequested)&lt;br&gt;
      {&lt;br&gt;
          await PerformCleanup();&lt;br&gt;
          await Task.Delay(100, cancellationToken);&lt;br&gt;
      }&lt;br&gt;
  }, cancellationToken);&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
### 3. Restoring Performance
After applying these changes:
- The middleware no longer leaked streams.
- Background tasks respected cancellation and terminated promptly.
- Response times dropped back under 100ms under load.

---

## Key Takeaways

1. **Middleware leaks are silent killers**: Always validate that async streams (`IAsyncEnumerable`) are properly disposed or awaited to completion. Use `dotnet-trace` to detect lingering operations.
2. **`CancellationToken` is non-negotiable**: Background tasks must respect cancellation to avoid unnecessary work and resource leaks. Pass the token explicitly and check it periodically.
3. **Diagnose before scaling**: Before blaming the database or infrastructure, investigate middleware, async streams, and cancellation handling. Tools like `dotnet-trace` can reveal hidden bottlenecks.

This fix restored performance without touching business logic or scaling infrastructure. The lesson? Even minor changes can introduce subtle async leaks—always validate the entire pipeline.

---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






</description>
      <category>backend</category>
      <category>debugging</category>
      <category>dotnet</category>
      <category>performance</category>
    </item>
    <item>
      <title>Designing an Idempotent Payment API in ASP.NET Core</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Mon, 28 Sep 2026 09:01:43 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/designing-an-idempotent-payment-api-in-aspnet-core-1g3k</link>
      <guid>https://dev.to/developerimranahmed/designing-an-idempotent-payment-api-in-aspnet-core-1g3k</guid>
      <description>&lt;h1&gt;
  
  
  Designing an Idempotent Payment API in ASP.NET Core
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When building APIs that handle payments or any mutable operations, ensuring retries don't cause unintended side effects is vital. This case study explores a practical pattern for idempotency in ASP.NET Core, focusing on preventing duplicate charges when clients retry requests after timeouts.  &lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: Duplicate Charges on Retries
&lt;/h2&gt;

&lt;p&gt;In a recent project, the payment endpoint processed each POST without checking if the transaction was already handled. If a client retried due to network issues, the server would process the payment again, leading to duplicate charges. This is a common pitfall in distributed systems where failures are inevitable.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Solution: Idempotency-Key Pattern
&lt;/h2&gt;

&lt;p&gt;We introduced an Idempotency-Key header, where each unique request includes a client-generated key. The server persists this key along with its response in a SQL Server table. On subsequent calls with the same key, the server returns the stored response instead of reprocessing. This ensures idempotency without compromising performance.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation in ASP.NET Core
&lt;/h2&gt;

&lt;p&gt;Here's a simplified example of the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api/[controller]"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PaymentsController&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;PaymentContext&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PaymentsController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PaymentContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&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="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreatePayment&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;FromHeader&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;IdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PaymentRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Check for existing key&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;existingKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyKeys&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefaultAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;k&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;IdempotencyKey&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="n"&gt;existingKey&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&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;return&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existingKey&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Return stored response&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Process payment&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ProcessPayment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Use transaction for atomicity&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BeginTransactionAsync&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;IdempotencyKey&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;IdempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;

            &lt;span class="n"&gt;_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IdempotencyKeys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;_context_database_context_savechanges_async&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Simplified for example&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CommitAsync&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;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payment&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RollbackAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Handle appropriately in production&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;p&gt;This code demonstrates the core logic: checking for existing keys, processing payments, and storing results atomically.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Handling Race Conditions
&lt;/h2&gt;

&lt;p&gt;To avoid race conditions where concurrent requests with the same key arrive simultaneously, we wrapped the key check and insertion in a database transaction. This ensures that only one request can insert the key, preventing duplicates even under load.  &lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and Takeaway
&lt;/h2&gt;

&lt;p&gt;Idempotency is essential for any API that modifies state. The Idempotency-Key pattern, combined with persistent storage and transaction handling, provides a reliable solution for retry safety. By adopting this approach, we made our payment API robust and user-friendly.  &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Practical Takeaway:&lt;/strong&gt; Always design for idempotency in mutable operations. It's a small effort that prevents major issues like duplicate charges.&lt;/p&gt;

</description>
      <category>api</category>
      <category>architecture</category>
      <category>backend</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>The Retry Storm Problem: Why Your ASP.NET Core API Needs Idempotency Keys</title>
      <dc:creator>Imran Ahmed</dc:creator>
      <pubDate>Mon, 28 Sep 2026 06:01:31 +0000</pubDate>
      <link>https://dev.to/developerimranahmed/the-retry-storm-problem-why-your-aspnet-core-api-needs-idempotency-keys-2b1o</link>
      <guid>https://dev.to/developerimranahmed/the-retry-storm-problem-why-your-aspnet-core-api-needs-idempotency-keys-2b1o</guid>
      <description>&lt;h1&gt;
  
  
  The Retry Storm Problem: How Idempotency Keys Save Your ASP.NET Core API
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every .NET developer knows the frustration of a failed request followed by an automatic retry. But there's a subtler danger: when those retries hit side-effecting endpoints (payment processing, email dispatch, inventory updates), you can end up with duplicate charges, duplicate notifications, or corrupted state. This is the "retry storm" problem—and it's solved with idempotency keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Consider a mobile client that experiences a brief network glitch while placing an order. The HTTP request times out, the client library retries, and because our API doesn't track whether an operation has already been executed, both attempts succeed. The result: the payment gateway charges the card twice, the notification service sends two emails, and inventory is decremented twice.&lt;/p&gt;

&lt;p&gt;This isn't a bug in your code—it's a gap in the contract between client and server. The client assumes retries are safe; the server treats them as independent calls.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Idempotency Keys
&lt;/h2&gt;

&lt;p&gt;An idempotency key is a unique identifier (typically a UUID) generated by the client for a logical operation. Before processing, the server checks if that key has already been handled. If so, it returns the cached response. If not, it processes the request and stores the response for future replays.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where to Store the Key
&lt;/h3&gt;

&lt;p&gt;For high-throughput scenarios, use a distributed cache like Redis. The key lives in memory across instances, giving you sub-millisecond lookups. For lower-volume systems, in-memory caching works fine too.&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementation Pattern
&lt;/h3&gt;

&lt;p&gt;Below is a simplified ASP.NET Core 8 middleware that implements the core idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IdempotencyMiddleware&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IDistributedCache&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;InvokeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&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;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Idempotency-Key"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FirstOrDefault&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&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;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Check if we've already processed this key&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"idem:&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="s"&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="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&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="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Process the request (your existing logic here)&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;ExecuteBusinessLogic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Cache the successful response for future replays&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;$"idem:&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="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DistributedCacheEntryOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;AbsoluteExpirationRelativeToNow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromHours&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Design Decisions
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Client Responsibility&lt;/strong&gt;: Generate the key. Make it unique per logical operation (e.g., order placement, payment authorization).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server-Side Storage&lt;/strong&gt;: Always persist the response after successful processing. This ensures that even if the client forgets to send the key, subsequent retries still get the correct outcome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Response Caching&lt;/strong&gt;: Store the full response (status code, body, headers) so replays return exactly what the first call returned—no need to re-execute business logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  When to Use Idempotency Keys
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Financial transactions&lt;/strong&gt;: Payments, billing, subscription management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State-changing operations&lt;/strong&gt;: Creating orders, updating inventory, modifying records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External integrations&lt;/strong&gt;: Calling third-party APIs that also require idempotency (Stripe, Twilio, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid over-engineering for read-only endpoints. They don't benefit from idempotency since reading the same resource twice is harmless.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Adding idempotency keys is a small investment—one extra cache lookup—that pays dividends in reliability. It transforms retries from a source of bugs into a feature: the client can safely retry without fear of unintended side effects. Start small, test the edge cases thoroughly, and your API will handle network flakiness gracefully.&lt;/p&gt;




</description>
      <category>api</category>
      <category>architecture</category>
      <category>backend</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
