RateLimiter¶
The main rate limiter classes for async and sync usage.
RateLimiter (Async)¶
RateLimiter
¶
RateLimiter(repository=None, name=None, region=None, endpoint_url=None, stack_options=None, on_unavailable=_UNSET, auto_update=_UNSET, bucket_ttl_refill_multiplier=_UNSET, speculative_writes=True)
Async rate limiter backed by DynamoDB.
Implements token bucket algorithm with support for: - Multiple limits per entity/resource - Two-level hierarchy (parent/child entities) - Cascade mode (consume from entity + parent) - Stored limit configs - Usage analytics
Example (new API - preferred): from zae_limiter import RateLimiter, Repository, StackOptions
repo = Repository(
name="my-app",
region="us-east-1",
stack_options=StackOptions(),
)
limiter = RateLimiter(repository=repo)
Example (old API - deprecated): limiter = RateLimiter( name="my-app", region="us-east-1", stack_options=StackOptions(), )
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repository
|
RepositoryProtocol | None
|
Repository instance (new API, preferred). Pass a Repository or any RepositoryProtocol implementation. |
None
|
name
|
str | None
|
DEPRECATED. Use |
None
|
region
|
str | None
|
DEPRECATED. Use |
None
|
endpoint_url
|
str | None
|
DEPRECATED. Use |
None
|
stack_options
|
StackOptions | None
|
DEPRECATED. Use |
None
|
on_unavailable
|
OnUnavailable | Any
|
DEPRECATED. Use |
_UNSET
|
auto_update
|
bool | Any
|
DEPRECATED. Use |
_UNSET
|
bucket_ttl_refill_multiplier
|
int | Any
|
DEPRECATED. Use
|
_UNSET
|
speculative_writes
|
bool
|
Enable speculative UpdateItem fast path. When True, acquire() tries a speculative write first, falling back to the full read-write path only when needed. |
True
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If both repository and name/region/endpoint_url/stack_options are provided. |
list_deployed
async
classmethod
¶
List all deployed rate limiter instances in a region.
This is a class method that discovers existing deployments without
requiring an initialized RateLimiter instance. It queries CloudFormation
for stacks tagged with ManagedBy=zae-limiter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
region
|
str | None
|
AWS region (default: use boto3 defaults) |
None
|
endpoint_url
|
str | None
|
CloudFormation endpoint (for LocalStack) |
None
|
Returns:
| Type | Description |
|---|---|
list[LimiterInfo]
|
List of LimiterInfo objects describing deployed instances. |
list[LimiterInfo]
|
Sorted by user-friendly name. Excludes deleted stacks. |
Example
Discover all limiters in us-east-1¶
limiters = await RateLimiter.list_deployed(region="us-east-1") for limiter in limiters: if limiter.is_healthy: print(f"✓ {limiter.user_name}: {limiter.version}") elif limiter.is_failed: print(f"✗ {limiter.user_name}: {limiter.stack_status}")
Raises:
| Type | Description |
|---|---|
ClientError
|
If CloudFormation API call fails |
is_available
async
¶
Check if the rate limiter backend (DynamoDB) is reachable.
Performs a lightweight health check without requiring initialization. This method never raises exceptions - it returns False on any error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float
|
Maximum time in seconds to wait for response (default: 1.0) |
1.0
|
Returns:
| Type | Description |
|---|---|
bool
|
True if DynamoDB table is reachable, False otherwise. |
Example
limiter = RateLimiter(name="my-app", region="us-east-1") if await limiter.is_available(): async with limiter.acquire(...) as lease: ... else: # Handle degraded mode pass
create_entity
async
¶
Create a new entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Unique identifier for the entity |
required |
name
|
str | None
|
Human-readable name (defaults to entity_id) |
None
|
parent_id
|
str | None
|
Parent entity ID (None for root/project entities) |
None
|
cascade
|
bool
|
If True, acquire() will also consume from parent entity |
False
|
metadata
|
dict[str, str] | None
|
Additional metadata to store |
None
|
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
Returns:
| Type | Description |
|---|---|
Entity
|
The created Entity |
Raises:
| Type | Description |
|---|---|
EntityExistsError
|
If entity already exists |
delete_entity
async
¶
Delete an entity and all its related data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
ID of the entity to delete |
required |
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
Raises:
| Type | Description |
|---|---|
RateLimiterUnavailable
|
DynamoDB kept some deletes unprocessed
after the retries. What was deleted is audited, and calling
|
set_parent
async
¶
Move an entity to a new parent, or to no parent (ADR-150).
Takes effect on the next acquire in every process: the entity's bucket
items are restamped, and a process whose cache still names the old
parent learns the new one from the item on its next call, refunding
the old parent. Consumption already charged to the old parent, including
by leases open across the move, stays there. Whether the entity debits
the new parent follows its cascade policy (ADR-146) or its own
cascade flag, which a move does not change.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to move. |
required |
parent_id
|
str | None
|
The new parent, which must exist, or None for no parent. |
required |
principal
|
str | None
|
Caller identity for audit logging (optional). |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items restamped. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
The move would make the entity its own ancestor. |
EntityNotFoundError
|
The entity or the new parent does not exist. |
VersionMismatchError
|
The stack's Lambdas predate parent moves. |
FanoutIncomplete
|
The move landed but restamping stopped part-way; repeating the call finishes it. |
get_audit_events
async
¶
Get audit events for an entity.
Retrieves security audit events logged for administrative operations on the specified entity, ordered by most recent first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
ID of the entity to query |
required |
limit
|
int
|
Maximum number of events to return (default: 100) |
100
|
start_event_id
|
str | None
|
Event ID to start after (for pagination) |
None
|
Returns:
| Type | Description |
|---|---|
list[AuditEvent]
|
List of AuditEvent objects, ordered by most recent first |
Example
events = await limiter.get_audit_events("proj-1") for event in events: print(f"{event.timestamp}: {event.action} by {event.principal}")
get_usage_snapshots
async
¶
get_usage_snapshots(entity_id=None, resource=None, window_type=None, start_time=None, end_time=None, limit=100, next_key=None)
Query usage snapshots for historical consumption data.
Usage snapshots are created by the aggregator Lambda from DynamoDB stream events. They track token consumption per entity/resource within time windows (hourly, daily).
Supports two query modes: 1. Entity-scoped: Provide entity_id (optionally with resource filter) 2. Resource-scoped: Provide resource to query across all entities
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str | None
|
Entity to query (uses primary key) |
None
|
resource
|
str | None
|
Resource name filter (required if entity_id is None) |
None
|
window_type
|
str | None
|
Filter by window type ("hourly", "daily") |
None
|
start_time
|
datetime | None
|
Filter snapshots >= this timestamp |
None
|
end_time
|
datetime | None
|
Filter snapshots <= this timestamp |
None
|
limit
|
int
|
Maximum items to fetch from DynamoDB per page (default: 100) |
100
|
next_key
|
dict[str, Any] | None
|
Pagination cursor from previous call |
None
|
Returns:
| Type | Description |
|---|---|
tuple[list[UsageSnapshot], dict[str, Any] | None]
|
Tuple of (snapshots, next_key). next_key is None if no more results. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither entity_id nor resource is provided |
Note
The limit parameter controls the DynamoDB query batch size.
Client-side filters (window_type, start_time, end_time) are applied
after fetching, so the returned count may be less than limit.
Use next_key to paginate through all matching results.
Example
Get hourly snapshots for an entity¶
snapshots, cursor = await limiter.get_usage_snapshots( entity_id="user-123", resource="gpt-4", window_type="hourly", start_time=datetime(2024, 1, 1), end_time=datetime(2024, 1, 31), ) for snap in snapshots: print(f"{snap.window_start}: {snap.counters}")
Paginate through results¶
while cursor: more, cursor = await limiter.get_usage_snapshots( entity_id="user-123", next_key=cursor, )
get_usage_summary
async
¶
Get aggregated usage summary across multiple snapshots.
Fetches all matching snapshots and computes total and average consumption statistics. Useful for billing, reporting, and capacity planning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str | None
|
Entity to query |
None
|
resource
|
str | None
|
Resource name filter (required if entity_id is None) |
None
|
window_type
|
str | None
|
Filter by window type ("hourly", "daily") |
None
|
start_time
|
datetime | None
|
Filter snapshots >= this timestamp |
None
|
end_time
|
datetime | None
|
Filter snapshots <= this timestamp |
None
|
Returns:
| Type | Description |
|---|---|
UsageSummary
|
UsageSummary with total and average consumption per limit type |
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither entity_id nor resource is provided |
Example
summary = await limiter.get_usage_summary( entity_id="user-123", resource="gpt-4", window_type="hourly", start_time=datetime(2024, 1, 1), end_time=datetime(2024, 1, 31), ) print(f"Total tokens: {summary.total.get('tpm', 0)}") print(f"Average per hour: {summary.average.get('tpm', 0.0):.1f}") print(f"Snapshots: {summary.snapshot_count}")
acquire
async
¶
acquire(entity_id, resource, consume, limits=None, use_stored_limits=False, on_unavailable=None, *, also=None)
Acquire rate limit capacity.
Limits are resolved automatically from stored config using four-tier
hierarchy: Entity > Entity Default > Resource > System. Pass limits to override.
Cascade behavior is controlled by the entity's cascade flag, set at
entity creation time via create_entity(cascade=True). When enabled,
acquire() automatically consumes from both the entity and its parent.
Several resources can be debited in one acquire, all or none
(ADR-148): name the extra ones in also. Each resolves its own
limits, disabled flag, cascade policy, shards and quota grants
exactly as a single-resource acquire does. When any resource is
rejected, disabled or unavailable, every debit the acquire wrote is
refunded before the exception reaches the caller. The yielded lease
covers every resource: its own adjust(), consume(),
release() and consumed act on resource, and
lease.resource(name) reaches each of the others.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to acquire capacity for |
required |
resource
|
str
|
Resource being accessed (e.g., "gpt-4") |
required |
consume
|
dict[str, int]
|
Amounts to consume by limit name, each >= 0 (return
capacity with |
required |
limits
|
list[Limit] | None
|
Override stored config with explicit limits (optional).
Cannot be combined with |
None
|
use_stored_limits
|
bool
|
DEPRECATED - limits are now always resolved from stored config. This parameter will be removed in v1.0. |
False
|
on_unavailable
|
OnUnavailable | None
|
Override default on_unavailable behavior |
None
|
also
|
dict[str, dict[str, int]] | None
|
Further resources to debit in the same acquire, mapping each
resource name to its amounts by limit name (optional). At most
|
None
|
Yields:
| Type | Description |
|---|---|
AsyncIterator[Lease]
|
Lease for managing additional consumption |
Raises:
| Type | Description |
|---|---|
RateLimitExceeded
|
If any limit would be exceeded. With |
ResourceDisabled
|
If the resource — or, with |
RateLimiterUnavailable
|
If DynamoDB unavailable and BLOCK |
ValidationError
|
If no limits configured at any level, an amount
in |
VersionMismatchError
|
If |
check_availability
async
¶
Check available capacity and wait time in a single read (issue #472).
Answers both "how much is left?" and "how long until I can proceed?" for every resolved limit, from one config resolution and one bucket read taken at one instant.
Why one call rather than two. Calling :meth:available and
:meth:time_until_available back to back is two reads at two instants,
and they can disagree: tokens refill in between, and each discovers the
entity's shards separately. A display built from the pair can render
"0 remaining, available now" or "47 remaining, wait 0s". Both are lies
a user acts on. Everything on the returned :class:Availability is
derived from a single snapshot stamped with checked_at_ms, so the
numbers cannot contradict each other.
Limits are resolved using four-tier hierarchy: Entity > Entity Default >
Resource > System. If no stored limits found, falls back to the limits
parameter.
Consumes nothing and writes nothing. This is a read; it is not a
pre-flight check for :meth:acquire. Deciding with it and then calling
acquire() is TOCTOU and costs an extra read — acquire() already
answers "may I proceed, and if not when" in 1 WCU, with no read even on
a fast rejection, via RateLimitExceeded.retry_after_seconds. Use
this when the answer is displayed rather than acted on.
Sharding. A sharded entity's balance is spread across its shards
(GHSA-76rv), so this sums every shard, exactly as :meth:available
and get_resource_capacity() do, and computes the wait against the
summed refill rate. The reported limit is therefore the undivided
configured limit, not one shard's share — unlike the per-shard
statuses in RateLimitExceeded (#475), which describe the one shard
a rejection happened on. Known limitation, inherited from #475: a
single request larger than capacity // shard_count is
unadmittable on every shard even while the entity is under its
configured limit, so acquire() can reject an amount this call
reports as available.
Schedules. Everything reported is the value in force at
checked_at_ms, not the stored base (#222 §7): the ceiling comes
from effective_params, so a scale: 0.5 window reports 500 and
not 1000, and the wait walks forward across boundaries instead of
dividing by the rate that happens to apply right now. A limit with a
reset_schedule reports the wait to its next edge — for a daily
quota, whose refill_amount is 0 by ADR-137, that is the only finite
answer there is. A bucket that crossed a reset edge and has not been
written to since reports the balance the next acquire() will
restore, per shard, rather than the burnt one still on disk.
Cost: 1 GSI3 query (KEYS_ONLY) + 1 BatchGetItem, plus config
resolution (free on a cache hit), regardless of limit count or shard
count. A limit carrying a reset_schedule adds one backwards cron
scan per shard and the forward walk adds up to eight per exceeded
limit; an unscheduled limit adds neither.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to check |
required |
resource
|
str
|
Resource to check |
required |
needed
|
dict[str, int] | None
|
Required amounts by limit name. Omit to ask only about
current availability ( |
None
|
limits
|
list[Limit] | None
|
Override limits (optional, falls back to stored config) |
None
|
Returns:
| Type | Description |
|---|---|
Availability
|
Availability carrying one :class: |
Availability
|
plus the derived aggregate verdict and countdown. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If no limits found at any level and no override provided |
available
async
¶
Check available capacity without consuming.
Limits are resolved using four-tier hierarchy: Entity > Entity Default > Resource > System.
If no stored limits found, falls back to the limits parameter.
Sums every shard of the entity (GHSA-76rv). Can return negative values if the bucket is in debt.
Thin wrapper over :meth:check_availability, which returns this and
the wait time together from one read at one instant. Prefer it when
you want both — asking here and there is two snapshots that can
disagree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to check |
required |
resource
|
str
|
Resource to check |
required |
limits
|
list[Limit] | None
|
Override limits (optional, falls back to stored config) |
None
|
use_stored_limits
|
bool
|
DEPRECATED - limits are now always resolved from stored config. This parameter will be removed in v1.0. |
False
|
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
Dict mapping limit_name -> available tokens |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If no limits found at any level and no override provided |
time_until_available
async
¶
Calculate seconds until requested capacity is available.
Limits are resolved using four-tier hierarchy: Entity > Entity Default > Resource > System.
If no stored limits found, falls back to the limits parameter.
The estimate is computed against the entity's total across every shard
and the summed refill rate (GHSA-76rv), matching :meth:available.
Thin wrapper over :meth:check_availability, which returns this and
the available capacity together from one read at one instant. Prefer
it when you want both, or when you want the wait per limit rather than
the slowest one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to check |
required |
resource
|
str
|
Resource to check |
required |
needed
|
dict[str, int]
|
Required amounts by limit name |
required |
limits
|
list[Limit] | None
|
Override limits (optional, falls back to stored config) |
None
|
use_stored_limits
|
bool
|
DEPRECATED - limits are now always resolved from stored config. This parameter will be removed in v1.0. |
False
|
Returns:
| Type | Description |
|---|---|
float
|
Seconds until available (0.0 if already available) |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If no limits found at any level and no override provided |
set_limits
async
¶
Store limit configs for an entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to set limits for |
required |
limits
|
list[Limit]
|
Limits to store |
required |
resource
|
str
|
Resource these limits apply to (or default) |
DEFAULT_RESOURCE
|
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
get_limits
async
¶
Get stored limit configs for an entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to get limits for |
required |
resource
|
str
|
Resource to get limits for |
DEFAULT_RESOURCE
|
Returns:
| Type | Description |
|---|---|
list[Limit]
|
List of stored limits (empty if none) |
delete_limits
async
¶
Delete stored limit configs for an entity.
Reconciles existing buckets to fallback config (resource/system defaults) by syncing limit fields, setting TTL, and removing stale limit attributes (issue #327).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to delete limits for |
required |
resource
|
str
|
Resource to delete limits for |
DEFAULT_RESOURCE
|
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
list_entities_with_custom_limits
async
¶
List all entities that have custom limit configurations.
Uses GSI3 sparse index for efficient queries. Only entities with custom limits for the specified resource are returned.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource to filter by. |
required |
limit
|
int | None
|
Maximum number of entities to return. None for all. |
None
|
cursor
|
str | None
|
Pagination cursor from previous call. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[list[str], str | None]
|
Tuple of (entity_ids, next_cursor). next_cursor is None if no more results. |
Example
Get all entities with custom limits for gpt-4¶
entities, cursor = await limiter.list_entities_with_custom_limits("gpt-4") for entity_id in entities: print(entity_id)
Paginate through results¶
while cursor: more, cursor = await limiter.list_entities_with_custom_limits( "gpt-4", cursor=cursor ) entities.extend(more)
list_resources_with_entity_configs
async
¶
List all resources that have entity-level custom limit configurations.
Uses the entity config resources registry for efficient O(1) lookup.
Returns:
| Type | Description |
|---|---|
list[str]
|
Sorted list of resource names with at least one entity having custom limits |
Example
resources = await limiter.list_resources_with_entity_configs() for resource in resources: entities, _ = await limiter.list_entities_with_custom_limits(resource) print(f"{resource}: {len(entities)} entities with custom limits")
set_resource_defaults
async
¶
Store default limit configs for a resource.
Resource defaults override system defaults for the specified resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
limits
|
list[Limit]
|
Limits to store |
required |
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
get_resource_defaults
async
¶
Get stored default limit configs for a resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
Returns:
| Type | Description |
|---|---|
list[Limit]
|
List of stored limits (empty if none) |
delete_resource_defaults
async
¶
Delete stored default limit configs for a resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
list_resources_with_defaults
async
¶
List all resources that have default limit configs.
set_system_defaults
async
¶
Store system-wide default limits and config.
System defaults apply to ALL resources unless overridden at resource or entity level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limits
|
list[Limit]
|
Limits to store (apply globally to all resources) |
required |
on_unavailable
|
OnUnavailable | None
|
Behavior when DynamoDB unavailable (optional) |
None
|
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
get_system_defaults
async
¶
Get system-wide default limits and config.
Returns:
| Type | Description |
|---|---|
tuple[list[Limit], OnUnavailable | None]
|
Tuple of (limits, on_unavailable). on_unavailable may be None if not set. |
delete_system_defaults
async
¶
Delete all system-wide default limits and config.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
get_resource_capacity
async
¶
Get aggregated capacity for a resource across all entities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource to query |
required |
limit_name
|
str
|
Limit name to query |
required |
parents_only
|
bool
|
If True, only include parent entities |
False
|
Returns:
| Type | Description |
|---|---|
ResourceCapacity
|
ResourceCapacity with aggregated data |
SyncRateLimiter¶
SyncRateLimiter
¶
SyncRateLimiter(repository=None, name=None, region=None, endpoint_url=None, stack_options=None, on_unavailable=_UNSET, auto_update=_UNSET, bucket_ttl_refill_multiplier=_UNSET, speculative_writes=True)
Async rate limiter backed by DynamoDB.
Implements token bucket algorithm with support for: - Multiple limits per entity/resource - Two-level hierarchy (parent/child entities) - Cascade mode (consume from entity + parent) - Stored limit configs - Usage analytics
Example (new API - preferred): from zae_limiter import SyncRateLimiter, SyncRepository, StackOptions
repo = SyncRepository(
name="my-app",
region="us-east-1",
stack_options=StackOptions(),
)
limiter = SyncRateLimiter(repository=repo)
Example (old API - deprecated): limiter = SyncRateLimiter( name="my-app", region="us-east-1", stack_options=StackOptions(), )
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repository
|
SyncRepositoryProtocol | None
|
SyncRepository instance (new API, preferred). Pass a SyncRepository or any SyncRepositoryProtocol implementation. |
None
|
name
|
str | None
|
DEPRECATED. Use |
None
|
region
|
str | None
|
DEPRECATED. Use |
None
|
endpoint_url
|
str | None
|
DEPRECATED. Use |
None
|
stack_options
|
StackOptions | None
|
DEPRECATED. Use |
None
|
on_unavailable
|
OnUnavailable | Any
|
DEPRECATED. Use |
_UNSET
|
auto_update
|
bool | Any
|
DEPRECATED. Use |
_UNSET
|
bucket_ttl_refill_multiplier
|
int | Any
|
DEPRECATED. Use
|
_UNSET
|
speculative_writes
|
bool
|
Enable speculative UpdateItem fast path. When True, acquire() tries a speculative write first, falling back to the full read-write path only when needed. |
True
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If both repository and name/region/endpoint_url/stack_options are provided. |
list_deployed
classmethod
¶
List all deployed rate limiter instances in a region.
This is a class method that discovers existing deployments without
requiring an initialized SyncRateLimiter instance. It queries CloudFormation
for stacks tagged with ManagedBy=zae-limiter.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
region
|
str | None
|
AWS region (default: use boto3 defaults) |
None
|
endpoint_url
|
str | None
|
CloudFormation endpoint (for LocalStack) |
None
|
Returns:
| Type | Description |
|---|---|
list[LimiterInfo]
|
List of LimiterInfo objects describing deployed instances. |
list[LimiterInfo]
|
Sorted by user-friendly name. Excludes deleted stacks. |
Example
Discover all limiters in us-east-1¶
limiters = SyncRateLimiter.list_deployed(region="us-east-1") for limiter in limiters: if limiter.is_healthy: print(f"✓ {limiter.user_name}: {limiter.version}") elif limiter.is_failed: print(f"✗ {limiter.user_name}: {limiter.stack_status}")
Raises:
| Type | Description |
|---|---|
ClientError
|
If CloudFormation API call fails |
is_available
¶
Check if the rate limiter backend (DynamoDB) is reachable.
Performs a lightweight health check without requiring initialization. This method never raises exceptions - it returns False on any error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float
|
Maximum time in seconds to wait for response (default: 1.0) |
1.0
|
Returns:
| Type | Description |
|---|---|
bool
|
True if DynamoDB table is reachable, False otherwise. |
Example
limiter = SyncRateLimiter(name="my-app", region="us-east-1") if limiter.is_available(): async with limiter.acquire(...) as lease: ... else: # Handle degraded mode pass
create_entity
¶
Create a new entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Unique identifier for the entity |
required |
name
|
str | None
|
Human-readable name (defaults to entity_id) |
None
|
parent_id
|
str | None
|
Parent entity ID (None for root/project entities) |
None
|
cascade
|
bool
|
If True, acquire() will also consume from parent entity |
False
|
metadata
|
dict[str, str] | None
|
Additional metadata to store |
None
|
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
Returns:
| Type | Description |
|---|---|
Entity
|
The created Entity |
Raises:
| Type | Description |
|---|---|
EntityExistsError
|
If entity already exists |
delete_entity
¶
Delete an entity and all its related data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
ID of the entity to delete |
required |
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
Raises:
| Type | Description |
|---|---|
RateLimiterUnavailable
|
DynamoDB kept some deletes unprocessed
after the retries. What was deleted is audited, and calling
|
set_parent
¶
Move an entity to a new parent, or to no parent (ADR-150).
Takes effect on the next acquire in every process: the entity's bucket
items are restamped, and a process whose cache still names the old
parent learns the new one from the item on its next call, refunding
the old parent. Consumption already charged to the old parent, including
by leases open across the move, stays there. Whether the entity debits
the new parent follows its cascade policy (ADR-146) or its own
cascade flag, which a move does not change.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to move. |
required |
parent_id
|
str | None
|
The new parent, which must exist, or None for no parent. |
required |
principal
|
str | None
|
Caller identity for audit logging (optional). |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items restamped. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
The move would make the entity its own ancestor. |
EntityNotFoundError
|
The entity or the new parent does not exist. |
VersionMismatchError
|
The stack's Lambdas predate parent moves. |
FanoutIncomplete
|
The move landed but restamping stopped part-way; repeating the call finishes it. |
get_audit_events
¶
Get audit events for an entity.
Retrieves security audit events logged for administrative operations on the specified entity, ordered by most recent first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
ID of the entity to query |
required |
limit
|
int
|
Maximum number of events to return (default: 100) |
100
|
start_event_id
|
str | None
|
Event ID to start after (for pagination) |
None
|
Returns:
| Type | Description |
|---|---|
list[AuditEvent]
|
List of AuditEvent objects, ordered by most recent first |
Example
events = limiter.get_audit_events("proj-1") for event in events: print(f"{event.timestamp}: {event.action} by {event.principal}")
get_usage_snapshots
¶
get_usage_snapshots(entity_id=None, resource=None, window_type=None, start_time=None, end_time=None, limit=100, next_key=None)
Query usage snapshots for historical consumption data.
Usage snapshots are created by the aggregator Lambda from DynamoDB stream events. They track token consumption per entity/resource within time windows (hourly, daily).
Supports two query modes: 1. Entity-scoped: Provide entity_id (optionally with resource filter) 2. Resource-scoped: Provide resource to query across all entities
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str | None
|
Entity to query (uses primary key) |
None
|
resource
|
str | None
|
Resource name filter (required if entity_id is None) |
None
|
window_type
|
str | None
|
Filter by window type ("hourly", "daily") |
None
|
start_time
|
datetime | None
|
Filter snapshots >= this timestamp |
None
|
end_time
|
datetime | None
|
Filter snapshots <= this timestamp |
None
|
limit
|
int
|
Maximum items to fetch from DynamoDB per page (default: 100) |
100
|
next_key
|
dict[str, Any] | None
|
Pagination cursor from previous call |
None
|
Returns:
| Type | Description |
|---|---|
tuple[list[UsageSnapshot], dict[str, Any] | None]
|
Tuple of (snapshots, next_key). next_key is None if no more results. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither entity_id nor resource is provided |
Note
The limit parameter controls the DynamoDB query batch size.
Client-side filters (window_type, start_time, end_time) are applied
after fetching, so the returned count may be less than limit.
Use next_key to paginate through all matching results.
Example
Get hourly snapshots for an entity¶
snapshots, cursor = limiter.get_usage_snapshots( entity_id="user-123", resource="gpt-4", window_type="hourly", start_time=datetime(2024, 1, 1), end_time=datetime(2024, 1, 31), ) for snap in snapshots: print(f"{snap.window_start}: {snap.counters}")
Paginate through results¶
while cursor: more, cursor = limiter.get_usage_snapshots( entity_id="user-123", next_key=cursor, )
get_usage_summary
¶
Get aggregated usage summary across multiple snapshots.
Fetches all matching snapshots and computes total and average consumption statistics. Useful for billing, reporting, and capacity planning.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str | None
|
Entity to query |
None
|
resource
|
str | None
|
Resource name filter (required if entity_id is None) |
None
|
window_type
|
str | None
|
Filter by window type ("hourly", "daily") |
None
|
start_time
|
datetime | None
|
Filter snapshots >= this timestamp |
None
|
end_time
|
datetime | None
|
Filter snapshots <= this timestamp |
None
|
Returns:
| Type | Description |
|---|---|
UsageSummary
|
UsageSummary with total and average consumption per limit type |
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither entity_id nor resource is provided |
Example
summary = limiter.get_usage_summary( entity_id="user-123", resource="gpt-4", window_type="hourly", start_time=datetime(2024, 1, 1), end_time=datetime(2024, 1, 31), ) print(f"Total tokens: {summary.total.get('tpm', 0)}") print(f"Average per hour: {summary.average.get('tpm', 0.0):.1f}") print(f"Snapshots: {summary.snapshot_count}")
acquire
¶
acquire(entity_id, resource, consume, limits=None, use_stored_limits=False, on_unavailable=None, *, also=None)
Acquire rate limit capacity.
Limits are resolved automatically from stored config using four-tier
hierarchy: Entity > Entity Default > Resource > System. Pass limits to override.
Cascade behavior is controlled by the entity's cascade flag, set at
entity creation time via create_entity(cascade=True). When enabled,
acquire() automatically consumes from both the entity and its parent.
Several resources can be debited in one acquire, all or none
(ADR-148): name the extra ones in also. Each resolves its own
limits, disabled flag, cascade policy, shards and quota grants
exactly as a single-resource acquire does. When any resource is
rejected, disabled or unavailable, every debit the acquire wrote is
refunded before the exception reaches the caller. The yielded lease
covers every resource: its own adjust(), consume(),
release() and consumed act on resource, and
lease.resource(name) reaches each of the others.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to acquire capacity for |
required |
resource
|
str
|
Resource being accessed (e.g., "gpt-4") |
required |
consume
|
dict[str, int]
|
Amounts to consume by limit name, each >= 0 (return
capacity with |
required |
limits
|
list[Limit] | None
|
Override stored config with explicit limits (optional).
Cannot be combined with |
None
|
use_stored_limits
|
bool
|
DEPRECATED - limits are now always resolved from stored config. This parameter will be removed in v1.0. |
False
|
on_unavailable
|
OnUnavailable | None
|
Override default on_unavailable behavior |
None
|
also
|
dict[str, dict[str, int]] | None
|
Further resources to debit in the same acquire, mapping each
resource name to its amounts by limit name (optional). At most
|
None
|
Yields:
| Type | Description |
|---|---|
SyncLease
|
SyncLease for managing additional consumption |
Raises:
| Type | Description |
|---|---|
RateLimitExceeded
|
If any limit would be exceeded. With |
ResourceDisabled
|
If the resource — or, with |
RateLimiterUnavailable
|
If DynamoDB unavailable and BLOCK |
ValidationError
|
If no limits configured at any level, an amount
in |
VersionMismatchError
|
If |
check_availability
¶
Check available capacity and wait time in a single read (issue #472).
Answers both "how much is left?" and "how long until I can proceed?" for every resolved limit, from one config resolution and one bucket read taken at one instant.
Why one call rather than two. Calling :meth:available and
:meth:time_until_available back to back is two reads at two instants,
and they can disagree: tokens refill in between, and each discovers the
entity's shards separately. A display built from the pair can render
"0 remaining, available now" or "47 remaining, wait 0s". Both are lies
a user acts on. Everything on the returned :class:Availability is
derived from a single snapshot stamped with checked_at_ms, so the
numbers cannot contradict each other.
Limits are resolved using four-tier hierarchy: Entity > Entity Default >
Resource > System. If no stored limits found, falls back to the limits
parameter.
Consumes nothing and writes nothing. This is a read; it is not a
pre-flight check for :meth:acquire. Deciding with it and then calling
acquire() is TOCTOU and costs an extra read — acquire() already
answers "may I proceed, and if not when" in 1 WCU, with no read even on
a fast rejection, via RateLimitExceeded.retry_after_seconds. Use
this when the answer is displayed rather than acted on.
Sharding. A sharded entity's balance is spread across its shards
(GHSA-76rv), so this sums every shard, exactly as :meth:available
and get_resource_capacity() do, and computes the wait against the
summed refill rate. The reported limit is therefore the undivided
configured limit, not one shard's share — unlike the per-shard
statuses in RateLimitExceeded (#475), which describe the one shard
a rejection happened on. Known limitation, inherited from #475: a
single request larger than capacity // shard_count is
unadmittable on every shard even while the entity is under its
configured limit, so acquire() can reject an amount this call
reports as available.
Schedules. Everything reported is the value in force at
checked_at_ms, not the stored base (#222 §7): the ceiling comes
from effective_params, so a scale: 0.5 window reports 500 and
not 1000, and the wait walks forward across boundaries instead of
dividing by the rate that happens to apply right now. A limit with a
reset_schedule reports the wait to its next edge — for a daily
quota, whose refill_amount is 0 by ADR-137, that is the only finite
answer there is. A bucket that crossed a reset edge and has not been
written to since reports the balance the next acquire() will
restore, per shard, rather than the burnt one still on disk.
Cost: 1 GSI3 query (KEYS_ONLY) + 1 BatchGetItem, plus config
resolution (free on a cache hit), regardless of limit count or shard
count. A limit carrying a reset_schedule adds one backwards cron
scan per shard and the forward walk adds up to eight per exceeded
limit; an unscheduled limit adds neither.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to check |
required |
resource
|
str
|
Resource to check |
required |
needed
|
dict[str, int] | None
|
Required amounts by limit name. Omit to ask only about
current availability ( |
None
|
limits
|
list[Limit] | None
|
Override limits (optional, falls back to stored config) |
None
|
Returns:
| Type | Description |
|---|---|
Availability
|
Availability carrying one :class: |
Availability
|
plus the derived aggregate verdict and countdown. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If no limits found at any level and no override provided |
available
¶
Check available capacity without consuming.
Limits are resolved using four-tier hierarchy: Entity > Entity Default > Resource > System.
If no stored limits found, falls back to the limits parameter.
Sums every shard of the entity (GHSA-76rv). Can return negative values if the bucket is in debt.
Thin wrapper over :meth:check_availability, which returns this and
the wait time together from one read at one instant. Prefer it when
you want both — asking here and there is two snapshots that can
disagree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to check |
required |
resource
|
str
|
Resource to check |
required |
limits
|
list[Limit] | None
|
Override limits (optional, falls back to stored config) |
None
|
use_stored_limits
|
bool
|
DEPRECATED - limits are now always resolved from stored config. This parameter will be removed in v1.0. |
False
|
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
Dict mapping limit_name -> available tokens |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If no limits found at any level and no override provided |
time_until_available
¶
Calculate seconds until requested capacity is available.
Limits are resolved using four-tier hierarchy: Entity > Entity Default > Resource > System.
If no stored limits found, falls back to the limits parameter.
The estimate is computed against the entity's total across every shard
and the summed refill rate (GHSA-76rv), matching :meth:available.
Thin wrapper over :meth:check_availability, which returns this and
the available capacity together from one read at one instant. Prefer
it when you want both, or when you want the wait per limit rather than
the slowest one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to check |
required |
resource
|
str
|
Resource to check |
required |
needed
|
dict[str, int]
|
Required amounts by limit name |
required |
limits
|
list[Limit] | None
|
Override limits (optional, falls back to stored config) |
None
|
use_stored_limits
|
bool
|
DEPRECATED - limits are now always resolved from stored config. This parameter will be removed in v1.0. |
False
|
Returns:
| Type | Description |
|---|---|
float
|
Seconds until available (0.0 if already available) |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If no limits found at any level and no override provided |
set_limits
¶
Store limit configs for an entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to set limits for |
required |
limits
|
list[Limit]
|
Limits to store |
required |
resource
|
str
|
Resource these limits apply to (or default) |
DEFAULT_RESOURCE
|
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
get_limits
¶
Get stored limit configs for an entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to get limits for |
required |
resource
|
str
|
Resource to get limits for |
DEFAULT_RESOURCE
|
Returns:
| Type | Description |
|---|---|
list[Limit]
|
List of stored limits (empty if none) |
delete_limits
¶
Delete stored limit configs for an entity.
Reconciles existing buckets to fallback config (resource/system defaults) by syncing limit fields, setting TTL, and removing stale limit attributes (issue #327).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to delete limits for |
required |
resource
|
str
|
Resource to delete limits for |
DEFAULT_RESOURCE
|
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
list_entities_with_custom_limits
¶
List all entities that have custom limit configurations.
Uses GSI3 sparse index for efficient queries. Only entities with custom limits for the specified resource are returned.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource to filter by. |
required |
limit
|
int | None
|
Maximum number of entities to return. None for all. |
None
|
cursor
|
str | None
|
Pagination cursor from previous call. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[list[str], str | None]
|
Tuple of (entity_ids, next_cursor). next_cursor is None if no more results. |
Example
Get all entities with custom limits for gpt-4¶
entities, cursor = limiter.list_entities_with_custom_limits("gpt-4") for entity_id in entities: print(entity_id)
Paginate through results¶
while cursor: more, cursor = limiter.list_entities_with_custom_limits( "gpt-4", cursor=cursor ) entities.extend(more)
list_resources_with_entity_configs
¶
List all resources that have entity-level custom limit configurations.
Uses the entity config resources registry for efficient O(1) lookup.
Returns:
| Type | Description |
|---|---|
list[str]
|
Sorted list of resource names with at least one entity having custom limits |
Example
resources = limiter.list_resources_with_entity_configs() for resource in resources: entities, _ = limiter.list_entities_with_custom_limits(resource) print(f"{resource}: {len(entities)} entities with custom limits")
set_resource_defaults
¶
Store default limit configs for a resource.
Resource defaults override system defaults for the specified resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
limits
|
list[Limit]
|
Limits to store |
required |
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
get_resource_defaults
¶
Get stored default limit configs for a resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
Returns:
| Type | Description |
|---|---|
list[Limit]
|
List of stored limits (empty if none) |
delete_resource_defaults
¶
Delete stored default limit configs for a resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
list_resources_with_defaults
¶
List all resources that have default limit configs.
set_system_defaults
¶
Store system-wide default limits and config.
System defaults apply to ALL resources unless overridden at resource or entity level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limits
|
list[Limit]
|
Limits to store (apply globally to all resources) |
required |
on_unavailable
|
OnUnavailable | None
|
Behavior when DynamoDB unavailable (optional) |
None
|
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
get_system_defaults
¶
Get system-wide default limits and config.
Returns:
| Type | Description |
|---|---|
tuple[list[Limit], OnUnavailable | None]
|
Tuple of (limits, on_unavailable). on_unavailable may be None if not set. |
delete_system_defaults
¶
Delete all system-wide default limits and config.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
principal
|
str | None
|
Caller identity for audit logging (optional) |
None
|
get_resource_capacity
¶
Get aggregated capacity for a resource across all entities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource to query |
required |
limit_name
|
str
|
Limit name to query |
required |
parents_only
|
bool
|
If True, only include parent entities |
False
|
Returns:
| Type | Description |
|---|---|
ResourceCapacity
|
ResourceCapacity with aggregated data |
OnUnavailable¶
OnUnavailable
¶
Bases: Enum
Behavior when DynamoDB is unavailable.
Lease (Async)¶
The object yielded by RateLimiter.acquire(). Only limits declared in
acquire(consume=...) are adjustable through it; see
Adjusting Consumption.
A lease from acquire(..., also={...}) covers several resources: its own
methods act on the primary resource and resource(name) reaches the others; see
Several Resources in One Acquire.
Lease
dataclass
¶
Lease(repository, entries=list(), _committed=False, _rolled_back=False, _initial_committed=False, degraded=False, _unknown_keys=frozenset(), _carriers=list(), _cache_mark=None, _resources=(), _unknown_keys_by_resource=dict(), compute_images=True)
Manages an active rate limit acquisition.
Tracks consumption across multiple entities/limits and handles rollback on exception.
degraded
class-attribute
instance-attribute
¶
Whether this is the no-op lease yielded under on_unavailable=ALLOW.
True only when the backend was unreachable and the limiter degraded
to allowing the request (Issue #455). Such a lease has no entries, and
adjust(), consume() and release() are silent no-ops on it —
the declared-scope check that normally reports keys outside consume
is skipped, so an outage never turns into a warning storm. Set
explicitly where that lease is built, never inferred from an empty
entries: a real lease with nothing declared is not degraded.
resources
property
¶
The resources this lease covers, the primary one first (ADR-148).
A single-resource acquire covers one resource; acquire(...,
also={...}) covers the primary resource and every resource in
also, in that order. Empty on the degraded lease yielded under
on_unavailable=ALLOW, which covers nothing.
consumed
property
¶
Total consumed amounts by limit name (declared limits only).
Scoped to the primary resource; use lease.resource(name).consumed
for another resource of a multi-resource lease (ADR-148).
resource
¶
A handle on one resource of this lease (ADR-148).
lease.adjust(), consume(), release() and consumed act
on the primary resource only — the resource argument of
acquire() — so a single-resource lease behaves exactly as it
always has. Every other resource of a multi-resource lease is reached
through its handle::
async with limiter.acquire(
"user-1", "search", consume={"rpm": 1, "units": est},
also={"budget": {"weekly": est}},
) as lease:
...
await lease.adjust(units=actual - est)
await lease.resource("budget").adjust(weekly=actual - est)
Adjustments made through a handle are written with the rest of the lease's adjustments when the context exits, one write per bucket. Two resources may declare limits of the same name; each handle sees only its own.
On the degraded lease yielded under on_unavailable=ALLOW every
name returns a no-op handle.
Raises:
| Type | Description |
|---|---|
ValidationError
|
If |
adjust
async
¶
Adjust consumption by delta (positive or negative).
Never raises - allows bucket to go negative. Use for post-hoc reconciliation (e.g., LLM token counts).
A negative delta is a credit, an ADD, and a reset edge or a refill
to capacity since the debit can make it land above the shard's
ceiling. So the credit is conditioned on staying within the ceiling
and, when it would not, re-issued with vu = 0 in the same write
(#721): the next acquire takes the slow path, which clamps every limit,
and the aggregator does not reopen the fast path meanwhile (#679,
681). No process can spend the excess in between. Not covered: a¶
limit whose ceiling the lease could not compute (logged, the credit is then unconditional) and an adjustment commit that fails partway (#682).
Only limits declared in acquire(consume=...) can be adjusted;
other keys are reported (Issue #455) and ignored.
On a multi-resource lease this acts on the primary resource only;
reach the others through :meth:resource (ADR-148).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**amounts
|
int
|
Mapping of limit_name -> delta (positive = consume more) |
{}
|
consume
async
¶
Consume additional capacity from the buckets.
Raises RateLimitExceeded if any bucket has insufficient capacity.
Only limits declared in acquire(consume=...) can be consumed;
other keys are reported (Issue #455) and ignored.
On a multi-resource lease this acts on the primary resource only;
reach the others through :meth:resource (ADR-148).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**amounts
|
int
|
Mapping of limit_name -> amount to consume |
{}
|
release
async
¶
Return unused capacity to bucket.
Equivalent to adjust() with every amount negated, so the same
ceiling rule applies: the stored balance can land above the shard's
ceiling, and the excess is trimmed before the fast path can spend it
(#679, #681). Returning more than was consumed is therefore not a way
to grant extra capacity.
Only limits declared in acquire(consume=...) can be released;
other keys are reported (Issue #455) and ignored.
On a multi-resource lease this acts on the primary resource only;
reach the others through :meth:resource (ADR-148).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**amounts
|
int
|
Mapping of limit_name -> amount to return |
{}
|
SyncLease¶
SyncLease
dataclass
¶
SyncLease(repository, entries=list(), _committed=False, _rolled_back=False, _initial_committed=False, degraded=False, _unknown_keys=frozenset(), _carriers=list(), _cache_mark=None, _resources=(), _unknown_keys_by_resource=dict(), compute_images=True)
Manages an active rate limit acquisition.
Tracks consumption across multiple entities/limits and handles rollback on exception.
degraded
class-attribute
instance-attribute
¶
Whether this is the no-op lease yielded under on_unavailable=ALLOW.
True only when the backend was unreachable and the limiter degraded
to allowing the request (Issue #455). Such a lease has no entries, and
adjust(), consume() and release() are silent no-ops on it —
the declared-scope check that normally reports keys outside consume
is skipped, so an outage never turns into a warning storm. Set
explicitly where that lease is built, never inferred from an empty
entries: a real lease with nothing declared is not degraded.
consumed
property
¶
Total consumed amounts by limit name (declared limits only).
Scoped to the primary resource; use lease.resource(name).consumed
for another resource of a multi-resource lease (ADR-148).
resources
property
¶
The resources this lease covers, the primary one first (ADR-148).
A single-resource acquire covers one resource; acquire(...,
also={...}) covers the primary resource and every resource in
also, in that order. Empty on the degraded lease yielded under
on_unavailable=ALLOW, which covers nothing.
adjust
¶
Adjust consumption by delta (positive or negative).
Never raises - allows bucket to go negative. Use for post-hoc reconciliation (e.g., LLM token counts).
A negative delta is a credit, an ADD, and a reset edge or a refill
to capacity since the debit can make it land above the shard's
ceiling. So the credit is conditioned on staying within the ceiling
and, when it would not, re-issued with vu = 0 in the same write
(#721): the next acquire takes the slow path, which clamps every limit,
and the aggregator does not reopen the fast path meanwhile (#679,
681). No process can spend the excess in between. Not covered: a¶
limit whose ceiling the lease could not compute (logged, the credit is then unconditional) and an adjustment commit that fails partway (#682).
Only limits declared in acquire(consume=...) can be adjusted;
other keys are reported (Issue #455) and ignored.
On a multi-resource lease this acts on the primary resource only;
reach the others through :meth:resource (ADR-148).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**amounts
|
int
|
Mapping of limit_name -> delta (positive = consume more) |
{}
|
consume
¶
Consume additional capacity from the buckets.
Raises RateLimitExceeded if any bucket has insufficient capacity.
Only limits declared in acquire(consume=...) can be consumed;
other keys are reported (Issue #455) and ignored.
On a multi-resource lease this acts on the primary resource only;
reach the others through :meth:resource (ADR-148).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**amounts
|
int
|
Mapping of limit_name -> amount to consume |
{}
|
release
¶
Return unused capacity to bucket.
Equivalent to adjust() with every amount negated, so the same
ceiling rule applies: the stored balance can land above the shard's
ceiling, and the excess is trimmed before the fast path can spend it
(#679, #681). Returning more than was consumed is therefore not a way
to grant extra capacity.
Only limits declared in acquire(consume=...) can be released;
other keys are reported (Issue #455) and ignored.
On a multi-resource lease this acts on the primary resource only;
reach the others through :meth:resource (ADR-148).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**amounts
|
int
|
Mapping of limit_name -> amount to return |
{}
|
resource
¶
A handle on one resource of this lease (ADR-148).
lease.adjust(), consume(), release() and consumed act
on the primary resource only — the resource argument of
acquire() — so a single-resource lease behaves exactly as it
always has. Every other resource of a multi-resource lease is reached
through its handle::
async with limiter.acquire(
"user-1", "search", consume={"rpm": 1, "units": est},
also={"budget": {"weekly": est}},
) as lease:
...
lease.adjust(units=actual - est)
lease.resource("budget").adjust(weekly=actual - est)
Adjustments made through a handle are written with the rest of the lease's adjustments when the context exits, one write per bucket. Two resources may declare limits of the same name; each handle sees only its own.
On the degraded lease yielded under on_unavailable=ALLOW every
name returns a no-op handle.
Raises:
| Type | Description |
|---|---|
ValidationError
|
If |
LeaseResource (Async)¶
One resource of a multi-resource lease, returned by Lease.resource(name) (ADR-148).
LeaseResource
¶
One resource of a lease, reached through Lease.resource(name) (ADR-148).
adjust(), consume(), release() and consumed behave exactly
as the lease-level methods do, scoped to this resource's declared limits:
the ones named for it in acquire()'s consume (the primary
resource) or also (every other one). Nothing is written until the
lease's context exits, where every resource is reconciled together.
bypassed
property
¶
Whether the acquiring entity was admitted under bypass on this resource (#311).
overdrawn
property
¶
This resource's declared soft limits in debt on the shard written (#467).
consume
async
¶
Consume more of this resource; see :meth:Lease.consume.
Raises:
| Type | Description |
|---|---|
RateLimitExceeded
|
If any of this resource's limits cannot cover it. |
LeaseExpiredError
|
If the lease has already exited. |
adjust
async
¶
Adjust this resource's consumption by delta; see :meth:Lease.adjust.
Never raises for capacity: the bucket may go negative.
Raises:
| Type | Description |
|---|---|
LeaseExpiredError
|
If the lease has already exited. |
release
async
¶
Return unused capacity of this resource; see :meth:Lease.release.
Raises:
| Type | Description |
|---|---|
LeaseExpiredError
|
If the lease has already exited. |
SyncLeaseResource¶
SyncLeaseResource
¶
One resource of a lease, reached through SyncLease.resource(name) (ADR-148).
adjust(), consume(), release() and consumed behave exactly
as the lease-level methods do, scoped to this resource's declared limits:
the ones named for it in acquire()'s consume (the primary
resource) or also (every other one). Nothing is written until the
lease's context exits, where every resource is reconciled together.
bypassed
property
¶
Whether the acquiring entity was admitted under bypass on this resource (#311).
overdrawn
property
¶
This resource's declared soft limits in debt on the shard written (#467).
consume
¶
Consume more of this resource; see :meth:SyncLease.consume.
Raises:
| Type | Description |
|---|---|
RateLimitExceeded
|
If any of this resource's limits cannot cover it. |
LeaseExpiredError
|
If the lease has already exited. |
adjust
¶
Adjust this resource's consumption by delta; see :meth:SyncLease.adjust.
Never raises for capacity: the bucket may go negative.
Raises:
| Type | Description |
|---|---|
LeaseExpiredError
|
If the lease has already exited. |
release
¶
Return unused capacity of this resource; see :meth:SyncLease.release.
Raises:
| Type | Description |
|---|---|
LeaseExpiredError
|
If the lease has already exited. |