title: Rate Limiting description: FraiseQL implements request rate limiting to prevent denial-of-service (DoS) attacks and resource exhaustion. keywords: [] tags: ["documentation", "reference"]
Rate Limiting¶
FraiseQL implements request rate limiting to prevent denial-of-service (DoS) attacks and resource exhaustion.
Prerequisites¶
Required Knowledge:
- HTTP request fundamentals (status codes, headers)
- Rate limiting strategies (fixed window, sliding window, token bucket)
- Python and FastAPI basics
- Authentication concepts (IP-based vs user-based rate limiting)
- DoS attack patterns and mitigation strategies
Required Software:
- FraiseQL v1 (PostgreSQL)
- Python 3.13+
- curl or Postman (for testing rate limit headers)
Required Infrastructure:
- A FraiseQL FastAPI application (built with
create_fraiseql_app) - PostgreSQL (FraiseQL's database)
- Redis (optional, for shared rate limit state across multiple app instances)
Optional but Recommended:
- Monitoring tools (Prometheus, Grafana) to track rate limit violations
- API gateway (Kong, Tyk) for additional rate limiting at proxy level
- Redis for distributed rate limiting in multi-instance deployments
- Logging aggregation (ELK, Splunk) for rate limit event analysis
Time Estimate: 15-30 minutes for basic configuration, 1-2 hours for production tuning
Overview¶
Rate limiting in FraiseQL is implemented as FastAPI middleware
(fraiseql.security.RateLimitMiddleware). It supports:
- Per-IP rate limiting: limits requests from individual client IPs
- Per-user rate limiting: limits requests from authenticated users (keyed on
request.state.user_idwhen set by the auth layer, falling back to IP) - Multiple strategies: fixed window, sliding window, and token bucket
- GraphQL-aware limits: separate limits per operation type (query / mutation / subscription) and per estimated query complexity
- Response headers: clients can check their quota via HTTP headers
- Pluggable stores: in-memory by default, or Redis for shared state
Configuration¶
The simplest way to enable rate limiting is through FraiseQLConfig. Settings can be
provided in code or via FRAISEQL_-prefixed environment variables, then passed to
create_fraiseql_app(config=...).
from fraiseql.fastapi import FraiseQLConfig, create_fraiseql_app
config = FraiseQLConfig(
database_url="postgresql://localhost/mydb",
# Rate limiting (defaults shown)
rate_limit_enabled=True,
rate_limit_requests_per_minute=60,
rate_limit_requests_per_hour=1000,
rate_limit_burst_size=10,
rate_limit_window_type="sliding", # "sliding" or "fixed"
rate_limit_whitelist=[], # IPs/keys never rate limited
rate_limit_blacklist=[], # IPs/keys always rejected
)
app = create_fraiseql_app(config=config, types=[...], queries=[...])
The same settings as environment variables:
FRAISEQL_RATE_LIMIT_ENABLED=true
FRAISEQL_RATE_LIMIT_REQUESTS_PER_MINUTE=60
FRAISEQL_RATE_LIMIT_REQUESTS_PER_HOUR=1000
FRAISEQL_RATE_LIMIT_BURST_SIZE=10
FRAISEQL_RATE_LIMIT_WINDOW_TYPE=sliding
Configuration Fields¶
| Field | Type | Default | Description |
|---|---|---|---|
rate_limit_enabled |
bool |
True |
Enable/disable rate limiting |
rate_limit_requests_per_minute |
int |
60 |
Allowed requests per minute |
rate_limit_requests_per_hour |
int |
1000 |
Allowed requests per hour |
rate_limit_burst_size |
int |
10 |
Burst capacity (token-bucket strategy) |
rate_limit_window_type |
str |
"sliding" |
Window strategy: "sliding" or "fixed" |
rate_limit_whitelist |
list[str] |
[] |
IPs/keys never rate limited |
rate_limit_blacklist |
list[str] |
[] |
IPs/keys always rejected |
Programmatic Setup¶
For finer control (custom per-path rules, a Redis-backed store, multi-instance
deployments) wire the middleware yourself with setup_rate_limiting. It is exported from
fraiseql.security, takes the FastAPI app returned by create_fraiseql_app, and installs
RateLimitMiddleware.
from fraiseql.fastapi import create_fraiseql_app
from fraiseql.security import (
RateLimit,
RateLimitRule,
RateLimitStrategy,
setup_rate_limiting,
)
app = create_fraiseql_app(
database_url="postgresql://localhost/mydb",
types=[...],
queries=[...],
)
custom_rules = [
# Throttle the GraphQL endpoint
RateLimitRule(
path_pattern="/graphql",
rate_limit=RateLimit(requests=60, window=60),
message="GraphQL rate limit exceeded",
),
# Tighter limit on auth endpoints (5 requests / 5 minutes)
RateLimitRule(
path_pattern="/auth/*",
rate_limit=RateLimit(requests=5, window=300),
message="Authentication rate limit exceeded",
),
]
setup_rate_limiting(
app,
custom_rules=custom_rules,
default_limit=RateLimit(requests=100, window=60),
)
RateLimit accepts a strategy (RateLimitStrategy.FIXED_WINDOW,
RateLimitStrategy.SLIDING_WINDOW, or RateLimitStrategy.TOKEN_BUCKET) and an optional
burst value for the token-bucket strategy:
RateLimit(
requests=100,
window=60,
burst=20,
strategy=RateLimitStrategy.TOKEN_BUCKET,
)
If you do not pass custom_rules, FraiseQL installs a sensible set via
create_default_rate_limit_rules() (GraphQL, /auth/*, and /api/* patterns).
Multi-Instance Deployments (Redis)¶
The in-memory store keeps counters per process, so each app instance enforces limits
independently. To share state across instances, pass a Redis client — setup_rate_limiting
then uses RedisRateLimitStore automatically:
import redis.asyncio as redis
from fraiseql.security import setup_rate_limiting
redis_client = redis.from_url("redis://localhost:6379/0")
setup_rate_limiting(app, redis_client=redis_client)
Key Extraction Strategy¶
Rate limits are applied using the following key extraction logic:
1. Authenticated Requests¶
For requests with an authenticated user (the auth layer sets request.state.user_id):
- Key:
user:<user_id> - Use case: authenticated users are tracked individually
2. Unauthenticated Requests¶
For requests without authentication:
- Key:
ip:<client_ip> - Use case: protects against anonymous abuse
3. IP Address Resolution¶
When extracting the client IP address, the middleware checks, in order:
- The first entry of the
X-Forwarded-Forheader (if present) - The
X-Real-IPheader (if present) - The direct socket peer address (
request.client.host)
Security note:
X-Forwarded-ForandX-Real-IPare client-supplied and can be spoofed. Only trust them when your application sits behind a proxy you control that overwrites these headers. When exposed directly, strip or ignore them at the proxy so the socket peer address is used.
Response Headers¶
When a request is rejected, the middleware returns HTTP 429 with the following headers:
Retry-After: 60 # Seconds to wait before retrying
X-RateLimit-Limit: 100 # Maximum requests in the window
X-RateLimit-Window: 60 # Window length in seconds
The JSON body of a rejected non-GraphQL request looks like:
{
"error": "Rate Limit Exceeded",
"message": "Rate limit exceeded",
"retry_after": 60
}
GraphQL operations that exceed a limit return a GraphQL error with
extensions.code = "RATE_LIMITED".
Example client handling:
import time
import requests
def graphql_request(url, query):
while True:
response = requests.post(url, json={"query": query})
if response.status_code == 429: # Too Many Requests
wait_time = int(response.headers.get("Retry-After", "60"))
print(f"Rate limited, retrying in {wait_time}s")
time.sleep(wait_time)
continue
return response.json()
GraphQL-Aware Limits¶
For POST /graphql, the middleware applies additional limits via GraphQLRateLimiter:
- Per operation type: separate buckets for
query,mutation, andsubscription. - Per estimated complexity: queries are bucketed into
low/medium/hightiers (based on nesting depth and field count), each with its own limit.
A request must pass both the operation-type limit and the complexity limit. These limits are keyed on the authenticated user when available, otherwise on the client IP.
Strategies¶
RateLimitStrategy controls how requests are counted:
- Fixed window (
FIXED_WINDOW): a counter resets at the start of each window. - Sliding window (
SLIDING_WINDOW): smooths counting across the window boundary. - Token bucket (
TOKEN_BUCKET): each key gets a bucket of up tobursttokens that refills over the window; each request consumes one token. This allows short bursts while bounding the long-run rate.
Set the application-wide strategy with rate_limit_window_type, or per rule via the
strategy argument to RateLimit.
Disabling Rate Limiting¶
To disable rate limiting (not recommended for production), set the flag in config:
config = FraiseQLConfig(
database_url="postgresql://localhost/mydb",
rate_limit_enabled=False,
)
Or via environment variable:
FRAISEQL_RATE_LIMIT_ENABLED=false
Monitoring¶
Rate limit violations are recorded through FraiseQL's security logger (see
fraiseql.audit.get_security_logger). Each violation logs the client IP, endpoint, the
limit and window, and — when known — the user ID and operation metadata.
Configure visibility through Python's standard logging:
import logging
logging.getLogger("fraiseql.security").setLevel(logging.DEBUG)
Pipe these logs into your aggregation stack (ELK, Splunk, etc.) and alert on spikes in
429 responses to catch abuse or misconfigured clients.
Security Considerations¶
-
DoS Protection: Rate limiting helps prevent DoS attacks but should be combined with other protections (firewall rules, WAF).
-
Proxy Spoofing:
X-Forwarded-For/X-Real-IPare client-controlled. Only trust them behind a proxy you control that overwrites them; otherwise strip them at the proxy. -
Distributed Attacks: the in-memory store is per app instance. Use a Redis-backed store (
RedisRateLimitStore) for shared limits across multiple instances. -
User ID Extraction: ensure authentication is correct and
request.state.user_idcannot be forged before relying on per-user limits. -
Clock Skew: counting uses system time. Significant clock skew between instances can affect accuracy when sharing state.
Testing Rate Limiting¶
Test the implementation against the default per-minute limit:
# Send more requests than the per-minute limit allows
for i in $(seq 1 110); do
curl -s http://localhost:8000/graphql \
-H "Content-Type: application/json" \
-d '{"query":"{ users { id } }"}'
echo "Request $i"
done
Once the configured limit is exceeded, further requests return HTTP 429 (Too Many
Requests).