Skip to content

Rate Limiting

Craft Easy API uses Slowapi (built on top of limits) to enforce token-bucket rate limiting on all endpoints.

Configuration

Setting Default Description
RATE_LIMIT_ENABLED true Enable/disable rate limiting globally
RATE_LIMIT_DEFAULT "100/minute" Default limit for all authenticated endpoints
RATE_LIMIT_AUTH "10/minute" Stricter limit for authentication endpoints
RATE_LIMIT_STORAGE_URI "memory://" Storage backend URI — set automatically when REDIS_URI is configured
REDIS_URI None Redis connection URI (e.g. "redis://localhost:6379/0"). When set, automatically overrides RATE_LIMIT_STORAGE_URI to use Redis.
PUBLIC_USER_RATE_LIMIT "60/minute" Rate limit for anonymous/public users (applied when PUBLIC_USER_ENABLED=true)

Rate Limit Key

The limiter identifies callers using a smart key function:

  • Authenticated users: keyed by user:{user_id} — ensures per-user limits regardless of IP
  • Anonymous requests: keyed by the client's IP address

This prevents one user behind a shared IP (e.g., corporate NAT) from exhausting the limit for everyone.

Storage Backends

Backend URI format Use case
In-memory memory:// Development, single-instance deployments
Redis redis://host:6379/0 Production, multi-instance deployments

Redis Backend

For production environments running multiple API instances behind a load balancer, configure Redis so every instance shares the same rate limit counters:

REDIS_URI=redis://localhost:6379/0

When REDIS_URI is set at startup, the settings validator automatically sets RATE_LIMIT_STORAGE_URI to the same value — you do not need to configure both.

Redis support requires the redis extra:

pip install 'craft-easy-api[redis]'

In-memory storage is the default and is appropriate for local development and single-instance deployments. If REDIS_URI is not set, the rate limiter uses in-memory storage automatically.

Fallback to in-memory

If REDIS_URI is configured but the redis package is not installed, the API logs a warning at startup and falls back to in-memory storage:

WARNING: REDIS_URI is set but the 'redis' package is not installed.
Install with: pip install 'craft-easy-api[redis]'.
Falling back to in-memory rate limiting.

The API continues to start and serve requests — rate limiting works but is not shared across instances.

Multi-instance without Redis

Running multiple API instances without Redis means each instance keeps its own counters. A client could exceed the intended limit by spreading requests across instances. Use Redis in any horizontally-scaled deployment.

Public (Anonymous) Rate Limiting

When PUBLIC_USER_ENABLED=true, anonymous requests are tracked in a separate bucket keyed by public:{ip} and subject to PUBLIC_USER_RATE_LIMIT (default: 60/minute). This stricter limit prevents unauthenticated clients from consuming capacity intended for authenticated users.

Response on Limit Exceeded

When a client exceeds the rate limit, the API returns:

HTTP/1.1 429 Too Many Requests
Retry-After: 42
X-Request-ID: abc-123

{
  "error": "RATE_LIMIT_EXCEEDED",
  "message": "Rate limit exceeded",
  "retry_after": 42
}

The Retry-After header indicates when the client can retry (in seconds).

Per-Route Limits

You can apply custom limits to specific routes using Slowapi decorators:

from slowapi import Limiter

@app.get("/expensive-report")
@limiter.limit("5/minute")
async def expensive_report():
    ...

Auth Rate Limiting

Authentication endpoints (login, OTP, verify) have a stricter rate limit configured via RATE_LIMIT_AUTH. This protects against brute-force attacks:

RATE_LIMIT_AUTH = "10/minute"  # Max 10 auth attempts per minute per user/IP

Additional auth-specific protections are covered in Abuse Protection.