Episode 9 · 7 min

Rate limits outside your code: per IP, per session, per API key

Limit what you can't block: rate limits by route, method, app and environment, counted per IP, globally, per API key or per session, shared across servers, and added in plain English or by your agent.

What you'll learn

  • ✓How a SecureNow rate limit works: route, method, limit, window, and a 429 your route never sees
  • ✓Count per IP, globally, per header value (API key) or per cookie value (session)
  • ✓Why per-IP limits fail against rotating addresses, and how the agent fixed it with a per-session limit
  • ✓Write limits in plain English, stack them, and share one counter across servers with Redis

Rate limits outside your code: per IP, per session, per API key

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. Everything below is from a real run on tutorial/ep8-9-firewall, with securenow@8.14.

Limits baked into code

Northstar already had one, written into the route:

// src/config.js
authRateLimit: 15,
rateWindowMs: 15 * 60_000,
// src/app.js
app.post('/api/auth/login', limit('login-ip', config.authRateLimit), async (req, res) => {

Changing it is a code change, a review and a deploy. And the new notes export shipped with no limit at all.

How a SecureNow limit works

A rule names a route and method (optionally an IP or range, an app, an environment), a limit and a window. The SDK enforces it before your route runs. Over the 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

Rules stack: a 5-per-minute burst limit and a 20-per-hour cap can both apply to the same route.

Who gets counted

keyByBudgetExample
ip (default)one per address—
globalone for everyoneprotect an expensive endpoint as a whole
headerone per header valuekeyName: "x-api-key": one per API key, from any address
cookieone per cookie valuekeyName: "session": one per signed-in session

Header and cookie values are hashed before they're used and never stored. A request without the header or cookie is counted by its address.

Step 1 — The obvious rule, and why it fails

securenow ratelimit add --route /api/notes/export --mode exact --method GET --limit 10 --window 1m
# ACTIVE GET exact:/api/notes/export at 10/60s per ip

The scraper then used one signed-in session from 100 rotating addresses. In the next minute: 275 exports, all 200.

Step 2 — Ask the agent why

Someone is still pulling the notes export on lab-auth production, even though we put a 10-per-minute-per-IP
rate limit on it. Using SecureNow, find out why that limit isn't stopping it, and propose a rate limit that
will, without changing any code. Don't apply anything yet.

It found about 245 exports a minute from 100 addresses, none above 3 a minute, so a per-IP limit of 10 could never trip; one client, every request a signed-in export. (In this lab the addresses are forged X-Forwarded-For headers, which the app trusts from loopback; it flagged that as a code fix for later. In the wild they come from proxy pools.) Its proposal: count by the session cookie, 10 exports a minute per session. Normal clients had never gone above 5.

Step 3 — Approve

One securenow_rate_limit_create call:

{ "method": "GET", "pathPattern": "/api/notes/export", "pathMatchMode": "exact",
  "keyBy": "cookie", "keyName": "lab_auth", "limit": 10, "windowSeconds": 60,
  "environment": "production", "duration": "2h" }

The 429s started six seconds later. securenow_firewall_hits showed 60 hits from 60 different addresses in twelve seconds, all on the one cookie: proof they shared a session. From then on the scraper got 10 exports a minute instead of 245.

Step 4 — Plain English

securenow ratelimit parse "rate limit POST /api/auth/login to 5 attempts per minute per IP"
#   Target  POST prefix:/api/auth/login   Limit 5 / 60s   Key by ip
securenow ratelimit from-text "rate limit POST /api/auth/login to 5 attempts per minute per IP" --yes

A password spray from one address: five wrong passwords got through, then every attempt got a 429 with Retry-After counting down (55, 54, 53…). Stack an hourly cap with one more sentence:

securenow ratelimit from-text "and no more than 20 login attempts per hour per IP on POST /api/auth/login" --yes
securenow ratelimit test 192.0.2.50 --path /api/auth/login --method POST
# RATE LIMITED - 2 matching rules

ratelimit add also takes --header x-api-key or --cookie session.

Step 5 — The dashboard

Rate Limits: describe the limit in the natural-language box and it fills in the form (route, method, bucket, cookie name, window). Nothing is created until you save. The rule list shows what each rule counts by, and Rate-limit hits shows what each rule stopped: here, over a thousand exports on the session rule, from 100 addresses.

Step 6 — One limit across many servers

Each process counts on its own. With production running as two processes, a 10-per-minute rule let 20 through (10 from each). One line in each deployment's .securenow/runtime.json:

"firewall": { "rateLimitStore": "redis://127.0.0.1:6379" }

Same test: 11 a minute across both. The SDK never waits on Redis to decide; if Redis goes away, each process falls back to counting on its own; for cookie and header rules, Redis only ever sees a hash of the value.

What you just did

You saw a per-IP limit fail against rotating addresses, let your agent find the real key and fix it without code, wrote limits in plain English, and made one limit hold across servers. Block what you must. Limit everything else. Both outside your code.

Try it on your own app

Start the threat model, then plug SecureNow in — the whole series works on your project today.