Skip to content

Bits of .NET

Daily micro-tips for C#, SQL, performance, and scalable backend engineering.

  • Asp.Net Core
  • C#
  • SQL
  • JavaScript
  • CSS
  • About
  • ErcanOPAK.com
  • No Access
  • Privacy Policy
Ajax

Ajax: Stop Double Form Submissions From Firing Duplicate Requests — Debounce and AbortController Explained

- 30.08.26 - ErcanOPAK

🔄 One Disable = No Duplicate Orders

A user double-clicks “Submit” or clicks fast on a slow connection, and your Ajax call fires twice — creating two orders, two payments, or two database rows. This is one of the most common (and expensive) bugs in web forms.

🐞 The Problem

document.getElementById("payBtn").addEventListener("click", function () {
  $.ajax({
    url: "/api/checkout",
    method: "POST",
    data: JSON.stringify(cartData),
    contentType: "application/json"
  });
  // Nothing stops the user from clicking again before this finishes --
  // two identical POST requests both succeed.
});

🔍 Cause & Effect

Click 1 -> request sent, still "in flight" (network latency, server processing)
Click 2 (0.3s later, impatient user) -> second request sent
Both requests reach the server and are processed independently
=> duplicate order, duplicate charge, duplicate database row

✅ Fix 1 — Disable the Button Immediately (Simplest, Always Do This)

document.getElementById("payBtn").addEventListener("click", async function (e) {
  const btn = e.target;
  if (btn.disabled) return; // guard clause -- extra safety
  btn.disabled = true;
  btn.textContent = "Processing...";
  try {
    await $.ajax({ url: "/api/checkout", method: "POST", data: JSON.stringify(cartData), contentType: "application/json" });
  } finally {
    btn.disabled = false;
    btn.textContent = "Pay Now";
  }
});

✅ Fix 2 — Cancel the Previous Request With AbortController (For Search/Filter Ajax)

let controller;
document.getElementById("search").addEventListener("input", function (e) {
  if (controller) controller.abort(); // cancel the previous in-flight request
  controller = new AbortController();
  fetch("/api/search?q=" + encodeURIComponent(e.target.value), {
    signal: controller.signal
  })
    .then(res => res.json())
    .then(renderResults)
    .catch(err => { if (err.name !== "AbortError") console.error(err); });
});

✅ Fix 3 — Debounce for Keystroke-Triggered Ajax (Fewer Requests, Not Just Fewer Duplicates)

function debounce(fn, delay) {
  let timer;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
const debouncedSearch = debounce((q) => {
  fetch("/api/search?q=" + encodeURIComponent(q)).then(/* ... */);
}, 300); // waits 300ms after the user stops typing before firing
document.getElementById("search").addEventListener("input", (e) => debouncedSearch(e.target.value));

📌 Server-Side Safety Net (Never Trust the Frontend Alone)

Always add an idempotency key: generate a unique request ID client-side,
send it in the request header, and have the server reject/ignore a
second request with the same ID within a short time window.
This protects you even if a user opens two browser tabs.

Disabling the button on click is a two-line fix that eliminates the single most expensive class of Ajax bug — duplicate payments — before it ever reaches your database.

— Frontend Engineer

Related posts:

AJAX: Build Real-time Apps with WebSockets

Ajax Requests Succeed but UI Never Updates

AJAX: Use Response Type for Automatic Data Parsing

Post Views: 2

Post navigation

CSS: Fix the Horizontal Scrollbar Nightmare With Two Properties Nobody Sets by Default
Git: Recover a Deleted Branch in 10 Seconds Using reflog — Even After a Force Push

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

October 2026
M T W T F S S
 1234
567891011
12131415161718
19202122232425
262728293031  
« Sep    

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (973)
  • How to make theater mode the default for Youtube (952)
  • How to add default value for Entity Framework migrations for DateTime and Bool (939)
  • Get the First and Last Word from a String or Sentence in SQL (847)
  • How to select distinct rows in a datatable in C# (837)
  • How to enable, disable and check if Service Broker is enabled on a database in SQL Server (624)
  • Add Constraint to SQL Table to ensure email contains @ (590)
  • Average of all values in a column that are not zero in SQL (553)
  • How to use Map Mode for Vertical Scroll Mode in Visual Studio (526)
  • Find numbers with more than two decimal places in SQL (468)

Recent Posts

  • CSS: Fix a prefers-color-scheme Media Query That Gets Silently Overridden by a Browser Extension’s Forced Dark Mode
  • Git: Fix a Merge Commit That Silently Drops a File Because Both Branches Deleted It Differently
  • HTML5: Fix a Native Lazy-Loading Image That Never Loads Because It Sits Inside a Hidden Tab Until the User Clicks It
  • The AI Prompt That Traces a Null Reference Exception Back to the Exact Line That First Produced the Null
  • The AI Prompt That Turns a Gym Membership Contract’s Fine Print Into a Plain-English List of Cancellation Steps
  • Photoshop: Fix a Color Profile Mismatch That Makes Printed Output Look Nothing Like What You Saw On Screen
  • WordPress: Fix Search Results That Return Pages From a Theme You Deactivated Months Ago
  • Visual Studio: Fix a Test Project That Builds Fine Alone but Fails to Discover Any Tests After a NuGet Restore
  • ASP.NET Core: Fix a File Upload That Times Out on Slow Connections Only Because Kestrel’s Minimum Data Rate Feature Kicked In
  • JavaScript: Fix an Array Destructuring Default Value That Silently Never Applies Because null Was Passed Instead of Undefined

Most Viewed Posts

  • Get the User Name and Domain Name from an Email Address in SQL (973)
  • How to make theater mode the default for Youtube (952)
  • How to add default value for Entity Framework migrations for DateTime and Bool (939)
  • Get the First and Last Word from a String or Sentence in SQL (847)
  • How to select distinct rows in a datatable in C# (837)

Recent Posts

  • CSS: Fix a prefers-color-scheme Media Query That Gets Silently Overridden by a Browser Extension’s Forced Dark Mode
  • Git: Fix a Merge Commit That Silently Drops a File Because Both Branches Deleted It Differently
  • HTML5: Fix a Native Lazy-Loading Image That Never Loads Because It Sits Inside a Hidden Tab Until the User Clicks It
  • The AI Prompt That Traces a Null Reference Exception Back to the Exact Line That First Produced the Null
  • The AI Prompt That Turns a Gym Membership Contract’s Fine Print Into a Plain-English List of Cancellation Steps

Social

  • ErcanOPAK.com
  • GoodReads
  • LetterBoxD
  • Linkedin
  • The Blog
  • Twitter
© 2026 Bits of .NET | Built with Xblog Plus free WordPress theme by wpthemespace.com