diff --git a/PROJECTS/advanced/api-rate-limiter/learn/04-CHALLENGES.md b/PROJECTS/advanced/api-rate-limiter/learn/04-CHALLENGES.md new file mode 100644 index 00000000..fab1e2db --- /dev/null +++ b/PROJECTS/advanced/api-rate-limiter/learn/04-CHALLENGES.md @@ -0,0 +1,899 @@ +# Extension Challenges + +You've built the base project. Now make it yours by extending it with new features. + +These challenges are ordered by difficulty. Start with the easier ones to build confidence, then tackle the harder ones when you want to dive deeper. + +## Easy Challenges + +### Challenge 1: Add Custom Rate Limit Headers + +**What to build:** +Add custom headers to rate limit responses beyond the standard RateLimit-* headers. Include headers like X-RateLimit-Policy (which rule was hit) and X-RateLimit-Scope (user/endpoint/global). + +**Why it's useful:** +Debugging and client-side logic. Clients can see exactly which limit they hit and adjust behavior accordingly. If they hit the per-endpoint limit vs per-user limit, they know whether to slow down across all endpoints or just the current one. + +**What you'll learn:** +- Modifying the RateLimitResult data structure +- Adding information to HTTP responses via middleware +- Debugging rate limiting behavior + +**Hints:** +- Look at `src/fastapi_420/types.py:66-92` where RateLimitResult is defined. Add new fields for policy name and scope. +- Modify the `headers` property at lines 84-92 to include your new headers. +- Test it by making requests and checking response headers: `curl -i localhost:8000/api/test` + +**Test it works:** +```bash +curl -i http://localhost:8000/api/test +# Should see: +# RateLimit-Limit: 100 +# RateLimit-Remaining: 99 +# X-RateLimit-Policy: default +# X-RateLimit-Scope: user +``` + +### Challenge 2: Add Endpoint-Specific Limits via Decorator + +**What to build:** +Create a decorator that applies different limits to different endpoints without using middleware configuration. Allow developers to write `@rate_limit("10/minute", "100/hour")` directly on route functions. + +**Why it's useful:** +More explicit than middleware configuration. Developers see limits directly in code next to the endpoint. Easier to maintain because limits live with the code they protect. + +**What you'll learn:** +- Python decorators with parameters +- Preserving function metadata with functools.wraps +- Integrating with FastAPI's dependency injection system + +**Hints:** +- Study the existing decorator at `src/fastapi_420/limiter.py:143-177` +- The challenge is making it work without passing request explicitly. Use FastAPI's Depends() to inject request. +- Don't forget to call `functools.wraps(func)` to preserve the original function's name and docstring. + +**Test it works:** +```python +@app.get("/custom") +@rate_limit("5/minute") +async def custom_endpoint(): + return {"limited": True} + +# Make 5 requests, should work +# 6th request should return HTTP 420 +``` + +### Challenge 3: Add Redis Key Prefix Per Environment + +**What to build:** +Automatically prefix Redis keys with environment name (dev/staging/prod) so different environments can share a Redis instance without key collisions. + +**Why it's useful:** +Cost savings. Instead of running 3 separate Redis instances, run one with namespaced keys. Development keys don't interfere with production keys. + +**What you'll learn:** +- Redis key design patterns +- Environment-based configuration +- Key namespacing strategies + +**Hints:** +- Modify `RateLimitKey.build()` at `src/fastapi_420/types.py:354-362` to include environment prefix +- Get environment from settings: `self._settings.ENVIRONMENT` +- Result should be: `dev:ratelimit:v1:user:...` instead of `ratelimit:v1:user:...` + +**Test it works:** +```bash +# Set environment +export RATELIMIT_ENVIRONMENT=staging + +# Start app, make requests +# Check Redis keys +redis-cli keys "*" +# Should see: staging:ratelimit:v1:user:... +``` + +## Intermediate Challenges + +### Challenge 4: Implement Request Cost Weighting + +**What to build:** +Allow different requests to consume different amounts of rate limit "budget." A simple GET might cost 1 point, while an expensive report generation costs 10 points. Clients with 100 points/minute can make 100 simple requests or 10 expensive ones. + +**Real world application:** +OpenAI's API uses this pattern. Different models cost different amounts of "tokens." GPT-4 is more expensive than GPT-3.5. Same concept applies to APIs where some endpoints are computationally expensive. + +**What you'll learn:** +- Extending rate limiting beyond simple request counting +- Designing flexible APIs that account for resource consumption +- Balancing simplicity with power + +**Implementation approach:** + +1. **Add cost parameter to check method** + - Files to create: None (modify existing) + - Files to modify: `src/fastapi_420/limiter.py:181`, `src/fastapi_420/algorithms/base.py:26` + +2. **Multiply counter by cost** + - Hook into storage increment operations + - Pass cost to Lua scripts + - Update sliding window algorithm to handle weighted counts + +3. **Test edge cases:** + - What if cost=0? (Free requests) + - What if cost=1000 but limit is 100? (Single request exhausts limit) + - What if cost is negative? (Should reject as invalid) + +**Hints:** +- Start by adding an optional `cost: int = 1` parameter to `limiter.check()` +- In the Lua script, change `redis.call('INCR', current_key)` to `redis.call('INCRBY', current_key, cost)` +- For testing, create endpoints with different costs: `/cheap` (cost=1), `/expensive` (cost=10) + +**Extra credit:** +Make cost configurable per endpoint via decorator: `@limiter.limit("100/minute", cost=5)` + +### Challenge 5: Add CAPTCHA Challenge for Suspicious Clients + +**What to build:** +When a client hits rate limits repeatedly, instead of blocking them completely, return a CAPTCHA challenge. If they solve it, allow the request through. Track CAPTCHA success rate per client. + +**Real world application:** +Cloudflare's "I'm Under Attack" mode works this way. Suspicious traffic gets CAPTCHA challenges instead of hard blocks. Reduces false positives (blocking legitimate users) while still stopping bots. + +**What you'll learn:** +- Integrating third-party services (hCaptcha, reCAPTCHA) +- Progressive enforcement strategies +- Balancing security with user experience + +**Implementation approach:** + +1. **Track violation count per client** + - Add counter: "violations:{identifier}" in Redis + - Increment on rate limit exceeded + - Reset on successful CAPTCHA + +2. **Return CAPTCHA challenge instead of 420** + - When violations > threshold (e.g., 3), return HTTP 429 with CAPTCHA challenge + - Response body includes CAPTCHA site key + - Client solves CAPTCHA, submits solution token + +3. **Validate CAPTCHA solution** + - New endpoint: POST /verify-captcha + - Validate token with CAPTCHA API + - If valid, grant temporary bypass (store token in Redis with expiration) + +4. **Bypass rate limit with valid CAPTCHA** + - Check for CAPTCHA bypass token in fingerprint extraction + - If present and valid, skip rate limit check + +**Hints:** +- Use hCaptcha (simpler API than reCAPTCHA) +- Store bypass tokens with 5-minute expiration +- Track CAPTCHA solve rate: `captcha_solved / captcha_presented` + +**Testing:** +Can't easily test CAPTCHA in automated tests. Instead: +1. Create mock CAPTCHA verifier that always returns true +2. Inject mock in test environment +3. In production, use real hCaptcha API + +### Challenge 6: Implement Geolocation-Based Rate Limits + +**What to build:** +Apply different rate limits based on client geographic location. Stricter limits for regions with high bot activity (e.g., limit clients from certain countries to 10/minute vs 100/minute for domestic traffic). + +**Real world application:** +E-commerce sites often see bot traffic primarily from certain regions. Applying regional limits reduces fraud without impacting legitimate international customers. + +**What you'll learn:** +- IP geolocation databases (MaxMind GeoIP2) +- Policy-based rate limiting +- Geographic discrimination considerations (be careful with this) + +**Implementation approach:** + +1. **Add geolocation lookup** + - Library: `geoip2` with MaxMind database + - On fingerprint extraction, lookup IP address + - Add country code to FingerprintData + +2. **Define geo-based policies** + - Config: `GEO_LIMITS = {"US": "100/minute", "CN": "10/minute", "default": "50/minute"}` + - Load from environment or config file + +3. **Apply policy at rate limit check** + - In LayeredDefense, check client's country code + - Select appropriate limit from GEO_LIMITS + - Fall back to default if country not configured + +4. **Monitor and adjust** + - Log rate limit violations by country + - Dashboard showing requests/violations per region + +**Hints:** +- MaxMind GeoLite2 database is free but requires registration +- Cache geo lookups (IP to country mapping doesn't change frequently) +- Consider privacy implications. Logging IP addresses with country codes might be PII in some jurisdictions. + +**Ethical considerations:** +Geo-based limiting can be discriminatory. Only use it for legitimate security purposes. Document why certain regions have stricter limits. Consider offering CAPTCHA challenge instead of hard blocking. + +## Advanced Challenges + +### Challenge 7: Build a Rate Limit Dashboard + +**What to build:** +Web dashboard showing real-time rate limiting stats. Metrics include requests/sec, top violators, circuit breaker status, algorithm performance. Built with FastAPI + htmx or React. + +**Why this is hard:** +Requires collecting metrics without impacting rate limiting performance. Need efficient aggregation of high-frequency events. Must handle dashboard queries without slowing down rate limit checks. + +**What you'll learn:** +- High-performance metrics collection +- Time-series data aggregation +- Building admin dashboards +- WebSocket or Server-Sent Events for real-time updates + +**Architecture changes needed:** +``` +┌─────────────────┐ +│ Rate Limiter │ +│ (existing) │ +└────────┬────────┘ + │ + ├─ Check limit (fast path) + │ + └─ Emit metrics (async, non-blocking) + ↓ + ┌────────────────┐ + │ Metrics Buffer │ + │ (in-memory) │ + └────────┬───────┘ + │ + Batch write every 5 seconds + ↓ + ┌────────────────┐ + │ Redis TimeSeries│ + │ or InfluxDB │ + └────────┬───────┘ + │ + ↓ + ┌────────────────┐ + │ Dashboard │ + │ (FastAPI) │ + └────────────────┘ +``` + +**Implementation steps:** + +**Phase 1: Metrics Collection** (3-5 hours) +- Add metrics emitter to rate limiter +- Batch metrics in memory buffer +- Flush to Redis every 5 seconds +- Metrics: requests_total, violations_total, latency_histogram + +**Phase 2: Aggregation** (3-5 hours) +- Create background task to aggregate 5-second metrics into 1-minute/1-hour/1-day buckets +- Store in Redis sorted sets: `ZADD metrics:requests:minute:{timestamp} {count} {endpoint}` +- Retention: Keep 5-second data for 10 minutes, 1-minute data for 24 hours, 1-hour data for 30 days + +**Phase 3: Dashboard Backend** (4-6 hours) +- FastAPI endpoints: + - `GET /api/metrics/summary` - Current state (last minute stats) + - `GET /api/metrics/timeseries?metric=requests&window=1h` - Historical data + - `GET /api/metrics/top-violators?limit=10` - Clients with most violations +- WebSocket endpoint: `/ws/metrics` - Real-time updates + +**Phase 4: Dashboard Frontend** (6-8 hours) +- Chart.js or Recharts for time-series graphs +- Auto-refresh every 5 seconds +- Cards showing: Total requests, Rate limit violations, Circuit breaker status, Top endpoints +- Table of recent violations with client fingerprint (truncated), endpoint, timestamp + +**Gotchas:** +- **Don't block rate limit checks**: Metrics collection must be async. Use `asyncio.create_task()` to fire-and-forget. +- **Buffer overflow**: If metrics buffer grows too large (million events), drop oldest or sample. +- **Redis memory**: Time-series data grows fast. Set expiration policies. + +**Success criteria:** +Your implementation should: +- [ ] Collect metrics without adding >5ms latency to rate limit checks +- [ ] Display requests/sec updated every 5 seconds +- [ ] Show top 10 rate limit violators with endpoint breakdown +- [ ] Visualize circuit breaker state transitions +- [ ] Handle 10k requests/sec without overloading dashboard backend + +### Challenge 8: Implement Distributed Circuit Breaker + +**What to build:** +Circuit breaker that shares state across multiple API servers using Redis. When one server trips the circuit, all servers immediately enter the same state. Current implementation is per-process only. + +**Estimated time:** +2-3 days for full implementation with testing + +**Prerequisites:** +You should have completed Challenge 4 (request cost weighting) and Challenge 7 (dashboard) first because understanding metrics is crucial for distributed coordination. + +**What you'll learn:** +- Distributed systems coordination +- Redis pub/sub for event propagation +- Eventually consistent state management +- Handling network partitions and race conditions + +**Planning this feature:** + +Before you code, think through: +- **Consensus**: How do servers agree on circuit state? (Redis as single source of truth) +- **Propagation delay**: Server A trips circuit, how long until Server B knows? (Pub/sub gives ~100ms) +- **Failure modes**: What if Redis connection fails? (Fall back to local circuit breaker) +- **Race conditions**: Multiple servers trying to trip circuit simultaneously (Use Redis SET NX) + +**High level architecture:** +``` +┌──────────────┐ ┌──────────────┐ ┌──────────────┐ +│ Server 1 │ │ Server 2 │ │ Server 3 │ +│ Circuit │ │ Circuit │ │ Circuit │ +│ Breaker │ │ Breaker │ │ Breaker │ +└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ + │ │ │ + └──────────────────────┼──────────────────────┘ + │ + ┌──────────▼───────────┐ + │ Redis │ + │ Circuit State: │ + │ - is_open │ + │ - trip_time │ + │ - request_count │ + │ │ + │ Pub/Sub Channel: │ + │ circuit:events │ + └──────────────────────┘ +``` + +**Implementation phases:** + +**Phase 1: Shared State** (6-8 hours) +- Store circuit state in Redis hash: `circuit:state` +- Fields: is_open (0/1), last_trip_time, failure_count, request_count +- Each server reads state before checking thresholds +- Use Redis TTL to auto-reset after recovery_time + +**Phase 2: Atomic Tripping** (4-6 hours) +- Use Lua script for atomic trip operation: +```lua + local current_count = redis.call('GET', 'circuit:requests') + if current_count >= threshold then + redis.call('HSET', 'circuit:state', 'is_open', '1') + redis.call('HSET', 'circuit:state', 'trip_time', now) + redis.call('PUBLISH', 'circuit:events', 'TRIPPED') + return 1 + end + return 0 +``` +- Only one server successfully trips, others see is_open=1 immediately + +**Phase 3: Event Propagation** (5-7 hours) +- Subscribe to Redis pub/sub channel: `circuit:events` +- Events: TRIPPED, HALF_OPEN, CLOSED +- Background task listens for events, updates local cache +- Local cache reduces Redis reads (check local first, Redis if stale) + +**Phase 4: Graceful Degradation** (4-6 hours) +- If Redis connection fails, fall back to local circuit breaker +- Log warning: "Distributed circuit breaker unavailable, using local" +- When Redis reconnects, sync local state with distributed state +- Handle case where local circuit is open but distributed is closed (trust distributed) + +**Known challenges:** + +1. **Clock Skew** + - Problem: Servers have slightly different clocks. Server A thinks circuit should close at 12:00:30, Server B thinks 12:00:31. + - Hint: Use Redis TIME command to get server time, not local time. Or use TTL-based expiration instead of timestamp comparison. + +2. **Network Partition** + - Problem: Server C can't reach Redis, falls back to local circuit. Local circuit trips at lower threshold, blocks legitimate traffic. + - Hint: Add "confidence" metric. If Redis connection is flaky, increase thresholds or disable circuit entirely (fail-open). + +3. **Thundering Herd** + - Problem: Circuit opens, 100 servers all try to enter half-open state simultaneously after recovery_time. + - Hint: Randomize recovery time: `recovery_time ± random(0, 5)` so servers stagger their retry attempts. + +**Success criteria:** +Your implementation should: +- [ ] Trip circuit across all servers within 500ms of threshold exceeded +- [ ] Sync state even if a server restarts (reads from Redis on startup) +- [ ] Gracefully handle Redis connection failure (fall back to local) +- [ ] Support manual circuit state changes via Redis CLI: `HSET circuit:state is_open 1` +- [ ] Pass load test: 10 servers, 100k requests/sec, circuit trips correctly across all + +### Challenge 9: Build Custom Algorithm - Generic Cell Rate Algorithm (GCRA) + +**What to build:** +Implement the GCRA algorithm used by ATM networks and some high-performance rate limiters. More precise than token bucket, allows bursting within a defined "cell delay variation tolerance." + +**Estimated time:** +3-4 days including research, implementation, and thorough testing + +**Prerequisites:** +Strong understanding of token bucket and sliding window algorithms. Read the GCRA spec before starting. + +**What you'll learn:** +- Telecoms network algorithms +- Sub-second precision rate limiting +- Continuous vs discrete time modeling +- Algorithm correctness proofs + +**How GCRA works:** + +GCRA tracks "Theoretical Arrival Time" (TAT). Each request should arrive no earlier than TAT. If request arrives at time T: +``` +if T >= TAT: + TAT = max(TAT, T) + 1/rate + ALLOW +else: + DENY +``` + +With burst tolerance, allow requests that arrive up to `limit` time units early: +``` +if T >= TAT - limit: + TAT = max(TAT, T) + 1/rate + ALLOW +else: + DENY +``` + +**Implementation:** + +1. **Create algorithm file**: `src/fastapi_420/algorithms/gcra.py` +2. **Define state**: Store TAT in Redis/memory +3. **Implement check method**: +```python + async def check(self, storage, key, rule, timestamp=None): + now = timestamp or time.time() + rate = rule.requests / rule.window_seconds # requests per second + limit = rule.requests # burst tolerance + + tat = await storage.get_gcra_state(key) + if tat is None: + tat = now + + if now >= tat - limit / rate: + # Allow + new_tat = max(tat, now) + 1/rate + await storage.set_gcra_state(key, new_tat, ttl=rule.window_seconds) + remaining = calculate_remaining(new_tat, now, rate, limit) + return RateLimitResult(allowed=True, remaining=remaining, ...) + else: + # Deny + return RateLimitResult(allowed=False, retry_after=tat - now, ...) +``` + +4. **Add storage methods**: `get_gcra_state()` and `set_gcra_state()` to storage backends +5. **Write comprehensive tests**: Edge cases like clock adjustments, very high rates, fractional rates + +**Testing strategy:** +```python +# Test burst tolerance +rule = RateLimitRule(requests=10, window_seconds=10) # 1 req/sec, burst of 10 +algo = GCRAAlgorithm() + +# Should allow 10 immediate requests +for i in range(10): + assert algo.check(storage, "test", rule, timestamp=0).allowed + +# 11th should deny +assert not algo.check(storage, "test", rule, timestamp=0).allowed + +# After 1 second, should allow 1 more +assert algo.check(storage, "test", rule, timestamp=1.0).allowed +``` + +**Compare with token bucket:** +Both algorithms allow bursting, but GCRA is more precise. Token bucket updates in discrete increments (add X tokens every Y seconds). GCRA uses continuous time (TAT can be any float value). + +## Mix and Match + +Combine features for bigger projects: + +**Project Idea 1: Full Rate Limiting Dashboard with Geo Limits** +- Combine Challenge 7 (Dashboard) + Challenge 6 (Geolocation) +- Add map visualization showing requests by country +- Admin panel to adjust geo limits in real-time +- Result: Production-ready rate limiting with geographic policies + +**Project Idea 2: Cost-Weighted Limits with CAPTCHA Bypass** +- Combine Challenge 4 (Cost Weighting) + Challenge 5 (CAPTCHA) +- Expensive operations cost more points +- When points exhausted, offer CAPTCHA to get temporary boost +- Result: Flexible system that allows bursts with verification + +## Real World Integration Challenges + +### Integrate with Prometheus for Monitoring + +**The goal:** +Export rate limiting metrics to Prometheus for visualization in Grafana. Track requests, violations, algorithm latency, storage health. + +**What you'll need:** +- prometheus-client library +- Grafana dashboard JSON +- Understanding of metric types (counter, gauge, histogram) + +**Implementation plan:** +1. Add prometheus_client to dependencies +2. Create metrics in `src/fastapi_420/metrics.py`: +```python + from prometheus_client import Counter, Histogram + + requests_total = Counter( + 'ratelimit_requests_total', + 'Total requests checked', + ['algorithm', 'allowed'] + ) + + check_latency = Histogram( + 'ratelimit_check_duration_seconds', + 'Time to check rate limit' + ) +``` +3. Instrument code: `requests_total.labels(algorithm='sliding_window', allowed='true').inc()` +4. Expose metrics endpoint: `@app.get("/metrics")` returns `prometheus_client.generate_latest()` +5. Configure Prometheus scrape config, point at `/metrics` + +**Watch out for:** +- High cardinality labels (don't include user IDs in labels, use <10 unique values per label) +- Metric name conventions (use underscores, suffix with unit) +- Performance impact (prometheus_client is fast but not free, adds ~50μs per metric update) + +### Deploy to Kubernetes + +**The goal:** +Run the rate-limited API in Kubernetes with Redis, monitoring, and auto-scaling. + +**What you'll learn:** +- Kubernetes deployments and services +- ConfigMaps and Secrets for configuration +- Horizontal Pod Autoscaler +- Redis deployment with persistence + +**Steps:** + +1. **Create Dockerfile** for API: +```dockerfile + FROM python:3.12-slim + WORKDIR /app + COPY requirements.txt . + RUN pip install -r requirements.txt + COPY src/ ./src/ + CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0"] +``` + +2. **Kubernetes manifests**: +```yaml + # redis-deployment.yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: redis + spec: + replicas: 1 + template: + spec: + containers: + - name: redis + image: redis:7-alpine + ports: + - containerPort: 6379 + + # api-deployment.yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: api + spec: + replicas: 3 + template: + spec: + containers: + - name: api + image: your-api:latest + env: + - name: RATELIMIT_REDIS_URL + value: "redis://redis:6379" + ports: + - containerPort: 8000 + + # api-service.yaml + apiVersion: v1 + kind: Service + metadata: + name: api + spec: + selector: + app: api + ports: + - port: 80 + targetPort: 8000 + type: LoadBalancer +``` + +3. **ConfigMap** for settings: +```yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: ratelimit-config + data: + RATELIMIT_ALGORITHM: "sliding_window" + RATELIMIT_DEFAULT_LIMIT: "100/minute" + RATELIMIT_CIRCUIT_THRESHOLD: "10000" +``` + +4. **Horizontal Pod Autoscaler**: +```yaml + apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler + metadata: + name: api-hpa + spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: api + minReplicas: 3 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 +``` + +**Production checklist:** +- [ ] Redis has persistent volume for data +- [ ] Secrets used for sensitive config (not ConfigMap) +- [ ] Resource limits set (CPU/memory) +- [ ] Liveness and readiness probes configured +- [ ] Logging goes to stdout (captured by Kubernetes) +- [ ] Metrics endpoint exposed for Prometheus scraping + +## Performance Challenges + +### Challenge: Handle 100k Requests/Second + +**The goal:** +Optimize the rate limiter to handle 100k requests/sec on a single machine without falling over. + +**Current bottleneck:** +At 10k req/sec, Redis network latency becomes limiting factor. Each request = 1 Redis call = 1ms round trip = max 1000 req/sec per connection. + +**Optimization approaches:** + +**Approach 1: Connection Pooling** +- How: Increase `REDIS_MAX_CONNECTIONS` from 100 to 1000 +- Gain: 100x parallelism, can handle 100k req/sec +- Tradeoff: Redis connection overhead, more memory + +**Approach 2: Local Caching with Eventual Consistency** +- How: Cache rate limit state in-memory for 100ms. Only sync with Redis every 100ms. +- Gain: Reduce Redis calls by 90%, max 10x improvement +- Tradeoff: Accuracy drops to ~95%. Two servers might allow 110/100 requests in a window. + +**Approach 3: Batching** +- How: Accumulate requests for 10ms, send to Redis in single pipeline +- Gain: Reduce network overhead, ~5x improvement +- Tradeoff: 10ms added latency, complexity + +**Benchmark it:** +```bash +# Load test with wrk +wrk -t12 -c400 -d30s http://localhost:8000/api/test + +# Should see: +# Requests/sec: 100000 +# Latency avg: 4ms +# Latency 99th: 15ms +``` + +Target metrics: +- **Throughput**: >100k requests/sec +- **Latency p50**: <5ms +- **Latency p99**: <20ms + +### Challenge: Reduce Memory Usage by 90% + +**The goal:** +Reduce memory consumption for 1 million active rate limit keys from ~100MB to ~10MB. + +**Current usage:** +Each key stores: +- Python dict entry: 56 bytes +- WindowEntry: 24 bytes +- String key (~50 chars): 50 bytes +Total: ~130 bytes/key * 1M keys = 130MB + +**Optimization areas:** + +**Area 1: Use Redis instead of Memory** +- What's inefficient: Python objects have overhead +- How to fix: Store in Redis (more memory efficient) +- Savings: 70% reduction + +**Area 2: Compress Keys** +- What's inefficient: Keys like "ratelimit:v1:user:POST:/api/endpoint:192.168.1.1:Mozilla...:60" are 100+ chars +- How to fix: Hash keys to 16-char hex strings +- Savings: 80% reduction in key size + +**Area 3: Use Slots** +- What's inefficient: Python's `__dict__` attribute on dataclasses +- How to fix: Add `__slots__` to WindowEntry, RateLimitResult +- Savings: 30% reduction in object size + +## Security Challenges + +### Challenge: Add JWT-Based Rate Limit Tiers + +**What to implement:** +Different rate limits based on JWT claims. Free tier: 10/minute. Paid tier: 100/minute. Enterprise: 1000/minute. + +**Threat model:** +This protects against: +- Free tier abuse (creating many accounts to get more quota) +- Fair resource allocation (paying customers get what they paid for) + +**Implementation:** + +1. **Extract tier from JWT**: +```python + # In auth extractor + payload = jwt.decode(token, secret, algorithms=['HS256']) + tier = payload.get('tier', 'free') # free, paid, enterprise +``` + +2. **Add tier to fingerprint**: +```python + # In FingerprintData + tier: str | None = None +``` + +3. **Select limit based on tier**: +```python + # In RateLimiter.check + tier_limits = { + 'free': ['10/minute'], + 'paid': ['100/minute'], + 'enterprise': ['1000/minute'], + } + rules = [RateLimitRule.parse(l) for l in tier_limits.get(tier, tier_limits['free'])] +``` + +**Testing the security:** +- Try to forge JWT with tier=enterprise +- Should fail signature validation +- Try to create free account, abuse endpoint +- Should hit 10/minute limit +- Verify paid users can exceed free tier limits + +### Challenge: Pass OWASP API Security Top 10 + +**The goal:** +Audit the rate limiter against OWASP API Security Top 10 and fix any gaps. + +**Current gaps:** +- **API4:2023 - Unrestricted Resource Consumption**: Partially covered by rate limiting. Gap: No per-IP limits on unauthenticated endpoints. +- **API5:2023 - Broken Function Level Authorization**: Gap: Circuit breaker bypass doesn't check if "authenticated" user actually has permission. +- **API8:2023 - Security Misconfiguration**: Gap: No validation that production has Redis configured. + +**Remediation:** + +1. **Add per-IP limits for unauthenticated**: +```python + if not fingerprint.auth_identifier: + # Stricter limits for anonymous + rules = [RateLimitRule.parse('10/minute')] +``` + +2. **Add permission check to circuit bypass**: +```python + def _should_bypass_circuit(self, context): + if not context.is_authenticated: + return False + # Check actual permissions, not just authentication + return context.has_permission('circuit.bypass') +``` + +3. **Add production validation**: +```python + @model_validator(mode='after') + def validate_production(self): + if self.ENVIRONMENT == 'production': + if not self.storage.REDIS_URL: + raise ValueError("Production requires Redis") + return self +``` + +## Contribution Ideas + +Finished a challenge? Share it back: + +1. **Fork the repo** +2. **Implement your extension** in a feature branch: `git checkout -b feature/captcha-challenge` +3. **Document it** - Add to learn/ folder explaining how it works +4. **Submit a PR** with: + - Your implementation + - Tests proving it works + - Documentation in learn/ folder + - Example usage in examples/ + +Good extensions might get merged into the main project. Even if not merged, you'll have a portfolio piece showing real-world security engineering. + +## Challenge Yourself Further + +### Build Something New + +Use the concepts you learned here to build: +- **API Gateway with rate limiting** - Reverse proxy that adds rate limiting to any backend API +- **Database query rate limiter** - Limit queries/second to prevent database overload +- **Distributed task queue throttler** - Rate limit background job execution + +### Study Real Implementations + +Compare your implementation to production tools: +- **Kong API Gateway** - Supports multiple rate limiting strategies, read their Lua plugin code +- **Cloudflare Workers** - Edge-based rate limiting, study their architecture blog posts +- **GitHub API** - Read their rate limiting docs, understand their tier system + +Read their code, understand their tradeoffs, steal their good ideas. + +### Write About It + +Document your extensions: +- Blog post: "Adding Geolocation-Based Rate Limiting to FastAPI" +- Tutorial: "How I Built a Rate Limiting Dashboard in a Weekend" +- Comparison: "Sliding Window vs GCRA: Performance Benchmarks" + +Teaching others is the best way to verify you understand it. + +## Getting Help + +Stuck on a challenge? + +1. **Debug systematically** + - What did you expect? (Specific behavior) + - What actually happened? (Logs, errors, wrong output) + - What's the smallest test case that reproduces it? + +2. **Read the existing code** + - Similar features already implemented? (Look at algorithms/ for patterns) + - Tests showing how components work? (tests/test_*.py has examples) + +3. **Search for similar problems** + - GitHub issues on similar projects + - Stack Overflow for specific errors + - Redis/FastAPI docs for API questions + +4. **Ask for help** + - Open a GitHub discussion with: + - Challenge you're working on + - What you've tried + - Specific error or unexpected behavior + - Your hypothesis about what's wrong + - Don't just paste errors. Explain your understanding. + +## Challenge Completion + +Track your progress: + +- [ ] Easy Challenge 1: Custom headers +- [ ] Easy Challenge 2: Decorator syntax +- [ ] Easy Challenge 3: Environment prefixes +- [ ] Intermediate Challenge 4: Request cost weighting +- [ ] Intermediate Challenge 5: CAPTCHA integration +- [ ] Intermediate Challenge 6: Geolocation limits +- [ ] Advanced Challenge 7: Rate limit dashboard +- [ ] Advanced Challenge 8: Distributed circuit breaker +- [ ] Advanced Challenge 9: GCRA algorithm + +Completed all of them? You've mastered rate limiting. Time to build something new or contribute back to the community.