Library / SDK
tomasbasham/ratelimit avatar
tomasbasham/ratelimit

ratelimit: a Python decorator for API call budgets

API Rate Limit Decorator

829 stars159 forksPythonMIT

At a glance

What is it?
The ratelimit package wraps a function in a decorator that counts invocations inside a time window and stops you from exceeding an API provider's call budget. It offers two responses to a breach, an exception or a blocking sleep, and it keeps its state inside your own process rather than in a shared store.
Who is it for?
Use ratelimit when you control the code that talks to a third-party API and the calling process is the only caller, such as a single worker, a script or a notebook. Skip it when the limit must be shared across several processes or hosts, or when the provider publishes a rolling window that a fixed per-process counter will not match, since the package exposes no shared backend and no header parsing.
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?
Activity is slowing. The repository last received commits 6 months 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 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A decorator that spends a call budget you define

Most third-party APIs ration how often a client may talk to them, and the project's description of the problem is direct: as the need to consume data grows, so does the number of API calls needed to stay current, and in a worst case your application can be banned from making further calls if it abuses those limits.

The ratelimit package answers that with a function decorator. You decorate the function that performs the request, tell the decorator how many calls are allowed over how many seconds, and the library counts invocations for you. When the count is exceeded, the call does not happen. The design is deliberately narrow: it is a single Python package, `ratelimit`, with no runtime dependencies at all, and the packaging metadata in setup.py lists an empty install_requires.

The audience is any Python developer calling a web service that imposes a quota. Because the budget is expressed as a decorator around your own function, it works with any HTTP client, not just requests, and it can sit on a helper that wraps several different endpoints.

How the limits decorator counts and refuses

The decorator takes two arguments that define the budget: calls, the number of invocations allowed, and period, the length of the window measured in seconds. The documented example uses calls=15 with a period of 900 seconds, which is fifteen minutes.

When no period is given, the window defaults to fifteen minutes, and the README states plainly that this is the time window imposed by Twitter. That default is a nice touch for a package whose main use case is social API access, though it is worth remembering that it is a default rather than a negotiated value, and a provider with a different window needs the period spelled out.

The refusal mechanism is an exception. Calling a decorated function more times than allowed within the window raises `ratelimit.RateLimitException`. The decorator therefore does not slow anything down on its own; it converts a policy violation into a signal that your code can react to.

What the documentation does not spell out is how the window is tracked internally, whether the counter is exact or sampled, or how the window boundary is handled when calls straddle it. The observable contract is what you get: a call count over a period, and an exception past the limit. If your provider uses a sliding window rather than a fixed one, that difference in semantics will show up at the edges, and you will have to reason about it yourself.

Installing from PyPI and decorating your first request

Installation is a normal PyPI package. The project suggests adding the package name to your requirements file.

python
    ratelimit

Then install the requirements.

bash
    $ pip install -r requirements.txt

If you would rather install it on its own, the same page gives the single command.

bash
    $ pip install ratelimit

A first use is a decorator on the function that makes the request. The example below imports the decorator, defines the window in seconds, and wraps a function that fetches a URL and raises on a non-200 response.

python
    from ratelimit import limits

    import requests

    FIFTEEN_MINUTES = 900

    @limits(calls=15, period=FIFTEEN_MINUTES)
    def call_api(url):
        response = requests.get(url)

        if response.status_code != 200:
            raise Exception('API response: {}'.format(response.status_code))
        return response

With this in place, the README states that the function cannot make more than 15 API calls within a 15 minute period. The decorator is the entire integration surface: import it, apply it, and every call through that function shares one budget.

sleep_and_retry blocks the thread instead of raising

The second strategy is `sleep_and_retry`. Instead of raising, it pauses the calling thread until enough time has passed for the budget to allow another call, then runs the function again. The README describes the trade-off in one line: every function invocation succeeds, at the cost of halting the thread.

python
    from ratelimit import limits, sleep_and_retry

    import requests

    FIFTEEN_MINUTES = 900

    @sleep_and_retry
    @limits(calls=15, period=FIFTEEN_MINUTES)
    def call_api(url):
        response = requests.get(url)

        if response.status_code != 200:
            raise Exception('API response: {}'.format(response.status_code))
        return response

Note the decorator order, which the example gets right: `sleep_and_retry` sits on the outside, and the counting `limits` decorator sits underneath it. Reversing the two would change what is counted and what sleeps, and the example is worth copying exactly rather than improvising.

This pattern fits a batch job or a data sync where finishing matters more than throughput. It does not fit a web request handler, where holding a worker thread for the remainder of a 15 minute window is a poor trade. Nothing in the documentation offers a timeout, a cap on the wait, or a way to bound the sleep, so the blocking time is effectively the rest of the window.

Raising the exception and handing the retry policy to backoff

The exception path is the more flexible of the two because it lets an outside library decide how to wait. The README shows the exception being passed to the `backoff` package with an exponential strategy and a cap on the number of tries.

python
    from ratelimit import limits, RateLimitException
    from backoff import on_exception, expo

    import requests

    FIFTEEN_MINUTES = 900

    @on_exception(expo, RateLimitException, max_tries=8)
    @limits(calls=15, period=FIFTEEN_MINUTES)
    def call_api(url):
        response = requests.get(url)

        if response.status_code != 200:
            raise Exception('API response: {}'.format(response.status_code))
        return response

