Skip to main content
Feather offers two ways to rate limit. The decorator is convenient and keeps its counters in the process. Flask-Limiter with Redis shares counters between workers, which is what a limit needs in order to hold under more than one process.

The decorator

@rate_limit keeps counters in the process. Under gunicorn --workers 4 a limit of ten per minute is really forty. Treat it as a development guard and a convenience for single-process deployments, not as protection.

In production: Flask-Limiter

Scaffolded apps with authentication ship a rate_limits.py that handles this properly. It limits the Google OAuth login and callback and every admin POST route through Flask-Limiter, sharing counters across workers via Redis.
.env
Without it the limiter falls back to memory and logs a warning. The limits live in config.py as RATELIMIT_DEFAULT, RATELIMIT_LOGIN and RATELIMIT_ADMIN, each overridable by environment variable. Run flask limiter limits to see which routes are actually limited.

Two things that fail silently

Both cost real debugging time if you wire this up by hand.
Flask-Limiter enforces a limit only through the function it returns. Calling limiter.limit(rule)(app.view_functions[ep]) and discarding the result does nothing, and drops that endpoint out of the default limit as well.
A single page load fetches ten or more scripts and fonts from Flask. Without a limiter.request_filter exempting static and feather_static, a busy user gets a 429 on the app’s own JavaScript while the page is still loading.
A platform probes /health/ready from one address every few seconds. At every 5 seconds that is 720 requests an hour, over the default 600, so about an hour after each deploy the app answers its own readiness check with 429 and is taken out of rotation. The scaffold exempts health.health_check, health.liveness_check and health.readiness_check in EXEMPT_ENDPOINTS in rate_limits.py.