A high-performance FastAPI middleware powered by Redis that provides endpoint-aware rate limiting, adaptive threshold-based caching, and dynamic TTL scaling to protect backend databases and dramatically improve API latency under heavy load.
Modern APIs face several critical challenges:
- π¨ Traffic spikes and burst attacks can overload databases
- π° Expensive queries get re-executed repeatedly β same result, full DB cost every time
- π’ Every request hitting the DB increases latency β even for identical queries
- π Cold endpoints pollute cache β wasting memory on low-traffic paths
- π No adaptive mechanism to dynamically identify and prioritize "hot" endpoints
QueryShield solves all of these by acting as an intelligent middleware layer between your clients and the database β caching smart, throttling hard, and doing both adaptively.
- Token bucket algorithm implemented via Redis + Lua script (atomic, no race conditions)
- Configurable per-endpoint capacity β heavy analytics endpoints get tighter limits than lightweight reads
- IP-based isolation β each client IP gets its own token bucket
- Burst handling with configurable refill rates
- Protection against DDoS-like single-source floods
- Cache only activates after an endpoint becomes "hot" (crosses a request threshold)
- Prevents cache pollution from cold or rarely-used endpoints
- Automatic promotion of popular query patterns to cached status
- True rolling 1-hour window (not fixed-interval β no cliff effects)
- Implemented using Redis sorted sets for accuracy and efficiency
- Real-time hot endpoint detection without clock skew issues
TTL is not a fixed constant β it is computed from:
- Query execution cost (measured DB time)
- Endpoint popularity (hit frequency from sliding window)
This means expensive + popular endpoints stay cached longer, while cheap or rarely-hit endpoints expire quickly β maximizing cache value per memory byte.
All results from Locust load testing under controlled conditions.
Direct measurement of DB round-trip vs. Redis cache retrieval per endpoint:
| Endpoint | DB Avg (ms) | Redis Avg (ms) | Speedup |
|---|---|---|---|
| Active Users | 372 | 10.4 | 35.7Γ |
| Orders by Date | 410 | 11.2 | 36.6Γ |
| Category Stats | 298 | 9.3 | 32.0Γ |
| Products by Category | 212 | 7.8 | 27.1Γ |
| Above Avg Products | 284 | 8.6 | 33.0Γ |
| User Orders | 350 | 10.1 | 34.6Γ |
β‘ Average speedup: 33Γ β Redis responses are 30β36Γ faster than hitting the database directly.
Simulates an aggressive single-source attacker hammering all endpoints simultaneously.
Test Config: 200 concurrent users Β· 50 spawn rate Β· 2 min Β· Single IP
| Metric | Value |
|---|---|
| Total Requests | 45,050 |
| 429 Rate Limited | 38,979 (86.52%) |
| DB Hits | 175 (0.39%) |
| Cache Hits | 5,896 (13.09%) |
| Cache Hit Ratio | 97.12% (of successful requests) |
| Median Latency | 11ms |
| 95th Percentile | 130ms |
| Throughput | 250.71 req/s |
Per-Endpoint Results:
| Endpoint | Requests | 429 Rate | Notes |
|---|---|---|---|
| Active Users (30d) | 14,782 | 96.20% | Most aggressive β most blocked |
| Orders by Date (7d) | 10,139 | 88.90% | Heavy query, high throttle |
| Category Stats | 5,100 | 89.02% | Analytics endpoint, protected |
| Above Avg Products | 5,036 | 88.86% | Analytical query, blocked |
| User Orders | 4,982 | 88.72% | Joins-heavy, protected |
| Products by Category | 5,011 | 46.10% | Lighter query, less throttled |
Key Observations:
- π‘οΈ 86% of malicious traffic blocked β rate limiter correctly identified the flood
- π§ Database almost completely untouched β only 175 DB hits out of 45,050 requests
- β‘ Cached responses served in ~11ms median β lightning fast even under attack
- π« Zero 500 errors β system remained stable throughout the DDoS simulation
- π― Endpoint-aware throttling β lighter endpoints (Products by Category) were throttled less aggressively than expensive analytics
Simulates 10 distributed IPs β represents legitimate distributed users or a coordinated but dispersed traffic pattern.
Test Config: 200 concurrent users Β· 50 spawn rate Β· 2 min Β· 10 IP Pool
| Metric | Value |
|---|---|
| Total Requests | 30,047 |
| 429 Rate Limited | 4,253 (14.15%) |
| DB Hits | 361 (1.20%) |
| Cache Hits | 25,433 (84.65%) |
| Cache Hit Ratio | 98.60% (of successful requests) |
| Median Latency | 120ms |
| 95th Percentile | 770ms |
| Throughput | 167.25 req/s |
Per-Endpoint Breakdown:
| Endpoint | DB Hits | Cache Hits | 429 Throttled | Notes |
|---|---|---|---|---|
| Products by Category | 84 | 3,204 | 0 | Fully cached, no throttle |
| Orders by Date (7d) | 32 | 6,728 | 0 | Heavy query, well-cached |
| Category Stats | 13 | 3,242 | 0 | Nearly all from cache |
| Active Users (30d) | 48 | 5,857 | 4,253 | Only endpoint triggering 429s |
| User Orders | 88 | 3,195 | 0 | Cache absorbing load |
| Above Avg Products | 96 | 3,207 | 0 | Threshold-based cache working |
Key Observations:
- β 98.8% of requests never touched the database β cache doing its job under real load
- β
Only one endpoint triggered 429s β
active-usershit its per-endpoint rate cap, others stayed under - β Zero errors on 5 out of 6 endpoints β clean operation for legitimate distributed traffic
- β System stable at 167 RPS sustained β no degradation over the full test window
| Metric | Single-IP (DDoS) | Multi-IP (Distributed) |
|---|---|---|
| Total Requests | 45,050 | 30,047 |
| 429 Throttled | 86.52% | 14.15% |
| DB Hits | 0.39% | 1.20% |
| Cache Hit Ratio | 97.12% | 98.60% |
| Median Latency | 11ms | 120ms |
| 95th Percentile | 130ms | 770ms |
The contrast between tests is exactly what you want to see: a single abusive source is crushed at 86% throttle, while 10 legitimate distributed IPs experience only 14% throttling with near-perfect cache performance.
βββββββββββββββββ
β Client β
βββββββββ¬ββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββ
β QueryShield Middleware β
β β
β βββββββββββββββββββββββββββββ β
β β Rate Limiter β β
β β - Token bucket (Lua) β β
β β - Per-endpoint policy β β
β β - IP-based isolation β β
β ββββββββββββββββ¬βββββββββββββ β
β β β
β 429? βΌ Pass β
β βββββββββββββββββββββββββββββ β
β β Threshold Cache Engine β β
β β - Sliding window (1hr) β β
β β - Adaptive activation β β
β β - Dynamic TTL scaling β β
β ββββββββββββββββ¬βββββββββββββ β
β β β
β Cache? βΌ Miss β
β βββββββββββββββββββββββββββββ β
β β Redis Layer β β
β β - Rate limit tokens β β
β β - Cached responses β β
β β - Popularity sorted sets β β
β ββββββββββββββββ¬βββββββββββββ β
βββββββββββββββββββΌββββββββββββββββ
β (cache miss only)
βΌ
βββββββββββββββ
β PostgreSQL β
βββββββββββββββ
Request flow:
- Request arrives β Rate limiter checks IP + endpoint token bucket
- If tokens exhausted β return
429immediately (no DB touch) - If tokens available β check sliding window popularity
- If endpoint is "hot" and cache key exists β return cached response from Redis
- If cache miss or cold endpoint β query PostgreSQL, store result if hot, return response
- Python 3.11+
- Redis 7.x running locally or remotely
- PostgreSQL 15+
- pip / virtualenv
# Clone the repository
git clone https://github.com/yourusername/queryshield.git
cd queryshield
# Create virtual environment
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Install dependencies
pip install -r requirements.txtCreate a .env file in the project root:
# Database
DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/queryshield_db
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0
# Rate Limiting
RATE_LIMIT_WINDOW_SECONDS=60
RATE_LIMIT_DEFAULT_CAPACITY=10
# Caching
CACHE_HOT_THRESHOLD=5 # requests before endpoint is considered "hot"
CACHE_DEFAULT_TTL_SECONDS=60
# Dynamic TTL Scaling
TTL_COST_WEIGHT=0.6 # weight of query cost in TTL calculation
TTL_POPULARITY_WEIGHT=0.4 # weight of endpoint popularity in TTL calculation
TTL_MAX_SECONDS=300
TTL_MIN_SECONDS=10uvicorn app.main:app --host 0.0.0.0 --port 8000 --reloadThe API will be available at http://localhost:8000. Interactive docs at http://localhost:8000/docs.
QueryShield transparently intercepts all requests to configured endpoints:
# Standard request β middleware handles rate limiting + caching automatically
curl http://localhost:8000/endpoints/analytics/active-users?days=30
# Response headers reveal middleware decision
# X-Cache: HIT β served from Redis cache
# X-Cache: MISS β forwarded to PostgreSQL
# X-Cache: COLD β endpoint not yet "hot", no caching
# X-RateLimit-Remaining: 8
# X-RateLimit-Reset: 1708200060When the rate limit is exceeded:
{
"status": 429,
"error": "Rate limit exceeded",
"message": "Too many requests from this IP for this endpoint.",
"retry_after_seconds": 45
}curl http://localhost:8000/metrics{
"total_requests": 30047,
"cache_hits": 25433,
"db_hits": 361,
"throttled_requests": 4253,
"cache_hit_ratio": 0.986,
"hot_endpoints": [
"/endpoints/orders/by-date",
"/endpoints/analytics/active-users",
"/endpoints/analytics/category-stats"
]
}Tests were run with Locust. Test files are in the /load_tests directory.
locust -f load_tests/locust_single_ip.py \
--headless \
-u 200 \
-r 50 \
--run-time 2m \
--host http://localhost:8000locust -f load_tests/locust_multi_ip_10_pool.py \
--headless \
-u 200 \
-r 50 \
--run-time 2m \
--host http://localhost:8000All tests were run on a Dell laptop (local environment). Results are reproducible β test files and DB seed scripts are included in the repo.
- First N requests (below threshold) β always hit PostgreSQL
- No cache entry written
- Prevents cache memory waste on unpopular paths
- Request count crosses threshold within the 1-hour sliding window
- Endpoint is promoted to "hot" status in Redis
- Next DB response is cached with dynamically computed TTL
- Subsequent requests served from Redis until TTL expires
normalized_cost = query_execution_ms / max_observed_ms
normalized_popularity = endpoint_hits / max_endpoint_hits
ttl = TTL_MIN + (TTL_MAX - TTL_MIN) Γ (
cost_weight Γ normalized_cost +
popularity_weight Γ normalized_popularity
)
An endpoint that takes 400ms DB execution time and receives heavy traffic could be cached for up to 5 minutes, while a fast, rarely-hit endpoint might only cache for 10 seconds.
| Layer | Technology |
|---|---|
| API Framework | FastAPI |
| Database | PostgreSQL 15 + async SQLAlchemy |
| Cache / RL | Redis 7 (sorted sets + Lua scripts) |
| Load Testing | Locust |
| Runtime | Python 3.11+ with asyncio |
- Public API protection β rate limit by IP before any DB work happens
- SaaS backend shielding β prevent one tenant from starving others
- Analytics-heavy endpoints β cache expensive aggregation queries adaptively
- High-read workloads β serve the majority of reads from Redis at 10ms
- Cost reduction β dramatically cut DB compute costs on cloud-hosted databases
| Feature | Traditional Approach | QueryShield |
|---|---|---|
| Caching strategy | Cache everything | Cache only hot endpoints |
| TTL | Fixed constant | Dynamic: cost Γ popularity |
| Rate limiting | Global request count | Endpoint-aware token bucket |
| Popularity tracking | None | Sliding 1-hour Redis sorted set |
| Cache pollution | Common problem | Threshold-based activation prevents it |
| Attack vs. legit traffic | Same treatment | Single-IP floods blocked at 86% |
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Your Name