Flask Rate Limiting
Introduction
Rate limiting is an essential technique in web applications that restricts how many requests a client can make to your API within a specified time period. It serves several important purposes:
- Prevents abuse: Stops malicious users from overwhelming your server with too many requests
- Ensures fair usage: Prevents any single user from consuming too many resources
- Improves reliability: Helps maintain service quality during peak loads
- Reduces costs: Limits resource consumption, especially important for paid API services
In this tutorial, we'll explore how to implement rate limiting in Flask applications using the popular Flask-Limiter
extension, which integrates seamlessly with Flask's request handling system.
Prerequisites
Before we begin, you should have:
- Basic knowledge of Flask
- A working Flask application
- Python 3.6+ installed
- Pip package manager
Installing Flask-Limiter
First, let's install the Flask-Limiter extension:
pip install Flask-Limiter
This extension provides a simple way to apply rate limits to your Flask routes based on various criteria like IP address, user identity, or custom keys.
Basic Rate Limiting Setup
Let's start with a simple example of rate limiting in a Flask application:
from flask import Flask, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
# Initialize the rate limiter
limiter = Limiter(
get_remote_address, # Function that returns the client's IP address
app=app,
default_limits=["200 per day", "50 per hour"], # Default limits
storage_uri="memory://", # Store rate limiting data in memory
)
@app.route("/")
def index():
return "Hello, World! This endpoint has default rate limits."
@app.route("/api/limited")
@limiter.limit("5 per minute") # Custom limit for this route
def limited_route():
return jsonify({"message": "This endpoint is rate limited to 5 requests per minute"})
if __name__ == "__main__":
app.run(debug=True)
In this example:
- We import the necessary modules
- Initialize a
Limiter
object with default limits of 200 requests per day and 50 per hour - Create two routes:
- A default route with the default rate limits
- A more restricted route limited to 5 requests per minute
If you try accessing the /api/limited
endpoint more than 5 times within a minute, you'll receive a 429 Too Many Requests
error response.
Understanding Rate Limit Syntax
The rate limit strings follow a simple syntax:
[number] per [time period]
- Time periods can be:
second
,minute
,hour
,day
,month
,year
- You can combine multiple limits:
"5 per minute; 100 per day"
Examples:
"100 per day"
"10 per hour"
"5 per minute"
"1 per second"
Customizing Rate Limit Responses
By default, when a client exceeds the rate limit, Flask-Limiter returns a 429 Too Many Requests
status code with a basic message. You can customize this response:
@app.errorhandler(429)
def ratelimit_handler(e):
return jsonify({
"status": "error",
"message": "Rate limit exceeded. Please try again later.",
"retry_after": e.description
}), 429
Advanced Rate Limiting Techniques
Dynamic Rate Limits
Sometimes you need different rate limits for different types of users. Here's how to implement dynamic rate limits:
from flask import request
def limit_by_user_type():
if request.headers.get("X-API-KEY") == "premium-key":
return "100 per minute"
return "5 per minute"
@app.route("/api/dynamic")
@limiter.limit(limit_by_user_type)
def dynamic_limit():
return jsonify({"message": "This endpoint has dynamic rate limits"})
Rate Limiting by User Identity
For authenticated users, you might want to rate limit based on user ID rather than IP address:
from flask import g
def get_user_id():
# Assuming you store user_id in Flask's g after authentication
return g.user_id if hasattr(g, 'user_id') else get_remote_address()
# Create a new limiter that uses user ID when available
user_limiter = Limiter(
get_user_id,
app=app,
default_limits=["300 per day", "60 per hour"],
storage_uri="memory://",
)
@app.route("/api/user")
@user_limiter.limit("10 per minute")
def user_specific_limit():
return jsonify({"message": "This endpoint has user-specific rate limits"})
Exempting Routes
Sometimes you need to exempt certain routes from rate limiting:
@app.route("/health")
@limiter.exempt
def health_check():
return jsonify({"status": "ok"})
Rate Limiting with Persistent Storage
In a production environment, you'll want to use a persistent storage backend instead of memory storage. Redis is a popular choice:
# First install the Redis dependencies
# pip install redis
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per day", "50 per hour"],
storage_uri="redis://localhost:6379", # Use Redis as storage backend
strategy="fixed-window", # Use fixed-window strategy
)
This ensures that your rate limit counters persist across server restarts.
Rate Limiting Strategies
Flask-Limiter supports different rate limiting strategies:
- Fixed Window: Simplest approach, resets counters at the end of each time period
- Moving Window: More accurate but resource-intensive, maintains a sliding window of requests
- Elastic Window: A hybrid approach between the fixed and moving window methods
# Example with moving window strategy
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per day", "50 per hour"],
storage_uri="memory://",
strategy="moving-window", # Use moving window strategy
)
Real-World Example: API Service with Tiered Rate Limiting
Let's create a more comprehensive example of an API service with different rate limits for different user tiers:
from flask import Flask, request, jsonify, g
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import functools
app = Flask(__name__)
# Mock database of API keys
API_KEYS = {
"free-tier-key": {"tier": "free", "user_id": "user1"},
"basic-tier-key": {"tier": "basic", "user_id": "user2"},
"premium-tier-key": {"tier": "premium", "user_id": "user3"}
}
# Rate limits for different tiers
TIER_LIMITS = {
"free": "5 per minute; 100 per day",
"basic": "20 per minute; 1000 per day",
"premium": "60 per minute; 5000 per day"
}
def get_user_tier():
api_key = request.headers.get("X-API-KEY", "")
user_info = API_KEYS.get(api_key, {"tier": "anonymous"})
g.user_tier = user_info.get("tier")
g.user_id = user_info.get("user_id", get_remote_address())
return g.user_id
# Initialize the limiter with user identification function
limiter = Limiter(
get_user_tier,
app=app,
default_limits=["3 per minute"], # Very restrictive default for unauthenticated users
storage_uri="memory://",
)
# Auth decorator
def require_api_key(f):
@functools.wraps(f)
def decorated(*args, **kwargs):
api_key = request.headers.get("X-API-KEY")
if api_key not in API_KEYS:
return jsonify({"error": "Valid API key required"}), 401
return f(*args, **kwargs)
return decorated
# Routes
@app.route("/api/public")
def public_endpoint():
return jsonify({
"message": "This is a public endpoint with strict rate limits",
"tier": getattr(g, "user_tier", "anonymous")
})
@app.route("/api/data")
@require_api_key
def get_data():
return jsonify({
"message": f"Data accessed with {g.user_tier} tier privileges",
"tier": g.user_tier,
"rate_limit": TIER_LIMITS[g.user_tier]
})
@app.route("/api/premium")
@require_api_key
@limiter.limit(lambda: TIER_LIMITS[g.user_tier])
def premium_feature():
if g.user_tier != "premium":
return jsonify({"error": "Premium tier required for this endpoint"}), 403
return jsonify({
"message": "You're accessing a premium feature",
"data": "Exclusive content here"
})
@app.errorhandler(429)
def ratelimit_handler(e):
return jsonify({
"error": "Rate limit exceeded",
"tier": getattr(g, "user_tier", "anonymous"),
"limit": e.description,
"retry_after": e.headers.get('Retry-After', '60')
}), 429
if __name__ == "__main__":
app.run(debug=True)
This example demonstrates:
- Tiered rate limiting based on API key
- Dynamic rate limits per user tier
- User identification for rate limiting
- Custom error handling for rate limit exceptions
- Endpoint-specific permissions combined with rate limiting
Testing Your Rate Limits
To verify your rate limits are working correctly, you can use tools like curl
in a bash script:
#!/bin/bash
echo "Testing rate limits..."
for i in {1..10}
do
echo "Request $i"
curl -i http://localhost:5000/api/limited
echo -e "\n"
sleep 0.5
done
After the 5th request within a minute, you should see the 429 error response.
Monitoring Rate Limits
For production applications, it's important to monitor how your rate limits are being used. Flask-Limiter can be configured to log rate limit events:
import logging
# Configure logging
limiter.logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
limiter.logger.addHandler(handler)
# Or use your app's logger
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per day", "50 per hour"],
storage_uri="redis://localhost:6379",
logger=app.logger
)
Summary
Rate limiting is a crucial technique for protecting your Flask applications from abuse and ensuring fair resource usage. In this tutorial, we've covered:
- Basic rate limiting setup with Flask-Limiter
- Customizing rate limit responses
- Advanced techniques like dynamic and user-specific rate limiting
- Using persistent storage for production environments
- Different rate limiting strategies
- A comprehensive example with tiered API access
Implementing proper rate limiting will make your APIs more robust, fair, and secure. It's an essential part of any production-ready web application.
Additional Resources
Exercises
- Implement rate limiting in a Flask application with different limits for authenticated and unauthenticated users.
- Create a Flask API with three different endpoints, each with its own rate limit.
- Set up Redis as a persistent storage backend for rate limiting and test that limits persist across server restarts.
- Implement a "burst" rate limiting strategy that allows occasional spikes in usage.
- Build a dashboard endpoint that shows current rate limit usage for the authenticated user.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)