A lightweight reverse proxy gateway that sits in front of any API to protect it from traffic spikes and abuse. Built with FastAPI, Redis, and httpx.
When a client sends a request to localhost:8000/api/v1/users, the proxy:
- Identifies the client — via
X-API-Keyheader or IP address - Checks the rate limit — using the Sliding Window Log algorithm in Redis
- If allowed → forwards the request to the real backend and streams the response back
- If blocked → returns
429 Too Many Requestswith aRetry-Afterheader - Logs analytics — records RPM, latency, status codes, and top consumers
Client ──→ [Proxy :8000] ──→ Rate Limit Check ──→ Forward to Backend ──→ Stream Response
│ │
│ └── 429 Too Many Requests (if over quota)
└── Record analytics (viewable at /dashboard)
graph LR
subgraph Clients
C1[Client A - API Key]
C2[Client B - IP Based]
end
subgraph "Proxy Gateway :8000"
MW[Middleware<br/>Rate Limit + Analytics]
PX[Reverse Proxy<br/>Streaming Forwarder]
DASH[Dashboard<br/>/dashboard]
end
subgraph "Redis :6379"
RL[Sorted Sets<br/>Rate Limit Windows]
AN[Hashes + Lists<br/>Analytics Data]
end
subgraph "Backend :9000"
API[Upstream API]
end
C1 & C2 --> MW
MW -->|allowed| PX
MW -->|blocked → 429| C1 & C2
MW <--> RL & AN
PX <--> API
DASH <--> AN
| Feature | Implementation | File |
|---|---|---|
| Sliding Window Log rate limiting | Redis Sorted Sets + atomic Lua script | src/rate_limiter.py |
| Streaming reverse proxy | httpx AsyncClient with request.stream() |
src/proxy.py |
| Standard rate limit headers | X-RateLimit-Limit, Remaining, Reset, Retry-After |
src/middleware.py |
| Real-time analytics dashboard | Chart.js with auto-refresh | static/dashboard.html |
| Per-client identification | API key or IP-based quotas | src/middleware.py |
| Hop-by-hop header sanitization | RFC 9110 compliant | src/proxy.py |
| Connection pooling | Shared httpx.AsyncClient via lifespan |
src/main.py |
| Configurable via environment | Pydantic Settings + .env |
src/config.py |
- Python 3.11+
- Redis 7+ (via Docker or local install)
- Docker & Docker Compose (optional, for Redis)
git clone https://github.com/pushkarreddyy/api-rate-limiter-proxy.git
cd api-rate-limiter-proxy
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # macOS/Linux
pip install -r requirements.txtdocker compose up -dOr if Redis is already running locally on port 6379, skip this step.
python -m mock_backend.server# In a new terminal
uvicorn src.main:app --port 8000 --reloadNavigate to http://localhost:8000/dashboard in your browser.
curl -i http://localhost:8000/api/v1/usersHTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1694380860
{"users": [...], "count": 5}# Send 101 rapid requests
for /L %i in (1,1,105) do @curl -s -o NUL -w "Request %i: %%{http_code}\n" http://localhost:8000/api/v1/usersHTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1694380920
Retry-After: 47
{"error": "rate_limit_exceeded", "message": "Too many requests. Try again in 47 seconds.", "retry_after": 47}curl -H "X-API-Key: my-secret-key" http://localhost:8000/api/v1/userscurl http://localhost:8000/api/v1/streamcurl -X POST http://localhost:8000/api/v1/users \
-H "Content-Type: application/json" \
-d '{"name": "New User", "email": "new@example.com"}'This project uses the Sliding Window Log algorithm — the most precise rate limiting approach:
Window = 60 seconds, Limit = 100 requests
1. ZREMRANGEBYSCORE → Remove timestamps older than 60s ago (slides the window)
2. ZCARD → Count remaining entries (= requests in last 60s)
3. IF count < 100:
ZADD → Log this request's timestamp
→ Return 200 with X-RateLimit-Remaining: (99 - count)
ELSE:
ZRANGE 0 0 → Find oldest entry to calculate when a slot opens
→ Return 429 with Retry-After: (seconds until oldest entry expires)
All three steps run inside an atomic Lua script on the Redis server — zero race conditions even under high concurrency.
Building a production-grade rate limiting reverse proxy requires handling boundary conditions and mathematical anomalies:
| Edge Case | Failure Mode / Risk | Mitigation in Codebase | Source Reference |
|---|---|---|---|
| Sub-millisecond Collisions | Redis Sorted Sets deduplicate members with identical scores/names, losing request counts under simultaneous bursts. | Appends a unique UUID salt to member keys: "{timestamp}:{uuid4().hex[:8]}". |
src/rate_limiter.py |
| Clock Skew & Negative Retry-After | Client/server timestamp jitter could compute negative or zero Retry-After seconds. |
Enforced lower bound: if retry_after < 0 then retry_after = 1 end in Lua script. |
src/rate_limiter.py |
| Quota Underflow | Decrementing quota past zero could display negative X-RateLimit-Remaining values. |
Strict lower clamp: math.max(0, self.max_requests - current_count). |
src/rate_limiter.py |
| Division-by-Zero in Latency Metrics | Cold startups with 0 requests cause runtime exceptions when calculating average latencies. | Guarded calculation: latency_sum / latency_count if latency_count > 0 else 0.0. |
src/analytics.py |
| Memory Exhaustion (OOM) via Payload Buffering | Multi-megabyte file uploads/downloads buffered in RAM cause reverse proxy crashes. | Fully non-blocking chunked streaming using request.stream() and StreamingResponse(upstream_resp.aiter_raw()). |
src/proxy.py |
| Hop-by-Hop Header Desync | Transferring transfer-encoding or connection headers to upstream triggers protocol corruption. |
Strict RFC 9110 / RFC 7230 header sanitization and hop-by-hop stripping. | src/proxy.py |
All settings are configurable via environment variables or the .env file:
| Variable | Default | Description |
|---|---|---|
REDIS_URL |
redis://localhost:6379/0 |
Redis connection string |
UPSTREAM_BASE_URL |
http://localhost:9000 |
Target backend URL |
RATE_LIMIT_MAX_REQUESTS |
100 |
Max requests per window |
RATE_LIMIT_WINDOW_SECONDS |
60 |
Sliding window size in seconds |
PROXY_TIMEOUT_SECONDS |
30 |
Upstream request timeout |
LOG_LEVEL |
INFO |
Logging level |
├── docker-compose.yml # Redis service
├── requirements.txt # Python dependencies
├── .env.example # Environment variable template
├── src/
│ ├── main.py # FastAPI app entry point + lifespan
│ ├── config.py # Pydantic Settings configuration
│ ├── rate_limiter.py # Sliding Window Log (Redis + Lua script)
│ ├── proxy.py # Reverse proxy with streaming support
│ ├── middleware.py # Rate limit enforcement + analytics recording
│ ├── analytics.py # Metrics recording & query service
│ └── dashboard.py # Dashboard API routes
├── static/
│ └── dashboard.html # Analytics dashboard (Chart.js)
├── mock_backend/
│ └── server.py # Mock upstream API for testing
└── tests/
└── __init__.py
| Layer | Technology | Why |
|---|---|---|
| Framework | FastAPI + Uvicorn | Async-native, streaming support, auto OpenAPI |
| HTTP Client | httpx AsyncClient | Async streaming for request & response bodies |
| Rate Limiter | Redis Sorted Sets + Lua | Atomic sliding window, one round-trip, zero races |
| Analytics | Redis (Hashes, Sorted Sets, Lists) | Lightweight time-series without extra databases |
| Dashboard | HTML + Chart.js (CDN) | Zero build step, single file |
Created and maintained by @pushkarreddyy.
Important
Attribution Requirement: If you use, fork, adapt, or reference this project or its codebase in your own software, research, or articles, explicit credit must be given to @pushkarreddyy along with a link back to this original repository:
https://github.com/pushkarreddyy/api-rate-limiter-proxy
This project is licensed under the MIT License — see the LICENSE file for details. Attribution to the author (@pushkarreddyy) must be retained in all copies or substantial portions of the software.