Rate Limiting
The ZB ID API enforces rate limits to protect against brute-force attacks, abuse, and to ensure fair usage across all clients. Each endpoint has specific limits based on the sensitivity of the operation.
Rate limit tiers
Rate limits vary by endpoint and are tuned based on the risk profile of each operation. Authentication endpoints have stricter limits to prevent credential stuffing and brute-force attacks.
Authentication endpoints
- Name
Login- Type
- 5 requests / 10 minutes
- Description
Applies to
POST /auth/login. Prevents password brute-forcing. Tracked per login identifier (the email or phone number used to sign in, normalised).
- Name
Register- Type
- 10 requests / 60 minutes
- Description
Applies to
POST /auth/register. Prevents mass account creation. Tracked per client IP address.
- Name
Token refresh- Type
- 30 requests / 60 minutes
- Description
Applies to
POST /auth/token/refresh. Allows reasonable token rotation. Tracked per client IP address.
- Name
Forgot password- Type
- 5 requests / 60 minutes
- Description
Applies to
POST /auth/password/forgot. Throttles reset-link requests. Tracked per client IP address.
OAuth / Client credentials
- Name
Client credentials- Type
- 60 requests / minute
- Description
Applies to
POST /oauth/token. Tracked perclient_id. The limit is 60 requests per minute.
OAuth scope: the client-credentials endpoint is limited to 60 requests per minute, counted per client_id, so one client's traffic never affects another's.
Rate limit summary table
| Endpoint | Limit | Window | Scope |
|---|---|---|---|
| Login | 5 requests | 10 minutes | Login identifier |
| Register | 10 requests | 60 minutes | IP address |
| Token refresh | 30 requests | 60 minutes | IP address |
| Forgot password | 5 requests | 60 minutes | IP address |
| Client credentials | 60 requests | 1 minute | client_id |
Rate limit scope
Rate limits are scoped to different identifiers depending on the endpoint. This means that one user hitting a limit does not affect other users.
Scope by endpoint
- Name
Login- Type
- Per login identifier
- Description
Tracked by the email or phone number used to sign in (normalised). Each unique identifier has its own counter and sliding window.
- Name
Register, Token refresh, Forgot password- Type
- Per IP address
- Description
Tracked by the caller's IP address (the
X-Forwarded-Forclient IP). Each source IP has its own counter.
- Name
Client credentials (OAuth)- Type
- Per client_id
- Description
Tracked by the
client_idin the token request. Each registered OAuth client has its own counter.
Login scoping: login is throttled per login identifier, so an attacker targeting one account cannot exhaust the limit for other accounts. Register, token refresh, and forgot-password are throttled per source IP address.
Handling 429 responses
When you exceed a rate limit, the API returns a 429 Too Many Requests response. Your application should implement exponential backoff to retry gracefully.
429 Response format
{
"error": "RATE_LIMIT_EXCEEDED",
"message": "Too many login attempts",
"status": 429,
"timestamp": "2026-05-31T10:30:00.123Z"
}
The message varies by endpoint (Too many login attempts, Too many registration attempts, Too many refresh attempts, Too many password reset requests, or Rate limit exceeded for client: <id>), but the error code is always RATE_LIMIT_EXCEEDED.
Exponential backoff strategy
When you receive a 429 response, wait before retrying using increasing delays:
- First retry: Wait 2 seconds
- Second retry: Wait 4 seconds
- Third retry: Wait 8 seconds
- Fourth retry: Wait 16 seconds
- Max retries: Stop after 4-5 attempts and surface the error to the user
Adding a small random jitter (e.g., 0 to 500 ms) to each delay helps prevent multiple clients from retrying at exactly the same time.
Exponential backoff
# Simple retry loop with exponential backoff
DELAY=2
MAX_RETRIES=4
for i in $(seq 1 $MAX_RETRIES); do
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST https://id.zb.co.zw/auth/login \
-H "Content-Type: application/json" \
-d '{"phone": "+263771234567", "password": "SecurePass123"}')
if [ "$RESPONSE" -ne 429 ]; then
echo "Request succeeded with status $RESPONSE"
break
fi
echo "Rate limited. Waiting ${DELAY}s before retry $i..."
sleep $DELAY
DELAY=$((DELAY * 2))
done
Implementation details
Redis-based sliding window
ZB ID uses a Redis-backed sliding window algorithm for rate limiting. This provides accurate, per-key tracking with minimal overhead.
How it works:
- Each rate-limited action (e.g., login attempt for a specific phone number) is recorded as an entry in a Redis sorted set
- Entries older than the configured window (e.g., 10 minutes for login) are automatically pruned
- The count of remaining entries determines whether the request is allowed or rejected
- This approach avoids the "boundary burst" problem that fixed-window counters can have
Graceful degradation
If the Redis instance becomes unavailable, the rate limiter degrades gracefully:
- Requests are allowed through rather than blocked
- The API continues to function normally without rate limiting
- An internal alert is triggered so the operations team can restore Redis
- Once Redis recovers, rate limiting resumes automatically with a fresh window
This design ensures that a Redis outage never causes a complete service disruption for legitimate users.
Best practices
- Cache tokens instead of requesting new ones on every API call
- Use refresh tokens to rotate access tokens rather than re-authenticating
- Implement backoff in all client libraries to handle 429 responses automatically
- Monitor your usage and design around the published limits; contact the ZB ID team if a high-throughput integration needs a higher ceiling
Graceful degradation: If Redis is temporarily unavailable, the ZB ID API allows requests through without rate limiting. This means rate limits are enforced on a best-effort basis and will not block legitimate traffic during infrastructure issues.
Quick reference
| Detail | Value |
|---|---|
| Algorithm | Sliding window (Redis sorted sets) |
| Storage | Redis |
| Degradation | Fail-open (requests allowed if Redis is down) |
| Scope | Per login identifier, per IP, or per client_id |
| Response code | 429 Too Many Requests |