Rate Limit per API Key, per Session or per IP in Node.js, Without Code Changes
Add rate limits by route and method, counted per IP, per API key, per session cookie or globally, written in plain English and shared across servers with Redis.
Rate Limit per API Key, per Session or per IP in Node.js, Without Code Changes
Most abuse doesn't come from addresses you can simply block: a real account pulling data too fast, a password guesser on a shared network, a scraper that changes its address on every request. For those you want a limit, not a wall. And you want it on the endpoint that is being abused today, without waiting for a code change, a review and a deploy.
This guide covers SecureNow rate limits end to end: how a rule works, how to count per IP, per API key, per session or globally, how to write rules in plain English, how to make one limit hold across many servers, and how to check what each rule stopped. Every command can be copied as is, and every output is from the real run in Academy Episode 9: Rate limits outside your code.
Quick start
With the SDK installed and your app started with node -r securenow/register (see the quick start in the IP blocking guide):
# 5 login attempts per minute per IP, written in plain English
npx securenow ratelimit from-text "rate limit POST /api/auth/login to 5 attempts per minute per IP" --yes
# 10 exports per minute per signed-in session, for 24 hours
npx securenow ratelimit add --route /api/notes/export --mode exact --method GET \
--limit 10 --window 1m --cookie connect.sid --duration 24h --name "Export per session"
The rules reach every process running the SDK on its next check, within about ten seconds. ratelimit add uses your connected app and production unless you pass --app and --env.
How a rule works
A rule names what to match, how much to allow, and what to count by:
| Field | CLI flag | Example |
|---|---|---|
| Route | --route | /api/auth/login |
| Route match | --mode | exact, prefix (default), regex |
| Method | --method | POST (default: all methods) |
| Address or range (optional) | first argument | 203.0.113.0/24 |
| App · environment | --app · --env | your app key · production |
| Limit | --limit | 5 |
| Window | --window | 30s, 1m, 1h (default 1m) |
| Count by | --key-by | ip (default), global, header, cookie |
| Header or cookie name | --header · --cookie · --key-name | x-api-key · connect.sid |
| Expiry | --duration | 2h, 24h, 7d, or omit for permanent |
| Label | --name · --reason | shown in lists and hits |
Over the limit, the request gets a 429 and your route never runs. This is the real response from the lab, a login attempt over a 5-per-minute limit:
HTTP/1.1 429 Too Many Requests
Retry-After: 36
X-RateLimit-Limit: 5
X-RateLimit-Window: 60
X-SecureNow-Rate-Limit-Rule: 6ae0bfa742b3728bc9bb7a00
{"error":"Too Many Requests","ip":"192.0.2.61","retryAfter":36}
Clients can back off using Retry-After; you can find the rule that answered from X-SecureNow-Rate-Limit-Rule.
Who gets counted
The most important choice is the key: whose budget a request spends.
--key-by | One budget per… | Use it for |
|---|---|---|
ip (default) | client address | logins, signups, password resets |
global | the whole endpoint | an expensive endpoint you need to protect as a whole |
header | value of a request header | public APIs: one budget per API key |
cookie | value of a cookie | signed-in apps: one budget per session |
Header and cookie values are hashed (SHA-256) before they are used as a bucket key and are never stored. A request that doesn't carry the header or cookie is counted by its IP, so anonymous traffic can't slip through a header rule. Header names are case-insensitive.
Per IP
npx securenow ratelimit add --route /api/auth/login --mode exact --method POST --limit 5 --window 1m
Per API key
npx securenow ratelimit add --route /api/v1 --mode prefix --limit 600 --window 1m --header x-api-key --name "600/min per API key"
Every key gets 600 requests a minute, whether it calls from one server or a hundred.
Per session
npx securenow ratelimit add --route /api/notes/export --mode exact --method GET --limit 10 --window 1m --cookie connect.sid
Use your session cookie's real name: connect.sid is the express-session default, Auth.js uses authjs.session-token, NextAuth v4 uses next-auth.session-token. Check your browser's dev tools, and note that many apps use a __Host- or __Secure- prefixed name in production, which is a different cookie name.
Global
npx securenow ratelimit add --route /api/reports/generate --mode exact --method POST --limit 30 --window 1m --key-by global
A global budget protects the endpoint, but one client can use it up for everyone; pair it with a per-session or per-key rule.
For one range only
npx securenow ratelimit add 203.0.113.0/24 --route /api --mode prefix --limit 30 --window 1m
Stack rules
Rules stack. A short burst limit and an hourly cap can both apply to the same route:
npx securenow ratelimit from-text "rate limit POST /api/auth/login to 5 attempts per minute per IP" --yes
npx securenow ratelimit from-text "and no more than 20 login attempts per hour per IP on POST /api/auth/login" --yes
npx securenow ratelimit test 192.0.2.50 --path /api/auth/login --method POST
RATE LIMITED - 2 matching rules
6ae0c01042b3728bc9bb8da3 POST prefix:/api/auth/login 20/3600s per ip
6ae0bfa742b3728bc9bb7a00 POST prefix:/api/auth/login 5/60s per ip
Write rules in plain English
ratelimit parse shows what a sentence turns into, without creating anything:
npx securenow ratelimit parse "rate limit POST /api/auth/login to 5 attempts per minute per IP"
Name Limit login attempts
Target POST prefix:/api/auth/login
Limit 5 / 60s
Key by ip
Duration 24h
Provider anthropic
Confidence 0.98
ratelimit from-text "…" --yes creates it. The parser understands rates like "5 per minute", "10/min", "20 rps" and "1000 per hour", and keys like "per IP", "per API key" (the x-api-key header), "per header X-Customer-Id" and "per cookie sid". "Per session" assumes a cookie named session, so name yours ("per cookie connect.sid") or pass --cookie. A rule written in plain English expires after 24 hours unless the sentence or --duration says otherwise. You can also pass --header, --cookie or --env to fill in what the sentence leaves out.
In the dashboard, Rate Limits has the same box: describe the limit and it fills in the form (route, method, bucket, cookie name, window). Nothing is created until you save.
One limit across many servers
Each process counts on its own. If you run several Node processes (PM2 cluster, containers, several servers), a 10-per-minute rule allows up to 10 per minute in each of them. In our lab, production ran as two processes and the same session got 20 exports a minute through a 10-per-minute rule.
Add one line to each deployment's .securenow/runtime.json:
"firewall": {
"rateLimitStore": "redis://127.0.0.1:6379"
}
Use rediss:// for TLS, and redis://:password@host:6379/0 (or redis://user:password@…) for authentication and a database number. On startup the log confirms it:
[securenow] Firewall: shared rate-limit counters: connecting to redis://127.0.0.1:6379/0
[securenow] Firewall: shared rate-limit counters ON (redis://127.0.0.1:6379/0)
Same test: 11 a minute across both processes. Details that matter in production:
- The request path never waits on Redis. The SDK decides from the shared total it already knows and reports each counted request in the background, so at most about one extra request per process can pass while a reply is in flight.
- Fail-open to in-process. If Redis is unreachable, each process falls back to counting on its own and reconnects with backoff.
- Windows align to the clock when shared, so every process agrees on when a minute starts.
- No raw identifiers. For header and cookie rules, the Redis key holds a hash of the value, never the API key or session itself:
securenow:rl:6ae0bf43…|cookie:8d101f4bea36f4e3ef2def0e:29885242 count=11
No extra npm dependency is needed; the SDK ships a minimal Redis client.
See what each rule stopped
npx securenow firewall hits --since 1h --action rate_limited
By rule
303 429 Export: 10 per minute per session 100 IPs
64 429 Limit login attempts 1 IP
The dashboard's Rate-limit hits panel shows the same per rule and per address, and ratelimit list shows what each rule counts by:
npx securenow ratelimit list --env production
ID Status Target Limit Per
6ae0bfa742b3728bc9bb7a00 ● active POST prefix:/api/auth/login 5/60s ip
6ae0bf4342b3728bc9bb78cc ● active GET exact:/api/notes/export 10/60s cookie lab_auth
Manage rules
npx securenow ratelimit show <id>
npx securenow ratelimit disable <id>
npx securenow ratelimit enable <id>
npx securenow ratelimit remove <id> --reason "Incident closed"
Rules created with --duration expire on their own, and expired rules show as expired.
From your coding agent
Over MCP, the agent has securenow_rate_limit_create (structured fields), securenow_rate_limit_create_from_text, securenow_rate_limit_update (change the limit, window, route or key, enable or disable), securenow_rate_limit_test and securenow_firewall_hits. This is the call our agent made after we approved its proposal:
{
"method": "GET",
"pathPattern": "/api/notes/export",
"pathMatchMode": "exact",
"keyBy": "cookie",
"keyName": "lab_auth",
"limit": 10,
"windowSeconds": 60,
"environment": "production",
"duration": "2h"
}
Six seconds later, sixty different addresses hit the same session's limit. The full workflow and prompts are in Let your coding agent block attackers and add rate limits over MCP.
Choosing a limit
- Measure normal use first. Before proposing 10 exports a minute, our agent checked that no ordinary client had gone above 5.
firewall hitsand your traces show real peaks. - Start with an expiry.
--duration 24hduring an incident; make it permanent once you've watched the hits. - Key on identity when you have it. Per-IP limits are easy to sidestep with rotating addresses; see Why per-IP rate limits fail against rotating IPs.
- Keep blocks for traffic with no legitimate use. See Block an IP, a range or one route without a deploy.
Limits worth knowing
- Windows are fixed, not sliding. Without a shared store, a client that times its requests across a window boundary can briefly get up to twice the limit.
- Trusted IPs are still rate limited. Trust affects blocks, not budgets.
- Blocks are checked before rate limits, so a blocked address never spends a budget.
- Header and cookie keys and shared counters need
securenow8.14 or later.
SecureNow rules or express-rate-limit?
They solve different problems. A limiter in your code ships with your code: good for limits that are part of your product's contract, reviewed with the endpoint. SecureNow rules live outside the code: good for the limit you need on the endpoint being abused right now, for a new endpoint that shipped without one, and for limits owned by whoever is on call. Many teams use both. If you're tuning an in-code limiter, see Express rate limiting: production setup, Redis and proxies.
Related
- Academy Episode 9: Rate limits outside your code
- Why per-IP rate limits fail against rotating IPs
- Block an IP, a range or one route without a deploy
- Let your coding agent block attackers and add rate limits over MCP
- Express rate limiting: production setup, Redis and proxies
- CLI reference: ratelimit · CLI + MCP · The SecureNow firewall
Frequently Asked Questions
Can I rate limit per API key instead of per IP?
Yes. Count by a request header with --header x-api-key (keyBy: header, keyName: x-api-key). Every API key gets its own budget, whichever addresses it comes from. The header value is hashed before it is used and never stored.
How do I rate limit per logged-in user?
Count by your session cookie, for example --cookie connect.sid for express-session. Every signed-in session gets its own budget. A request without the cookie is counted by its IP.
Do rate limits work across several servers or PM2 workers?
Each process counts on its own by default. Set config.firewall.rateLimitStore to a redis:// or rediss:// URL and every process counts against one shared total. If Redis is unreachable, counting falls back to each process.
What does a rate-limited client receive?
A 429 Too Many Requests with Retry-After, X-RateLimit-Limit, X-RateLimit-Window and X-SecureNow-Rate-Limit-Rule headers. Your route handler never runs.
Which SDK version do I need?
Header and cookie keys and shared Redis counters need securenow 8.14 or later. Older SDKs treat header and cookie rules as per-IP rules.
Recommended reading
One login, 100 rotating IPs: a 10-per-minute-per-IP limit let 275 of 275 requests through. How to spot rotation in your traffic and what to key your limits on.
Oct 27Failed-login spikes, 404 bursts, one IP trying many usernames: how to tell if your web app is under attack, confirm it in your logs, and act in hour one.
Oct 26Those /.env, /.git and wp-login.php hits are bots hunting leaked secrets. See which matter, check you expose nothing, and what to do if one returns 200.
Oct 26