Rate Limiting
Velocity checks and abuse protection at the API Gateway layer
Phase 2
Spec
Rate Limiting middleware sits in the API Gateway right after Auth. It blocks abuse before the request reaches the orchestrator. All checks are atomic and stored in Redis. Two protection layers: IP-level (DDoS and bot protection) and user-level (velocity checks for deposits/withdrawals).
Velocity limits on deposit amount (
deposit_total_24h) are configurable per brand via the psp_configs table. Defaults are set at brand initialization. For Phase 2, hard-coded in config.1.
Request Flow
1
Request arrives at API Gateway
2
Auth middleware validates JWT → extracts user_id + brand_id
3
Rate limiter middleware runs checks (IP → user velocity → amount)
4
If any check fails → return HTTP 429 with retry_after
5
All checks pass → request forwarded to Request Router
Middleware runs synchronously, before any business logic. Overhead < 2ms with Redis.
2.
Velocity Rules
| Check | Window | Limit | Scope | Action |
|---|---|---|---|---|
| Deposit attempts | 1 minute | 5 per user | user_id | Block for 5 min |
| Deposit attempts | 1 hour | 20 per user | user_id | Block for 1 hour |
| Withdrawal attempts | 1 hour | 3 per user | user_id | Block for 2 hours |
| Failed payment attempts | 15 minutes | 3 per user | user_id | Require re-auth / captcha |
| Deposit total amount | 24 hours | Configurable per brand | user_id + brand_id | Soft block (KYC gate) |
| Requests per IP | 1 minute | 60 requests | IP address | HTTP 429 + exponential backoff |
3.
HTTP 429 Response
Response Fields
| Field | Type | Description |
|---|---|---|
error | string | Machine-readable error code: "RATE_LIMIT_EXCEEDED" |
message | string | Human-readable description of why the request was blocked |
retry_after | number | Seconds until the client may retry |
block_type | string | "velocity" | "ip" | "amount" — which check triggered |
Example Response
HTTP 429
Too Many Requests
{
"error": "RATE_LIMIT_EXCEEDED",
"message": "Too many deposit attempts.
Please wait before retrying.",
"retry_after": 300,
"block_type": "velocity"
}The Retry-After header should also be set in the HTTP response (RFC 6585). Frontend should read retry_after and show a countdown timer.
4.
Counter Storage
Redis (recommended)
Recommended
Pros
- Sub-millisecond counter increments with INCR + EXPIRE
- Atomic sliding window via sorted sets (ZADD / ZCOUNT)
- TTL-based expiry — counters auto-clean
- Shared state across multiple API replicas
Cons
- External dependency to deploy and monitor
Redis pattern for sliding window:
ZADD rate:deposit:{user_id} {now_ms} {now_ms}
ZREMRANGEBYSCORE rate:deposit:{user_id} 0 {now_ms - window_ms}
count = ZCARD rate:deposit:{user_id}
EXPIRE rate:deposit:{user_id} {window_seconds}In-memory (per-instance)
Pros
- Zero infrastructure overhead
- No network latency
Cons
- Limits not shared across API replicas — user can bypass by hitting different instances
- State lost on restart
PostgreSQL
Pros
- No extra infra — reuse existing DB
- Durable audit log of rate-limit events
Cons
- Slower than Redis for hot-path counters (extra round-trip per request)
- Write-heavy workload on counters at scale
DEPO44 | RATE LIMITING SPEC v1 | PHASE 2