Skip to main content

Flask Rate Limiting

Introduction

Rate limiting is an essential security feature for web applications that restricts how many requests a user can make within a specific time period. By implementing rate limiting in your Flask application, you can:

  • Prevent abuse and brute force attacks
  • Protect your application from being overwhelmed
  • Ensure fair resource usage among all users
  • Improve overall application stability and security

In this tutorial, we'll explore different approaches to implement rate limiting in Flask applications, starting from basic concepts and moving to more sophisticated solutions.

Understanding Rate Limiting

Rate limiting works by tracking the number of requests made by a particular client (usually identified by IP address or API key) and temporarily blocking additional requests once a predefined threshold has been reached.

Common rate limiting strategies include:

  • Fixed Window: Count requests in fixed time intervals (e.g., 100 requests per hour)
  • Sliding Window: Track requests in a continuously moving time window
  • Token Bucket: Use a token-based approach where tokens are added at a fixed rate and consumed by requests
  • Leaky Bucket: Similar to token bucket but processes requests at a constant rate

Implementing Rate Limiting with Flask-Limiter

The easiest way to add rate limiting to a Flask application is by using the Flask-Limiter extension, which provides a clean and flexible API.

Installation

First, let's install the extension:

bash
pip install Flask-Limiter

Basic Setup

Here's a simple example of how to set up 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 limiter
limiter = Limiter(
app=app,
key_func=get_remote_address, # Use IP address as the rate limiting key
default_limits=["200 per day", "50 per hour"] # Default limits
)

# A simple route with default rate limits
@app.route("/")
def index():
return jsonify({"message": "Welcome to the rate-limited API!"})

# A route with custom rate limits
@app.route("/sensitive")
@limiter.limit("5 per minute") # Override the default limits
def sensitive_route():
return jsonify({"message": "This is a rate-limited sensitive resource"})

if __name__ == "__main__":
app.run(debug=True)

In this example:

  • We created a Limiter instance that applies default limits of 200 requests per day and 50 requests per hour to all routes
  • For a specific sensitive route, we added a stricter limit of 5 requests per minute
  • The get_remote_address function is used to identify clients by their IP address

Testing Rate Limiting

When you run this application and make requests, everything works normally until you exceed the defined limits. Once the limits are exceeded, the server returns a 429 Too Many Requests status code with an appropriate message.

For example, if you send more than 5 requests to the /sensitive endpoint within a minute, you'll get:

HTTP/1.1 429 Too Many Requests
Content-Type: text/html; charset=utf-8

Rate limit exceeded. Try again in 56 seconds.

Custom Rate Limiting Strategies

Rate Limiting Specific HTTP Methods

You can apply different rate limits based on the HTTP method:

python
@app.route("/api/resource", methods=["GET", "POST"])
@limiter.limit("100/day", methods=["GET"])
@limiter.limit("10/day", methods=["POST"])
def resource():
if request.method == "GET":
return jsonify({"message": "Getting resource"})
else:
return jsonify({"message": "Creating resource"})

Exempting Routes from Rate Limiting

Sometimes you need to exempt certain routes from rate limiting:

python
@app.route("/health")
@limiter.exempt
def health_check():
return jsonify({"status": "healthy"})

Custom Rate Limit Keys

You might want to rate limit based on user identity rather than IP address:

python
from flask import request

def get_user_id():
# Get user ID from authentication token or session
return request.headers.get('X-API-Key', get_remote_address())

limiter = Limiter(
app=app,
key_func=get_user_id,
default_limits=["200 per day", "50 per hour"]
)

Advanced Rate Limiting with Redis

For production applications, it's recommended to use Redis as a storage backend for rate limiting, especially when running multiple application instances:

python
from flask import Flask, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_limiter.extension import C

app = Flask(__name__)

limiter = Limiter(
app=app,
key_func=get_remote_address,
storage_uri="redis://localhost:6379",
storage_options={"socket_connect_timeout": 30},
strategy="fixed-window", # or "moving-window"
default_limits=["200 per day", "50 per hour"]
)

@app.route("/")
def index():
return jsonify({"message": "Welcome to the API!"})

if __name__ == "__main__":
app.run(debug=True)

This configuration uses Redis to store rate limiting data, allowing it to be shared across multiple application instances.

Dynamic Rate Limits

You can also implement dynamic rate limits based on user roles or subscription levels:

python
from flask import Flask, request, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

app = Flask(__name__)

def get_user_limit():
# Get the user's API key from request
api_key = request.headers.get('X-API-Key')

# In a real app, you'd look up the user's subscription level
if api_key == 'premium-user-key':
return "1000 per day"
elif api_key == 'basic-user-key':
return "100 per day"
else:
return "10 per day" # Default for unauthenticated users

limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)

@app.route("/api/data")
@limiter.limit(get_user_limit)
def get_data():
return jsonify({"message": "Here's your data!"})

if __name__ == "__main__":
app.run(debug=True)

Real-World Example: Protecting Login Endpoints

A common use case for rate limiting is protecting authentication endpoints from brute force attacks:

python
from flask import Flask, request, jsonify
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import bcrypt

app = Flask(__name__)

# Sample user database (in a real app, use a proper database)
users = {
"[email protected]": {
"password_hash": bcrypt.hashpw("password123".encode('utf-8'), bcrypt.gensalt())
}
}

limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)

@app.route("/api/login", methods=["POST"])
@limiter.limit("5 per minute") # Strict limit for login attempts
def login():
data = request.get_json()
email = data.get("email")
password = data.get("password")

if not email or not password:
return jsonify({"error": "Email and password required"}), 400

if email not in users:
# Don't reveal if the user exists or not
return jsonify({"error": "Invalid credentials"}), 401

if bcrypt.checkpw(password.encode('utf-8'), users[email]["password_hash"]):
# In a real app, generate and return a token here
return jsonify({"message": "Login successful"}), 200
else:
return jsonify({"error": "Invalid credentials"}), 401

if __name__ == "__main__":
app.run(debug=True)

This example implements strict rate limiting on the login endpoint, allowing only 5 attempts per minute from the same IP address, which significantly reduces the risk of brute force attacks.

Custom Rate Limit Response

You can customize the response when a client exceeds the rate limit:

python
@app.errorhandler(429)
def ratelimit_handler(e):
return jsonify({
"error": "Rate limit exceeded",
"message": str(e.description),
"retry_after": e.retry_after
}), 429

Summary

Rate limiting is a crucial security feature for Flask applications, especially those exposing APIs or authentication endpoints. By implementing rate limiting, you can:

  1. Protect your application from abuse and brute force attacks
  2. Ensure fair resource distribution among users
  3. Prevent denial of service scenarios
  4. Improve overall application performance and stability

We've covered how to implement rate limiting using the Flask-Limiter extension, from basic setups to advanced configurations with Redis. We've also explored practical examples like protecting login endpoints and implementing dynamic rate limits based on user roles.

Additional Resources

Exercises

  1. Implement rate limiting on a Flask API that differentiates between authenticated and unauthenticated users.
  2. Create an endpoint that shows users their current rate limit status and remaining requests.
  3. Implement a tiered rate limiting system with three different user levels (free, basic, premium).
  4. Add custom rate limiting headers to your responses to inform clients about their rate limit status.
  5. Set up Redis as a backend for your rate limiter and deploy multiple instances of your application to test distributed rate limiting.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)