Open-source project
alisaifee/flask-limiter avatar
alisaifee/flask-limiter

Flask-Limiter puts rate limits on the decorator, not the middleware

Rate Limiting extension for Flask

1,207 stars132 forksPythonMIT

At a glance

What is it?
Flask-Limiter adds rate limiting to Flask applications by wrapping routes, blueprints and class based views with decorators, while the counting and storage work is delegated to the limits library. It covers a lot of ground, but the choice of strategy and storage is where the real decisions sit.
Who is it for?
Flask-Limiter suits a Flask team that wants limits declared next to the view they protect, with shared counters across workers, and that is willing to run a Redis or Memcached instance to hold them. It does not suit a project that cannot add a stateful dependency, since the in memory storage_uri used in the quickstart is per process, or a team that needs per user limits behind a proxy without supplying its own key function.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 166 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Four places to declare a limit, from coarse to per route

Rate limiting in a Flask app is easy to get wrong at the edges, because a limit applied to the wrong layer either blocks a shared client or fails to protect anything. Flask-Limiter lets you choose the level deliberately. The project lists application wide global limits per user, default limits per route, limits by Blueprint, limits by class based views, and limits on individual routes.

The default is the useful part of the design. A Limiter constructed with default_limits applies to every route it manages, and individual routes can then override it, which is the opposite of the usual arrangement where protection is opt in and a forgotten decorator leaves an endpoint open. The quickstart shows all three states in one file: a route with the default limit, a route with its own decorator, and a route marked exempt.

The Blueprint and class based view levels exist because real applications are organised that way, not in one flat list of functions. A Blueprint can carry a limit for every route inside it, and a class based view can carry one for the whole class, so a plan tier or an API namespace can be throttled in a single place rather than per method. The trade off is that limits declared this way are less visible at the call site, so a reader of one handler does not see the ceiling that applies to it.

strategy= decides the shape of the window

The counting logic is not implemented in Flask-Limiter. The project delegates to the limits library, and the configuration makes that explicit: strategy="fixed-window" is given as the default in the quickstart, with moving-window and sliding-window-counter offered as the alternatives. The README also states that any rate limiting strategy supported by limits can be used.

That delegation is a strength and a limitation at the same time. It means the storage and algorithm work is shared with a wider ecosystem, and the extras list in the project metadata shows the coupling is explicit, with optional installs for limits[redis], limits[memcached], limits[mongodb] and limits[valkey]. It also means the trade offs between the strategies are documented elsewhere rather than here, and the README does not explain when one window is preferable to another. A team that has never thought about window edge cases will have to read the limits documentation to make that choice, and then pin a strategy name in configuration where it will be read by the next person.

The limit expressions themselves are plain strings, for example "2 per minute", "1 per second" and "1 per day", which keeps configuration legible and lets different routes carry different granularities without a custom object.

storage_uri decides where the counters live

The counters have to live somewhere, and the storage_uri setting decides. The quickstart ships with storage_uri="memory://", and the commented alternatives in the same block name the production options: redis://localhost:6379, a Redis cluster form with several hosts, memcached://localhost:11211 with a cluster form, and mongodb://localhost:27017. A project can therefore change backend by editing one string, which is the migration path the design implies.

The repository ships a docker-compose.yml that makes the local versions of those services easy to run, with memcached mapped to host port 31211, redis to 46379 and mongo to 47017. The offsets are deliberate, so a local Redis on 46379 does not collide with a Redis already running on 6379. That file is also the clearest statement of what the project considers the realistic backend set.

The limitation to keep in mind is the one the memory option implies. A counter held in the process memory of a single worker only knows what that worker counted, so a multi worker deployment needs a shared backend, and a multi process one needs a shared backend for the same reason. The in memory setting is a development convenience, and treating it as a deployment choice means a limit that behaves differently under load than it did in testing.

The CLI prints the limit tree, which is the fastest way to audit it

One of the most useful things here costs one extra install. With the cli extra, the project registers a flask subcommand through the flask.commands entry point, pointing at flask_limiter.commands:cli, and the resulting command prints the effective limits as a tree:

bash
$ FLASK_APP=app:app flask limiter limits

app
├── fast: /fast
│   ├── 2 per 1 minute
│   └── 1 per 1 second
├── ping: /ping
│   └── Exempt
└── slow: /slow
    └── 1 per 1 day

The tree resolves inheritance for you. The fast route shows the default limits applied to it, the ping route shows as Exempt, and the slow route shows only its own decorator, which means the default does not stack on top of it. That last detail is a common source of confusion with layered limiters, and the tree makes it visible in one command instead of by reading the source.

It also gives a team a cheap regression check. Wiring this into a test or a deployment script turns limit configuration into something reviewable, since a route that quietly lost its decorator shows up as a changed tree.

First run: install, wire the limiter, watch the 429

The quickstart starts with the package install:

bash
pip install Flask-Limiter

Then the limiter is constructed against the app, with the key function, the default limits, the storage location and the strategy all in one call:

python
from flask import Flask
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

