Mahamudul Hasan Rubel
HomeAboutProjectsSkillsExperienceBlogPhotosContact
Mahamudul Hasan Rubel

Senior Software Engineer crafting high-performance web applications and SaaS platforms.

Navigation

  • Home
  • About
  • Projects
  • Skills
  • Experience
  • Blog
  • Photos
  • Contact

Get in Touch

Available for senior/lead roles and consulting.

bd.mhrubel@gmail.comHire Me

© 2026 Mahamudul Hasan Rubel. All rights reserved.

Built with using Next.js 16 & Tailwind v4

Back to Blog
Next.jsReactJune 21, 20264 min read

Next.js Server Actions Request Collapsing: Preventing Race Conditions

Master Next.js Server Actions request collapsing to prevent race conditions. Learn practical concurrency control patterns to stop redundant mutations today.

Next.jsServer ActionsReactPerformanceConcurrencyWeb DevelopmentFrontendTypeScript

Last month, we pushed a feature that allowed users to toggle "favorite" statuses on products. Within an hour, our Sentry logs were flooded with 409 Conflict errors because users were double-clicking the button, triggering two identical mutations simultaneously. We needed a robust way to handle this, and that's when we dove deep into implementing Next.js Server Actions request collapsing to maintain order.

The Problem with Concurrent Mutations

When you trigger a Server Action from a client component, Next.js handles the POST request, but it doesn't inherently prevent a user from firing off five identical requests if they have a fast mouse click. If your action performs a database write—like updating a balance or incrementing a counter—you end up with race conditions where the state becomes inconsistent.

We first tried simple client-side debouncing using lodash.debounce. It felt like a quick win, but it failed because it only tracks the state in the browser's memory. If the user navigated away or refreshed the page before the debounce timer finished, the state synchronization broke. We needed something that lived closer to the execution layer.

Understanding Next.js Request Collapsing

In an ideal world, request collapsing ensures that if multiple identical requests arrive for the same resource within a tiny window, only the first one executes, and the subsequent ones receive the same result. While Next.js request memoization handles GET requests beautifully using the cache function, it does not apply to POST mutations.

For mutations, we need to implement concurrency control at the database or application level. If you've used tools like Redis for locking in other stacks, you'll find the logic familiar. You can check out how we approached similar problems in Laravel distributed locks, but in Next.js, we have to be more surgical.

Implementing a Locking Pattern

To prevent redundant mutations, we can use a simple "in-flight" tracking mechanism. Since Server Actions execute in a Node.js environment, we can leverage a shared cache or a dedicated lock manager.

Here is how I typically structure a guarded mutation:

TYPESCRIPT
// lib/actions/toggle-favorite.ts
import { cache } from CE9178">'react';
import { redis } from CE9178">'@/lib/redis';

export async function toggleFavorite(productId: string, userId: string) {
  const lockKey = CE9178">`lock:favorite:${userId}:${productId}`;
  
  // Try to acquire a lock for 2 seconds
  const acquired = await redis.set(lockKey, CE9178">'true', { NX: true, EX: 2 });
  
  if (!acquired) {
    throw new Error("Request already in progress. Please wait.");
  }

  try {
    // Perform your database mutation here
    return await db.product.update({ ... });
  } finally {
    // Release the lock
    await redis.del(lockKey);
  }
}

This pattern ensures that even if the client sends three requests, the Redis NX (Set if Not Exists) flag forces the second and third requests to fail or wait immediately. It’s effective, but it adds latency because of the extra round-trip to Redis.

Why Native Concurrency Control Matters

When building production pipelines, Next.js server actions: implementing type-safe mutations are only as strong as the validation wrapping them. If you don't handle the "in-flight" state, your database becomes the source of truth for your bugs.

I prefer a two-pronged approach:

  1. Optimistic UI: Update the UI immediately on the client side so the user perceives zero latency.
  2. Server-Side Guardrails: Use the Redis lock pattern shown above to ensure that even if the optimistic UI fails or the user bypasses it, the database remains protected.

If you're already managing observability, integrating this with Next.js AsyncLocalStorage allows you to trace exactly which requests were collapsed and why. It’s a game-changer for debugging production race conditions.

Trade-offs and Considerations

Is this overkill for every action? Probably. If your mutation is idempotent—meaning calling it five times has the same result as calling it once—don't waste time on locking. Only apply this to non-idempotent operations like financial transactions, inventory updates, or state toggles where the sequence matters.

One thing I'm still experimenting with is the lock duration. If you set it too short, you might have overlapping requests if the database is slow. If you set it too long, you might lock out legitimate retry attempts from the client. I've found that around 500ms to 1s is usually the "sweet spot" for most web applications.

Key Takeaways

  • Don't rely on client-side debouncing for critical state updates; it's too easy to bypass.
  • Use Redis NX locks to implement server-side request collapsing for non-idempotent mutations.
  • Combine with Optimistic UI to keep the user experience snappy while the server handles the heavy lifting.
  • Keep your locks short-lived to avoid blocking the user experience unnecessarily.

I’m currently looking into whether we can move some of this logic into a custom useAction hook that handles the "loading" state globally, but that's a topic for another day. For now, the explicit locking approach remains the most reliable way I've found to stop redundant mutations in their tracks.

Back to Blog

Similar Posts

Next.jsReactJune 21, 20264 min read

Next.js Server Actions: Implementing Idempotency and Atomic Mutations

Master Next.js Server Actions by implementing idempotency keys and atomic mutations. Prevent duplicate requests and ensure data integrity in distributed systems.

Read more
Detailed view of a server rack with a focus on technology and data storage.
Next.js
React
June 21, 2026
4 min read

Next.js Request Memoization: Stop Over-Fetching in Server Components

Next.js request memoization prevents redundant data fetching across nested components. Learn how to use the React cache function to boost performance.

Read more
Serene long exposure of a cascading waterfall surrounded by lush greenery in Shifen, Taiwan.
Next.jsReactJune 20, 20264 min read

Next.js App Router Data Fetching: Avoiding Performance Waterfalls

Learn how to master Next.js App Router data fetching by parallelizing server requests. Stop blocking your renders and fix performance waterfalls today.

Read more