Rate-Limit Elicitation
Rate-limiting logic must identify the target being limited — and in many implementations, that means resolving the resource before incrementing a counter or checking a quota.
Rate-limiting logic must identify the target being limited — and in many implementations, that means resolving the resource before incrementing a counter or checking a quota. When rate-limit buckets are scoped per-resource, the server must find the resource to locate or create its bucket. If the resource doesn't exist, the server returns 404 before any rate-limit tracking occurs. The differential between 429 (or rate-limit headers on a 200) and 404 confirms existence.
Forcing 429 via Per-Resource Rate Limits
(Applies to: All methods) — Non-destructive when using GET/HEAD. ⚠️ Destructive when using POST/PUT/PATCH/DELETE if any requests succeed before the limit is hit.
Mechanism: Per RFC 6585 §4, a server returns 429 when the client has sent too many requests in a given time window. The oracle depends on where rate-limit counters are scoped:
- Global limit (no oracle): Gateway tracks requests per IP/API-key across all endpoints. No differential.
- Per-endpoint limit (weak oracle): Server tracks requests per endpoint pattern. Oracle only works if the server resolves the resource before incrementing the counter.
- Per-resource limit (strong oracle): Server tracks requests per specific resource instance. The server must resolve the resource to locate its bucket.
Isolated Variable: The request is completely standard. The only manipulation is sending a high volume of identical requests in rapid succession.
Oracle Signal: 429 after N requests (exists, rate-limit bucket found) vs 404 on every request (does not exist).
GET — Existing Resource (Rate-Limited After Burst)
GET /api/users/1001 HTTP/1.1
Host: target.com
Authorization: Bearer valid-token(Sent 100 times in 10 seconds)
Requests 1–50:
HTTP/1.1 200 OK
RateLimit-Limit: 50
RateLimit-Remaining: 0
RateLimit-Reset: 10
{"id": 1001, "name": "Alice"}Requests 51–100:
HTTP/1.1 429 Too Many Requests
Retry-After: 10
RateLimit-Limit: 50
RateLimit-Remaining: 0
{"error": "Too Many Requests", "detail": "Rate limit exceeded. Try again in 10 seconds."}GET — Non-Existing Resource (Never Rate-Limited)
GET /api/users/9999 HTTP/1.1
Host: target.com
Authorization: Bearer valid-token(Sent 100 times in 10 seconds) — All 100 requests:
HTTP/1.1 404 Not Found
{"error": "Not Found"}HEAD — Minimal Bandwidth Variant
HEAD requests are ideal for rate-limit probing because they return no body, minimizing bandwidth and reducing the chance of triggering body-size-based abuse detection.
HEAD /api/users/1001 HTTP/1.1
Host: target.com
Authorization: Bearer valid-token(Sent 100 times in 10 seconds)
Requests 1–50: 200 OK with RateLimit-Remaining decrementing.
Requests 51–100: 429 Too Many Requests with Retry-After: 10.
vs. all 404 Not Found for /api/users/9999.
💡 Distinguishing global vs per-resource limits: Send the same burst against a known-invalid resource ID and the target resource ID in parallel. If both produce
429, the limit is global (no oracle). If only the existing resource produces429while the invalid ID consistently returns404, the limit is per-resource and the oracle is confirmed.
💡 Rate-limit counter as a side-channel existence test: Even if you never trigger
429, the mere presence or absence of rate-limit response headers on successful responses reveals whether the server resolved the resource. See header fingerprinting below.
💡 Noise and false positives: Rate-limit oracles are noisier than status-code-based oracles. Shared rate-limit buckets, CDN caching, load-balancer distribution, and variable server load all affect when
429appears. Always test with a control resource (known-existing) and a control non-resource (known-non-existing) to establish a baseline.
Mitigation: Enforce rate limits at the gateway layer before resource resolution, using global or per-IP/per-API-key buckets. If per-resource rate limits are required, check resource existence first and return 404 before creating per-resource buckets. Alternatively, create rate-limit buckets for all requested IDs (including non-existing ones) to collapse the differential.
Rate-Limit Header Fingerprinting
(Applies to: All methods) — Non-destructive when using GET/HEAD.
Mechanism: Many APIs include rate-limit metadata headers (RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset or vendor-specific variants like X-RateLimit-*) on every response, not just 429 responses. If rate-limit headers are attached after resource resolution, responses for existing resources include these headers while 404 responses for non-existing resources omit them entirely.
Isolated Variable: A single, well-formed request — no burst required. Compare response headers between existing and non-existing resource requests.
Oracle Signal: Rate-limit headers present (exists, bucket was resolved) vs rate-limit headers absent (does not exist).
GET — Existing Resource (Rate-Limit Headers Present)
GET /api/users/1001 HTTP/1.1
Host: target.com
Authorization: Bearer valid-token
HTTP/1.1 200 OK
Content-Type: application/json
RateLimit-Limit: 100
RateLimit-Remaining: 99
RateLimit-Reset: 60
{"id": 1001, "name": "Alice"}GET — Non-Existing Resource (No Rate-Limit Headers)
GET /api/users/9999 HTTP/1.1
Host: target.com
Authorization: Bearer valid-token
HTTP/1.1 404 Not Found
Content-Type: application/json
{"error": "Not Found"}Comparison: Response Header Differential
// Existing resource response headers:
RateLimit-Limit: 100
RateLimit-Remaining: 99
RateLimit-Reset: 60
// Non-existing resource response headers:
(no rate-limit headers)💡 Single-request oracle: Unlike 429 forcing which requires a burst, header fingerprinting works on a single request. One request per target ID is enough to confirm or deny existence.
💡
Retry-Aftervalue as a resource-specific oracle: When both existing and non-existing resources return429(global limit), theRetry-Aftervalue may still differ if per-resource buckets have different reset windows.
💡 Vendor-specific header variations: Common variants: Standard IETF (
RateLimit-Limit/Remaining/Reset), GitHub/Twitter style (X-RateLimit-*), Stripe (Stripe-Should-Retry), Cloudflare (CF-Ray). Always compare the full set of response headers.
Mitigation: Attach rate-limit headers at the gateway or middleware layer before resource resolution, so that all responses — including 404 — include identical rate-limit metadata.