app = Flask(__name__)
limiter = Limiter(
    get_remote_address,
    app=app,
    default_limits=["2 per minute", "1 per second"],
    storage_uri="memory://",
    strategy="fixed-window",
)

Routes opt in to a stricter limit or out of limiting entirely:

python
@app.route("/slow")
@limiter.limit("1 per day")
def slow():
    return "24"

@app.route("/ping")
@limiter.exempt
def ping():
    return 'PONG'

Run the app and hit it. The third call to the fast route is rejected, while the exempt route keeps answering:

bash
$ FLASK_APP=app:app flask run
$ curl localhost:5000/fast
42
$ curl localhost:5000/fast
42
$ curl localhost:5000/fast
<title>429 Too Many Requests</title>
<h1>Too Many Requests</h1>
<p>2 per 1 minute</p>

That 429 body is worth reading once in a local run, because it tells you which limit was hit, which matters when a route inherits a default you did not know was there.

The key function decides who counts as one user

The first argument to Limiter is the key function, and the quickstart uses get_remote_address, imported from flask_limiter.util. That single choice defines the identity the counters are keyed on, and it is where most production surprises come from.

If your app runs behind a reverse proxy or a load balancer, every request may arrive with the same address unless the proxy is configured to forward the original one, and the limit then applies to the proxy rather than to the caller. The README does not document proxy header handling, so this is not something the project resolves for you. A deployment that needs per user limits behind a proxy has to pass its own key function.

The alternative approach is to key on something the application already knows, such as an account identifier or an API key, which gives per user limits rather than per address limits. The project lists application wide global limits per user as a supported configuration, so that pattern is within scope. What the project does not supply is the key function itself; get_remote_address is a convenience for the common case.

Packaging, linting and a slower release line

The project builds with hatchling and takes its version from the repository tags through hatch-vcs, with a no-guess-dev version scheme, and the wheel contains the flask_limiter package. It requires Python 3.10 or newer and lists support for versions 3.10 through 3.14, and it depends on limits>=3.13, Flask>=2, ordered-set and typing_extensions. The optional extras mirror the backends one for one, which keeps the install lean when a project only needs one of them.

Development tooling is modern and self contained. The Makefile runs ruff for import ordering, formatting and lint checks over the package, tests and examples, plus mypy against the package, all through uv, and there is a pytest.ini and a tests/ directory, along with a docker-compose.yml for backend services and examples/kitchensink.py and examples/sample.py as worked examples.

The licence is MIT, which is the permissive end of the scale for a library that other people deploy inside their own applications. The project also ships SECURITY.md, CODE_OF_CONDUCT.md, CONTRIBUTIONS.rst and HISTORY.rst, so vulnerability reporting and change history have a home.

On cadence, the repository is not archived and the last push was on 2026-05-10. The recent release tags are 4.1.0 and 4.1.1 in December 2025, after 4.0.0 in September 2025, and the gap between the last push and those tags suggests that work has landed on master without a numbered release behind it. Pin a version explicitly if you do not want a surprise, and read HISTORY.rst before upgrading across the 4.0 boundary, where the major number changed.

Editorial conclusion

Flask-Limiter suits a Flask team that wants limits declared next to the view they protect, with shared counters across workers, and that is willing to run a Redis or Memcached instance to hold them. It does not suit a project that cannot add a stateful dependency, since the in memory storage_uri used in the quickstart is per process, or a team that needs per user limits behind a proxy without supplying its own key function. Verify first that the key function you pass to Limiter matches what your deployment sees as the client address, because the quickstart keys on get_remote_address and the README does not document proxy header handling.

Frequently asked questions

How do I add rate limiting to a Flask app with Flask-Limiter?

Construct a Limiter with your app and a key function, then decorate routes with @limiter.limit, mark routes with @limiter.exempt, or set default limits to cover everything. The quickstart shows Limiter(get_remote_address, app=app, default_limits=["2 per minute", "1 per second"], storage_uri="memory://").

Where should Flask-Limiter store its counters in production?

In a shared backend such as Redis, Memcached or MongoDB, set through storage_uri, for example redis://localhost:6379 or memcached://localhost:11211. The memory:// setting used in the quickstart only holds counters inside the process, so it is a development choice.

Which rate limiting strategies does Flask-Limiter support?

Any strategy supported by the limits library, and the quickstart names fixed-window as the default with moving-window and sliding-window-counter as the alternatives. The strategy is set through the strategy argument on the Limiter.

How can I see the limits Flask-Limiter has applied?

Install the cli extra with pip install Flask-Limiter[cli] and run FLASK_APP=app:app flask limiter limits, which prints a tree of every route with its effective limits and shows exempt routes as Exempt.

Does Flask-Limiter support Flask Blueprints and class based views?

Yes. The project lists limits by Blueprint and by class based views alongside application wide limits, default limits per route and limits on individual routes.

Official sources

  1. alisaifee/flask-limiter on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/alisaifee-flask-limiter.svg)](https://hysenlabs.com/projects/alisaifee-flask-limiter)

Was this useful?