Skip to content

Fix shadow-mode rate throttle to key on real client IP - #210

Merged
brianglass merged 1 commit into
mainfrom
fix-throttle-client-ip
Aug 21, 2026
Merged

Fix shadow-mode rate throttle to key on real client IP#210
brianglass merged 1 commit into
mainfrom
fix-throttle-client-ip

Conversation

@brianglass

Copy link
Copy Markdown
Owner

Summary

  • Investigating the shadow-mode rate-limit logs (ShadowAnonRateThrottle, live since Wire up API rate limiting in shadow mode #205) turned up only 10 "would throttle" events against ~32k /api/ requests in 36 hours -- but several of those events showed the same client IP paired with a different second X-Forwarded-For entry per request.
  • Root cause: NINJA_NUM_PROXIES is unset, so django-ninja's get_ident() falls back to keying the throttle cache on the entire raw XFF string rather than just the client IP. Cloud Run's front end appends its own hop IP(s) after the real client IP, and those hops vary between requests -- fragmenting one client's requests across many different throttle buckets and undercounting how often the real limit is actually hit.
  • ClientIpMixin fixes this by explicitly taking the first XFF entry (Cloud Run's documented client-first format), falling back to REMOTE_ADDR when XFF is absent.

Test plan

  • Added ClientIpMixinTestCase verifying get_ident() extracts the first XFF entry and falls back to REMOTE_ADDR
  • Full suite passes (docker compose run --rm tests)
  • Verified against real production log data pulled via gcloud logging read that the current unpatched behavior does produce inconsistent per-client keys

🤖 Generated with Claude Code

https://claude.ai/code/session_01Hf6j2xXQXywHVh3HAVRxB3

NINJA_NUM_PROXIES is unset, so django-ninja's get_ident() was falling
back to keying the throttle cache on the entire X-Forwarded-For string.
Cloud Run's front end appends its own hop IP(s) after the real client
IP, and those hops aren't guaranteed to stay the same across requests
from the same client -- fragmenting one client's requests across many
throttle buckets and undercounting how often the real limit is hit.
Confirmed via the collected shadow-mode logs: only 10 "would throttle"
events against ~32k requests in 36 hours, several of which showed the
same client IP paired with a different second XFF entry per request.

ClientIpMixin now explicitly takes the first XFF entry (or REMOTE_ADDR
if XFF is absent), matching Cloud Run's documented client-first XFF
format, so the shadow-mode data collected going forward reflects real
per-client behavior before deciding whether to enforce the limit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Hf6j2xXQXywHVh3HAVRxB3
@brianglass
brianglass merged commit 38b68f7 into main Aug 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant