Summary
Found during a retrospective code review of merged PR #379 (fix(ops): db-aware health check + rate limit on paid API fan-out views). The new rate limiter is correctly wired (keys on request.user.pk, all guarded views are @login_required so it's not bypassable via spoofable headers) but its actual cap is weaker than documented under the production deployment topology, undercutting the PR's stated goal of bounding paid third-party API cost/DoS exposure.
Root cause
File: core/decorators.py:18-20 (_increment, uses django.core.cache.cache)
No CACHES setting exists anywhere in investor_app/settings.py (confirmed via repo-wide grep), so Django defaults to LocMemCache, which is local to each process. render.yaml runs gunicorn with --workers 2, so each worker process tracks its own independent in-memory counter for the same user. A limit stated as "5 rescreens per 5 minutes" is therefore enforceable only up to ~10 per 5 minutes in the worst case (requests happen to land on both workers), directly undercutting the intent.
This isn't caught by the existing tests (core/tests/test_decorators.py) because they all run single-process via RequestFactory and can't surface a multi-worker cache-partitioning issue.
Fix
Back the cache with a shared store (e.g. Redis via django-redis) in production settings so the counter is consistent across gunicorn workers, or explicitly document the reduced guarantee if a shared cache isn't feasible right now.
Related, lower-priority follow-up (not a bug in this PR, worth tracking separately if pipeline sizes grow)
The rate limit bounds trigger frequency, not total downstream fan-out per trigger. pipeline_screener's rescreen branch and pipeline_screening_settings's rescreen loop each still call screen_property() once per property in the (unfiltered) pipeline, which hits Rentometer/HUD/ATTOM per property when rent-based criteria apply. A user with a large pipeline can still trigger limit × pipeline_size paid API calls within the rate-limit window (e.g. 5 triggers × 500 properties = 2,500 calls in 5 minutes).
Full review: .claude/reviews/pr-379-review.md
Summary
Found during a retrospective code review of merged PR #379 (fix(ops): db-aware health check + rate limit on paid API fan-out views). The new rate limiter is correctly wired (keys on
request.user.pk, all guarded views are@login_requiredso it's not bypassable via spoofable headers) but its actual cap is weaker than documented under the production deployment topology, undercutting the PR's stated goal of bounding paid third-party API cost/DoS exposure.Root cause
File:
core/decorators.py:18-20(_increment, usesdjango.core.cache.cache)No
CACHESsetting exists anywhere ininvestor_app/settings.py(confirmed via repo-wide grep), so Django defaults toLocMemCache, which is local to each process.render.yamlruns gunicorn with--workers 2, so each worker process tracks its own independent in-memory counter for the same user. A limit stated as "5 rescreens per 5 minutes" is therefore enforceable only up to ~10 per 5 minutes in the worst case (requests happen to land on both workers), directly undercutting the intent.This isn't caught by the existing tests (
core/tests/test_decorators.py) because they all run single-process viaRequestFactoryand can't surface a multi-worker cache-partitioning issue.Fix
Back the cache with a shared store (e.g. Redis via
django-redis) in production settings so the counter is consistent across gunicorn workers, or explicitly document the reduced guarantee if a shared cache isn't feasible right now.Related, lower-priority follow-up (not a bug in this PR, worth tracking separately if pipeline sizes grow)
The rate limit bounds trigger frequency, not total downstream fan-out per trigger.
pipeline_screener's rescreen branch andpipeline_screening_settings's rescreen loop each still callscreen_property()once per property in the (unfiltered) pipeline, which hits Rentometer/HUD/ATTOM per property when rent-based criteria apply. A user with a large pipeline can still triggerlimit × pipeline_sizepaid API calls within the rate-limit window (e.g. 5 triggers × 500 properties = 2,500 calls in 5 minutes).Full review:
.claude/reviews/pr-379-review.md