Skip to main content

taplimit

Small, typed, thread-safe in-process rate-limit decorator: N calls per period, block or raise.

Zero dependencies, one obvious decorator, and a reusable limiter object when you need to share a budget. The clock and sleep are injectable, so tests never wait on real time.

Install

pip install taplimit

Usage

Cap a function at N calls per period. By default a call over the limit blocks until a slot frees:

from taplimit import rate_limit

@rate_limit(calls=15, period=60)
def call_api() -> None:
    ...

Prefer to fail fast instead of waiting? Ask it to raise:

from taplimit import rate_limit, RateLimitExceeded

@rate_limit(calls=15, period=60, on_limit="raise")
def call_api() -> None:
    ...

try:
    call_api()
except RateLimitExceeded as exc:
    print(f"slow down, retry in {exc.retry_after:.1f}s")

Share one budget across several functions, or control slots by hand, with the limiter object. It is a decorator, a context manager, and exposes acquire / try_acquire:

from taplimit import RateLimiter

limiter = RateLimiter(calls=100, period=60)

@limiter
def read() -> None: ...

@limiter
def write() -> None: ...  # read and write draw on the same 100-per-minute budget

with limiter:            # guard a block of code
    do_work()

if limiter.try_acquire():  # never blocks; returns False when full
    do_work()

Testing without real time

clock (returns seconds) and sleep (waits seconds) are injectable, so a limiter runs instantly under a fake clock:

now = [0.0]
slept: list[float] = []

limiter = RateLimiter(
    calls=1, period=10,
    clock=lambda: now[0],
    sleep=lambda s: (slept.append(s), now.__setitem__(0, now[0] + s)),
)

How it limits

taplimit uses a sliding window: it keeps the timestamps of recent calls and a slot frees exactly period seconds after the call that took it. A burst never carries over a fixed boundary the way a reset-every-minute counter does, so the rate you ask for is the rate you get. All state is guarded by a lock, so one decorator or limiter is safe to share across threads.

Why

The simple niche of "let this function run at most N times per period, in one process, synchronously" is oddly unserved:

  • ratelimit popularised the @limits(calls, period) shape and is still downloaded millions of times a month, but it has been abandoned since 2018 (last release 2.2.1). It also uses a fixed window and its threading story is thin.
  • pyrate-limiter and limits are maintained and capable, but they are backend-oriented and heavy: buckets, optional SQLite / Redis stores, async machinery. Overkill when all you want is a decorator on one function.
  • limiter is a token-bucket library with a different rate / capacity API, not the plain calls-per-period shape.

taplimit is the small, modern, typed take: one decorator, a sliding window, thread-safe, no dependencies, and a clock you can fake in tests. Reach for it when a single process needs to stay under a rate and a full rate-limiting backend would be too much.

License

MIT. See LICENSE.

Metadata

Release files for taplimit 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for taplimit 0.1.0
File Size Uploaded
taplimit-0.1.0.tar.gz 8.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for taplimit 0.1.0
File Interpreter ABI Platform
taplimit-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 15.3 kB

Release files / taplimit-0.1.0.tar.gz

Download URL taplimit-0.1.0.tar.gz
Size 8.6 kB
Tags Source
SHA-256 checksum
How to use checksums
ad0d11630d2a3672413659ac8f8b2b210393fd5c99da1edc7027382e23adb889
BLAKE2b-256 checksum
How to use checksums
3e0a4868d72593a2b8a0705b333baa5cff171a91dbf09c3859ee1f142d9d81ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release files / taplimit-0.1.0-py3-none-any.whl

Download URL taplimit-0.1.0-py3-none-any.whl
Size 6.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3095a27d04c8c74c906f9eefe8a5c1afc0ccd0acd8498c6193a6f6b71ff6f343
BLAKE2b-256 checksum
How to use checksums
47c3202717fbe56f81893b992210344631bcd60dd70690c493b23cadfb206096
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.2

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page