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.

Oct 27, 2026·12 min read

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:

FieldCLI flagExample
Route--route/api/auth/login
Route match--modeexact, prefix (default), regex
Method--methodPOST (default: all methods)
Address or range (optional)first argument203.0.113.0/24
App · environment--app · --envyour app key · production
Limit--limit5
Window--window30s, 1m, 1h (default 1m)
Count by--key-byip (default), global, header, cookie
Header or cookie name--header · --cookie · --key-namex-api-key · connect.sid
Expiry--duration2h, 24h, 7d, or omit for permanent
Label--name · --reasonshown 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-byOne budget per…Use it for
ip (default)client addresslogins, signups, password resets
globalthe whole endpointan expensive endpoint you need to protect as a whole
headervalue of a request headerpublic APIs: one budget per API key
cookievalue of a cookiesigned-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 hits and your traces show real peaks.
  • Start with an expiry. --duration 24h during 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 securenow 8.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.

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

Why Per-IP Rate Limits Fail Against Rotating IPs (and What to Count Instead)

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 27
How to Tell If Your Web App Is Under Attack (and What to Do)

Failed-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 26
Why Is My App Getting /.env and wp-login.php Requests?

Those /.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