Documentation

Advanced

Debug unexpected 429 errors

Find which VitNode rate-limit budget a visitor ran out of, using the X-RateLimit-Category header and the API's rate-limit log line, then fix the cause.

When a real visitor sees "Too many requests", find out which rate-limit budget refused them before you raise anything. Raising the wrong budget hides the cause and weakens protection you still need.

Read the refused budget

Open the browser's Network tab, repeat what the visitor did, and select the first request with status 429. The X-RateLimit-Category response header names the budget:

ValueWhat ran outUsual cause
readGET / HEAD budgetA loader that fetches on every preload, or a polling screen
writeMutation budgetA form that submits in a loop, or autosave on every keystroke
authSign-in budgetRepeated failed sign-ins, often a password manager retrying
content-writePublic Content Engine create and edit budgetA member, or a script, posting very quickly
safetyThe cap on every requestA client sending requests in parallel, or many people on one IP

Find the route in the API log

The API prints one line the first time a client goes over a budget in each window:

[VitNode] Rate limit exceeded: {"category":"read","subject":"user","method":"GET","path":"/api/@vitnode/forum/content/topics/:param","status":429,"retryAfter":24,"store":"redis"}

subject says whether a signed-in member (user) or an IP address (ip) ran out, path is the route template with IDs and slugs replaced, and store says whether the counters live in redis or in each instance's memory. The line never includes the member's ID, the IP address, cookies, headers or the query string, and later refusals in the same window are not logged, so a flood stays one line per client.

Fix the cause

  • read from navigation: check that the page's loader reads through the query cache with a staleTime, so hovering a link twice or hovering and then clicking costs one request. For a page that should never load early, set preload: false.
  • write or content-write: debounce autosave, and disable the submit button while a request is pending.
  • Many people behind one IP: an office or school shares one public address. Signed-in members already have their own read and write budgets, but guests share them, and everyone shares the safety cap. If subject is ip and the category is safety, raise rateLimiter.safety.points; if guests run out of reads, raise rateLimiter.read.points.
  • Every server-rendered request in one bucket: the API sees your web server instead of the visitor. Set up real visitor IPs.
  • Limits differ between instances: with store set to memory, each API instance counts on its own. Set REDIS_URL to share counters.

Check the result

Repeat the visitor's steps with the Network tab open. You see no 429, and the API prints no new Rate limit exceeded line.

Why a member's request is counted later

A request with a session cookie spends its read and write budgets only after the API has checked the session, so the budget belongs to a verified member. Guests are counted before that check, and the per-IP safety cap limits how many session checks one address can cause.