Skip to the solution
MediumSystem Design6 min read

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

564 views

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.

  • security
  • express
  • middleware

Try it yourself

Sketch your approach before you read the solution. That is what the interview tests.

Stuck? Get a hint.

A short nudge toward the approach. It will not give the answer away.

Solution

The solution is hidden so you can try first

6 min read Β· 8 sections

Read the code

JavaScriptA 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);

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 84 of 152 in Node.js

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

All Node.js questions
Up nextMedium

How do you run TypeScript directly in Node.js (native type stripping, tsx, ts-node)?

Continue