Idempotency in REST APIs: Preventing “Double Charges” with Node.js and Redis

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Why Is Idempotency So Important?

Put yourself in the customer’s shoes: You click the “Pay” button for a 1 million VND order. The network lags, and the loading spinner keeps spinning. Impatiently, you click three more times. By the time you get the notification, your account has been charged 4 million VND. This isn’t the user’s fault—it’s a serious flaw in the backend logic.

Data from major payment systems shows that duplicate requests due to retries (from the client or network) can account for 0.5% – 2% of total traffic. Without a control mechanism, your database will soon become a mess.

When I worked at a Fintech startup, a network congestion issue once caused the system to record hundreds of duplicate transactions. I had to stay up all night manually rolling back data. After that shock, I realized that Idempotency (consistency upon repetition) is a must-have for every sensitive API.

The Natural Behavior of HTTP Methods

Not every method requires idempotency handling. Let’s look at their design:

  • GET, HEAD: Naturally read-only. Whether you call it once or 1,000 times, the data on the server remains the same.
  • PUT: Used for overwriting. If you send the same payload multiple times, the final state remains identical.
  • DELETE: Removes a resource. Once deleted, subsequent calls simply confirm that the resource no longer exists.
  • POST: This is the source of all trouble. Each POST call usually creates a new record (Order, Transaction). We need to focus our handling on this method.

The Idempotency Key Strategy

The most optimal solution is to use an Idempotency Key sent in the Request Header. The standard process is as follows:

  1. The client generates a unique UUID for each action and sends it via the x-idempotency-key header.
  2. The server receives the request and checks for this Key in Redis.
  3. If the Key is found: The server immediately returns the previously cached result without re-running the business logic.
  4. If it is a completely new Key: The server processes the request, stores the result in Redis with a TTL (Time To Live), and then responds to the Client.

Hands-on: Building Middleware with Node.js and Redis

We will use Express.js combined with Redis to create a robust protection layer for the API.

1. Setting up the Environment

Install the necessary libraries to get started:

npm install express redis uuid

2. Initializing the Redis Connection

Redis is the perfect choice due to its extremely fast access speed (under 1ms) and its ability to automatically expire keys after a certain period (TTL).

const express = require('express');
const redis = require('redis');
const { v4: uuidv4 } = require('uuid');

const app = express();
app.use(express.json());

const redisClient = redis.createClient();
redisClient.connect().then(() => console.log('✅ Redis is ready'));

const CACHE_TTL = 3600; // Cache results for 60 minutes

3. Writing Intelligent Middleware

This middleware will act as a gatekeeper, preventing duplicate requests before they reach the Controller.

const idempotencyMiddleware = async (req, res, next) => {
  const key = req.headers['x-idempotency-key'];
  if (!key) return next();

  try {
    const cached = await redisClient.get(`idempotency:${key}`);
    if (cached) {
      const { status, body } = JSON.parse(cached);
      return res.status(status).json(body);
    }

    // Override res.send to capture the response body
    const originalSend = res.send;
    res.send = function (body) {
      if (res.statusCode >= 200 && res.statusCode < 300) {
        redisClient.setEx(`idempotency:${key}`, CACHE_TTL, 
          JSON.stringify({ status: res.statusCode, body: JSON.parse(body) })
        );
      }
      return originalSend.call(this, body);
    };
    next();
  } catch (err) {
    next(err);
  }
};

Three Crucial Considerations for Real-World Implementation

Don’t rush to copy this code into production just yet; you need to address these three additional issues:

Avoid Race Conditions: If two requests arrive within the same millisecond, Redis might not have saved the first key yet. Use the SET NX command to create a temporary lock as soon as the request is received.

Key Scoping: Do not fully trust the UUID from the client. Combine it with a User ID (e.g., idempotency:user_99:key_abc) to prevent key collisions between different accounts.

Only Cache Success: Absolutely do not cache 500 or 503 errors. If your system crashes during processing, you must allow the client to retry once the server has recovered.

Conclusion

Implementing Idempotency is not just a technical task; it is a responsibility toward your customers’ data. With Node.js and Redis, you can build a highly effective protection layer with just a few lines of code. Apply it to your payment or ordering APIs today to make your system more professional and reliable.

Share: