Skip to main content

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:

bash
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:

python
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:

  1. We import the necessary modules
  2. Initialize a Limiter object with default limits of 200 requests per day and 50 per hour
  3. 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:

python
@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:

python
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:

python
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:

python
@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:

python
# 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:

  1. Fixed Window: Simplest approach, resets counters at the end of each time period
  2. Moving Window: More accurate but resource-intensive, maintains a sliding window of requests
  3. Elastic Window: A hybrid approach between the fixed and moving window methods
python
# 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:

python
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:

  1. Tiered rate limiting based on API key
  2. Dynamic rate limits per user tier
  3. User identification for rate limiting
  4. Custom error handling for rate limit exceptions
  5. 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:

bash
#!/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:

python
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

  1. Implement rate limiting in a Flask application with different limits for authenticated and unauthenticated users.
  2. Create a Flask API with three different endpoints, each with its own rate limit.
  3. Set up Redis as a persistent storage backend for rate limiting and test that limits persist across server restarts.
  4. Implement a "burst" rate limiting strategy that allows occasional spikes in usage.
  5. 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! :)