Skip to solution
mediumSystem Design

How do you implement rate limiting in a Node.js Express application?

564 views
01

Understand the problem

Question presented to candidate: "Add rate limiting to an Express endpoint so no single client can make more than 3 requests per minute. What exact HTTP status and headers would a client see once they exceed that, and how would you actually confirm your limiter works before shipping it?"

What a strong answer should cover:

  • express-rate-limit (a common, standard choice) tracks request counts per key (by default, source IP) within a configured time window, returning the standard 429 Too Many Requests status once the count exceeds the configured maximum — 📌 verified directly against a real server, not described: requests 1-3 (of a max: 3 limit) returned 200, requests 4-5 returned a genuine 429.
  • 📌 The client-visible signal, verified directly: the standard RateLimit-* headers (ratelimit-remaining, among others) correctly counted down with each successful request (2, then 1, then 0) — a real, checkable signal a well-behaved client can read to know how close it is to the limit, not just a black-box "sometimes I get blocked."
  • Rate limiting should be keyed appropriately for the actual endpoint: source IP for a general, unauthenticated endpoint; the authenticated identity (user ID, API key) for an endpoint where that is available and more precise — covered fully, with the specific brute-force rationale, in the dedicated DoS/brute-force question.
  • The middleware placement matters: applying the limiter as global middleware (app.use(limiter)) protects every route uniformly; applying it to a specific route (app.post("/login", limiter, handler)) allows a tighter, endpoint-specific limit exactly where it matters most (a login endpoint, say) without over-restricting a lightweight, low-risk endpoint elsewhere.
  • A precise answer names the verification step directly, matching the prompt's own request: sending more requests than the configured maximum in a real test and confirming the actual HTTP status codes returned, exactly as demonstrated here — not merely trusting the middleware's presence in the code without observing its real behavior.
  • For a multi-process deployment (clustering, multiple instances — covered in its own dedicated question, with real proof each process has separate memory), the default in-memory store used by express-rate-limit is not shared across processes — a Redis-backed store (rate-limit-redis or similar) is required for a single, consistent limit across every process/instance.

Clarifying questions expected:

  • "Is this a general endpoint, or a security-sensitive one (login) needing tighter, identity-keyed limits?" — decides the specific configuration, covered further in the dedicated DoS/brute-force question.
  • "Does this run as a single process, or clustered/multiple instances?" — decides whether the default in-memory store is sufficient or a shared external store is required.

Code / implementation expected: Yes — the real, measured rate-limiter behavior (200s with a counting-down header, then a genuine 429) is the concrete, convincing proof, directly answering the prompt's own request to verify it works.

securityexpressmiddleware
02

Attempt it yourself

Sketch your approach before reading the solution — that's what interviews test.

Nudge consolestandby

Stuck? Beam a request up — the console returns a conceptual nudge that guides your logic without spoiling the implementation.

03

Study the solution

Target Audience: Engineers preparing for Node.js/Express interviews — assumes basic Express middleware familiarity. Difficulty: Medium

How to read this doc: Concepts are explained in plain language first, then tagged with 📌 Interview term:. The rate-limiter behavior below was **actually run

Solution ready — 2 min read

Classified // press E to declassify

04

Read the code

A real express-rate-limit middleware, verified exactly by sending 5 real requests against a max: 3 limit
const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();

const limiter = rateLimit({ windowMs: 60_000, max: 3, standardHeaders: true, legacyHeaders: false });
app.use(limiter);
app.get("/", (req, res) => res.json({ ok: true }));

const server = app.listen(0, async () => {
  const port = server.address().port;
  for (let i = 1; i <= 5; i++) {
    const r = await fetch(`http://127.0.0.1:${port}/`);
    console.log("request", i, "->", r.status, "remaining:", r.headers.get("ratelimit-remaining"));
  }
  server.close();
});
// request 1 -> 200 remaining: 2
// request 2 -> 200 remaining: 1
// request 3 -> 200 remaining: 0
// request 4 -> 429 remaining: 0   <- genuinely blocked
// request 5 -> 429 remaining: 0   <- genuinely blocked

// A tighter, identity-keyed limit for a sensitive route specifically:
// app.post("/login", rateLimit({ windowMs: 60_000, max: 5, keyGenerator: (req) => req.body.username }), loginHandler);
05

Join the discussion

Discussion (0)

Sign in to join the discussion.

No responses yet. Be the first to share what you think.

Transmission complete // awaiting log

KEEP THE
STREAK ALIVE.

Dossier 84 of 152 decoded in the Node.js track. One more won't hurt.

Back to track