Rate Limiting with Flask-Limiter
Rate Limiting with Flask-Limiter
Without rate limiting, anyone can hammer your login route 10,000 times per second to brute-force passwords, or scrape your entire database through your API. Rate limiting prevents this.
Installation
pip install Flask-Limiter
Flask-Limiter==3.8.0
Basic Setup
# app.py
from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
import os
app = Flask(__name__)
limiter = Limiter(
app=app,
key_func=get_remote_address, # Limit per IP address
default_limits=["200 per day", "50 per hour"], # Global defaults
storage_uri=os.environ.get("REDIS_URL", "memory://")
)
storage_uri="memory://" works for single-process development. In production with multiple workers, use Redis.
Applying Limits to Routes
# Use the global default limits
@app.route("/")
def home():
return render_template("home.html")
# Override with stricter limits (brute force protection)
@app.route("/login", methods=["POST"])
@limiter.limit("5 per minute")
def login():
# Only 5 login attempts per minute per IP
pass
# Multiple limits on one route
@app.route("/api/data")
@limiter.limit("100 per hour")
@limiter.limit("10 per minute")
def api_data():
pass
# Exempt a route from all limits (public CDN endpoint, health check)
@app.route("/health")
@limiter.exempt
def health():
return "ok"
Limit Strings
Flask-Limiter uses a human-readable format:
"5 per minute"
"100 per hour"
"1000 per day"
"3 per second"
"10 per 15 minutes"
Custom Error Response
When a limit is hit, Flask-Limiter returns a 429 by default. Customize it:
from flask import jsonify, render_template
from flask_limiter.errors import RateLimitExceeded
@app.errorhandler(429)
def rate_limit_handler(e):
# For API requests, return JSON
if request.is_json or request.path.startswith("/api/"):
return jsonify({
"error": "Rate limit exceeded",
"message": str(e.description),
"retry_after": e.retry_after
}), 429
# For browser requests, render a page
return render_template("429.html", retry_after=e.retry_after), 429
Limiting Per User (Not Just IP)
IP-based limits break when many users share an IP (offices, universities). Limit by user ID for authenticated routes:
def get_user_key():
"""Use user ID if logged in, otherwise fall back to IP."""
from flask import session
if "user_id" in session:
return f"user:{session['user_id']}"
return get_remote_address()
# Apply per-user limit to specific routes
@app.route("/api/export")
@login_required
@limiter.limit("3 per hour", key_func=get_user_key)
def export_data():
pass
Production Setup with Redis
In production, multiple gunicorn workers don't share memory. Use Redis so all workers share the same counter:
limiter = Limiter(
app=app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"],
storage_uri=os.environ.get("REDIS_URL", "redis://localhost:6379/0")
)
Add to .env:
REDIS_URL=redis://localhost:6379/0
Common Limit Configurations
# Login — strict anti-brute-force
@app.route("/login", methods=["POST"])
@limiter.limit("5 per minute; 20 per hour")
def login():
pass
# Registration — prevent spam signups
@app.route("/register", methods=["POST"])
@limiter.limit("3 per hour")
def register():
pass
# Password reset — prevent email flooding
@app.route("/forgot-password", methods=["POST"])
@limiter.limit("3 per hour")
def forgot_password():
pass
# Public API — reasonable usage
@app.route("/api/search")
@limiter.limit("30 per minute")
def api_search():
pass
# Admin actions — low limit, high severity
@app.route("/admin/delete-user/<int:id>", methods=["POST"])
@admin_required
@limiter.limit("10 per hour", key_func=get_user_key)
def delete_user(id):
pass
Showing Limit Headers to API Clients
Flask-Limiter automatically adds rate limit headers to responses:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1735689600
API clients can use these to know when to back off.
Was this helpful?
Get More Flask Vibe Tutorials
Join 1,000+ developers getting weekly Flask tips and AI-friendly code patterns.
No spam. Unsubscribe anytime.