Express rate limiting,per API key, not per IP.
One app.use() checks each caller’s API key, charges their credits, and applies the rate limit on their plan — the same count on every instance, with nothing to store yourself.
- Keys issued per customer
- Credits that refill
- Limits per plan, not per process
Official SDKs with drop-in middleware for the stack you already run
- Python
- Node.js
- Go
- Rust
- PHP
- .NET
- Java
- API requests validated
- 100M+
- Average key validation
- <5ms
- Average analytics ingest
- <5ms
- Check and log, end to end
- <10ms
The usual Express rate limiter counts requests. It doesn’t know your customers.
express-rate-limit is the default choice: a window, a limit, and a keyGenerator that decides who gets counted. Out of the box it counts per IP in memory.
// server.ts — with express-rate-limitimport { rateLimit } from "express-rate-limit";
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100,
standardHeaders: "draft-7",
legacyHeaders: false,
keyGenerator: (req) => req.get("x-api-key") ?? "anonymous",
});
app.use(limiter);- The memory store is per process — PM2 cluster mode or a second container doubles the limit
- Needs Redis (or another shared store) before the limit is correct on more than one server
- A keyGenerator can read a header, but nothing checks that the key is real
- No API keys: issuing, hashing, scoping, and revoking them is still yours to build
- No credit balance: a request can’t cost 5 on one route and 1 on another, or refill each month
- One limit for everyone — no per-plan limits for Free, Pro, and Enterprise customers
- API keys issued per customer, prefixed, hashed, and revocable from the dashboard
- A credit balance per customer — price each route, refill every hour, day, week, or month
- Rate limits set per plan, shared by all of a customer’s keys, from 1 second to 24 hours
- The same count on every worker, instance, and region — no Redis to run
- Over the limit? A 429 with Retry-After, and no credits charged
- Every request logged per customer — status, latency, endpoint — in under 5ms
Express in three steps, one of them code.
ReqKey runs inside your API, not in front of it. Your server asks one question per request and gets an answer in under 5ms.
- 1
Install the SDK
npm install reqkey express - 2
Set your project key
Copy it from the dashboard into
REQKEY_PROJECT_KEY. It stays on your server. - 3
Add the Express middleware
Every request is checked, charged, and logged before your handler runs.
import express from "express";import { reqkey } from "reqkey/express";import type { ReqKeyExpressRequest } from "reqkey/express"; const app = express(); app.use( reqkey({ projectKey: process.env.REQKEY_PROJECT_KEY, apiId: "api_payments", mode: "both", // validate keys AND record analytics keyName: "x-startup-key", // where consumers send their key excludePaths: ["/health", "/docs/*"], }),); app.post("/payments", (request, response) => { const decision = (request as ReqKeyExpressRequest).reqkey; response.status(201).json({ created: true, creditsRemaining: decision?.creditsRemaining });}); app.listen(3000);Every request gets one of these answers
- 200Valid key, credits charged — your handler runs
- 402Out of credits
- 403Key disabled, or not allowed on this API
- 429Over the rate limit — no credits charged
Where ReqKey sits
Responses go straight back to your customer. ReqKey sees the key check and the log line, nothing else.
Credits
Charge each route what it costs you.
A lookup can cost 1 credit and a render 5, from the same balance. Excluded paths are never validated, charged, or recorded. In Node.js:
// Exact paths or trailing-* prefixes: never validated, charged, or recordedexcludePaths: ["/health", "/openapi.json", "/docs/*", "/cron/*"] // Or decide per request with a sync or async resolvershouldProtect: ({ path }) => path.startsWith("/api/") // Charge different endpoints differently (non-negative integers)credits: ({ method, path }) => { if (method === "POST" && path === "/images") return 5; if (path.startsWith("/reports/")) return 2; return 1;}Rate limits
Set limits on plans, not in code.
Your Express code never hard-codes a number. Each plan carries its credits, refill, and rate limit; moving a customer to Pro changes all three with no deploy.
Example plans. You name them and pick the numbers.
Notes for Express teams hit in production.
Real keys, not just a header
ReqKey rejects unknown, disabled, and out-of-credit keys before your handler runs. With express-rate-limit, any string in the header gets its own fresh bucket.
Cluster mode and serverless
Counts and balances live in ReqKey, so PM2 clusters, Kubernetes replicas, and serverless functions all see the same number.
Charge heavy routes more
Pass a credits function to charge 5 for an image render and 1 for a lookup — the same balance, priced by what each route costs you.
Express rate limiting: the questions teams ask.
Something else? Ask the team or read the docs.
express-rate-limit counts requests per keyGenerator value in a store you choose. ReqKey issues and validates the API keys, keeps a credit balance per customer, applies each customer’s plan limit, and logs every request — all from one middleware.
A 429 with a Retry-After header and the stable error code rate_limited. A 429 costs no credits.
On their plan or on the customer (consumer) in ReqKey — a number of requests per window from 1 second to 24 hours, shared by all of that customer’s keys. Your Express code never hard-codes a limit, so upgrading a customer is a dashboard change, not a deploy.
A check averages under 5ms. ReqKey runs inside your app as middleware, not as a gateway in front of it, so responses go straight back to your customer.
You choose: fail closed and answer 503, or fail open and let requests through. Invalid keys are denied either way, and a validation is never retried, so no one is charged twice.
Ship API keys in Expressin five minutes.
Free for your first 5 million requests every month. No card, no gateway, no rewrite.