The decorator order flips here. The backoff handler is on the outside, so it sees the RateLimitException raised by the counting decorator and retries the wrapped call, while the counting decorator stays underneath and keeps enforcing the same budget on each attempt. The two documented examples therefore use opposite stacking, and the rule to remember is that the layer which reacts goes on top of the layer which counts.

One thing to keep in mind is that backoff is a separate dependency, and it is only an example here. The rate limiting package itself does not depend on it, which keeps the core install small but means the retry policy is yours to write and to tune.

The counter lives in one process, not across your fleet

The most important limitation is not in the API but in the scope of the state. A decorator counts calls in the process where the function runs. The repository is a single package with no runtime dependencies, and there is no store, no Redis client, no file-backed counter and no shared server in the dependency list or the documented usage. Each worker, each process and each container gets its own budget.

That is fine for a script, a notebook, a cron job or a service with one worker that owns the API account. It is wrong the moment you scale out, because ten workers with a limit of 15 calls each can produce 150 calls in the same window and still look compliant to the library. The provider sees the aggregate, the library does not.

There is a second limitation in the same family. The package has no notion of what the provider says. It does not read a rate limit header, does not track a reset time returned by the server, and does not adjust to a quota that changes. The limit is whatever you hardcode in the decorator, and a provider that quietly tightens its quota will find your configured value stale until you change it. The README does not document any mechanism for reacting to server-side quota information.

An alternative is to rate limit at the edge of your service

The common alternative to decorating call sites is to enforce the limit in shared infrastructure, for example in an API gateway or a reverse proxy that all your workers pass through, or in a client library with a shared state backend. The difference in approach is straightforward. Gateway or shared-backend limiting sees every request from every process, so the count matches what the provider actually receives, and it can be changed without a code change. A decorator is local, explicit, and visible in the source, but it only knows about the calls made by the process it runs in.

There is also a middle option worth naming: keeping the decorator but storing the counter in shared state. The package as it stands offers no hook for that, since the state is internal to the decorator, so this route means writing or forking the mechanism rather than configuring it.

The other difference is placement. A decorator protects the specific call site, so unrelated code paths to the same API are not covered unless you also decorate them, while a gateway covers everything by default. For a service with a handful of well-known integration points, the decorator is less machinery. For a platform with many teams, the gateway wins on consistency.

An MIT package with no tagged releases and a dated last push

The licence is the MIT License, and the repository holds the licence text in LICENSE.txt. MIT is a permissive licence with no copyleft obligations, so the main practical requirement is that the copyright notice and licence text travel with redistributed copies, including inside a container image or a bundled artifact. That is a description of the terms and not legal advice.

On maintenance, the repository has no GitHub releases at all, so there is no tagged version history to point at. The last push to the repository was on 2026-04-13, and the project has not been archived. Six months of silence before the current date is enough that you should not describe it as actively maintained. The packaging metadata in setup.py still declares the classifier "Development Status :: 5 - Production/Stable", which is what the project asserts about itself, but a classifier written at release time does not move on its own.

The remaining repository files are ordinary project furniture: a .pylintrc, a .travis.yml for continuous integration, a pytest.ini, a tests directory, a CONTRIBUTING.rst and a CODE_OF_CONDUCT.rst. The requirements.txt at the root pins development tools only, namely pytest, pytest-cov and pylint, which confirms again that installing the package pulls in nothing at runtime.

Editorial conclusion

Use ratelimit when you control the code that talks to a third-party API and the calling process is the only caller, such as a single worker, a script or a notebook. Skip it when the limit must be shared across several processes or hosts, or when the provider publishes a rolling window that a fixed per-process counter will not match, since the package exposes no shared backend and no header parsing. Before adopting it, check the version you would install from PyPI against the last push to the repository, which was on 2026-04-13, and confirm that the window semantics in the installed release match the ones your provider documents.

Frequently asked questions

How do I limit how often a Python function calls an API with the ratelimit package?

Apply the limits decorator to the function that makes the request, passing calls and period in seconds, for example @limits(calls=15, period=FIFTEEN_MINUTES). Calls beyond that count inside the window raise ratelimit.RateLimitException.

What is the default time window in ratelimit, and can I choose my own?

When no period is given the window defaults to fifteen minutes, which the README ties to the time window imposed by Twitter. You can set your own by passing period in seconds, and the period argument is described simply as a time period in seconds.

Should I use sleep_and_retry or catch RateLimitException?

sleep_and_retry pauses the calling thread until the budget allows another call, so every invocation succeeds at the cost of halting the thread. Catching RateLimitException and applying your own retry policy, as the example does with the backoff package, leaves the waiting strategy under your control.

Does the ratelimit package share rate limit state between processes or servers?

The package is a decorator, so the call count it enforces is the count inside the process where the decorated function runs, and it ships with no runtime dependencies and no shared store. The documentation describes no mechanism for sharing counters between hosts.

What licence is ratelimit released under and how often is it updated?

The project is licensed under the MIT License. The repository has no GitHub releases, is not archived, and the last push was on 2026-04-13.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. tomasbasham/ratelimit on GitHub
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/tomasbasham-ratelimit.svg)](https://hysenlabs.com/projects/tomasbasham-ratelimit)

Was this useful?