Skip to content

Repository

The Repository class owns all DynamoDB data access and infrastructure management. RateLimiter delegates data operations to the repository while owning business logic.

Preferred Usage Pattern

from zae_limiter import RateLimiter, Repository

# Open repository (auto-provisions if needed, recommended)
repo = await Repository.open()
limiter = RateLimiter(repository=repo)

# For explicit infrastructure provisioning, use builder:
repo = await Repository.builder().build()
limiter = RateLimiter(repository=repo)

# Infrastructure managed elsewhere (your own CloudFormation/Terraform/CDK):
repo = await Repository.connect("my-app")
limiter = RateLimiter(repository=repo)

See ADR-108 for the design rationale.

Choosing an Entry Point

Entry point Provisions infrastructure? Use when
Repository.open() Yes — deploys the stack, registers namespaces, updates the Lambda Application code, prototyping, LocalStack dev
Repository.builder().build() Yes — with custom options (permission boundaries, IAM naming, Lambda config) Enterprise deployments
Repository.connect() No — reads only, raises when anything is missing Infrastructure owned by your own CloudFormation, Terraform, or CDK, or an application role that may only acquire

An acquire-only role cannot set up a stack (v0.17.0)

Since v0.17.0 the acquire policies (AcquireOnlyPolicy, NamespaceAcquirePolicy) allow writes to rate-limit buckets only (ADR-152). open() writes more than that when there is something to set up: the first time it meets a stack or namespace (registering the namespace, writing the version record), and whenever the stack's Lambdas are older than the client (auto_update). Under an acquire-only role those writes fail with an access error.

  • Set up and upgrade with an operator role holding FullAccessPolicy / NamespaceFullAccessPolicy: zae-limiter deploy, zae-limiter upgrade, zae-limiter namespace register, or one open() run under that role.
  • Have acquire-only applications call connect(), which only reads. Once setup is done, open() also only reads, but it would try to update the Lambdas again after the next client upgrade.

Repository.connect()

connect() binds to infrastructure that already exists and never writes to it. It issues two reads — a namespace lookup and a version record read — and raises rather than repairing anything it finds missing:

Situation open() connect()
Table missing Deploys the stack InfrastructureNotFoundError
Namespace unregistered Registers it NamespaceNotFoundError
Version record missing Writes it InfrastructureNotFoundError
Lambda version behind client Updates the Lambda VersionMismatchError
from zae_limiter import RateLimiter, Repository

# Stack, table, and namespaces are all deployed by your own template
repo = await Repository.connect("my-app")
limiter = RateLimiter(repository=repo)

Register namespaces ahead of time with zae-limiter namespace register, or from a template, so connect() can resolve them. A SyncRepository.connect() counterpart is available with the same signature.

Repository

Repository

Repository(name, region=None, endpoint_url=None, stack_options=None, config_cache_ttl=60, *, rejection_cache_ttl=DEFAULT_REJECTION_CACHE_TTL, rejection_cache_size=DEFAULT_REJECTION_CACHE_SIZE, stamp_ttl=DEFAULT_STAMP_TTL_SECONDS, _skip_deprecation_warning=False)

Async DynamoDB repository for rate limiter data.

Handles all DynamoDB operations including entities, buckets, limit configs, and transactions.

Use :meth:open to open a repository (recommended), or :meth:builder for custom infrastructure options (permission boundaries, IAM config).

.. deprecated:: Direct construction via Repository(...) is deprecated and will be removed in v1.0.0. Use Repository.open(...) or Repository.builder().build() instead.

Parameters:

Name Type Description Default
name str

Resource identifier (e.g., "my-app"). Used as the CloudFormation stack_name and DynamoDB table_name.

required
region str | None

AWS region (e.g., "us-east-1").

None
endpoint_url str | None

Custom endpoint URL (e.g., LocalStack).

None
stack_options StackOptions | None

Configuration for CloudFormation infrastructure. Pass StackOptions to enable declarative infrastructure management.

None
config_cache_ttl int

TTL in seconds for config cache (default: 60, 0 to disable). Controls caching of resolved limit configs in resolve_limits().

60
rejection_cache_ttl float

Seconds a cached bucket state may be used to reject a request without a DynamoDB call (default: 1.0, 0 to disable). ADR-147.

DEFAULT_REJECTION_CACHE_TTL
rejection_cache_size int

Maximum cached bucket states (default: 10,000).

DEFAULT_REJECTION_CACHE_SIZE
stamp_ttl float

Seconds a bypass or soft stamp this repository writes stands after the config read that justified it (default: 60, at least 10). ADR-151 decision 8: a stamp a fan-out missed keeps its old meaning for at most this long, and every bypassed or soft bucket renews its stamp through one slow pass per shard per stamp_ttl.

DEFAULT_STAMP_TTL_SECONDS

Example::

# Most users (auto-provisions if needed)
repo = await Repository.open("my-app")

# Custom infrastructure options
repo = await Repository.builder().namespace("my-app").build()

namespace_name property

namespace_name

The human-readable namespace name.

namespace_id property

namespace_id

The opaque namespace ID used in DynamoDB keys.

capabilities property

capabilities

Declare which extended features this backend supports.

builder classmethod

builder()

Create a RepositoryBuilder for fluent configuration.

For most use cases, prefer :meth:open instead. Use builder() when you need custom infrastructure options (permission boundaries, Lambda config, IAM role naming).

Stack defaults mirror :meth:open: ZAEL_STACK env var or "zae-limiter", namespace "default".

Example

repo = await ( Repository.builder() .namespace("my-app") .lambda_memory(512) .build() )

open async classmethod

open(namespace=None, *, stack=None, region=None, endpoint_url=None, config_cache_ttl=60, auto_update=True, rejection_cache_ttl=DEFAULT_REJECTION_CACHE_TTL, rejection_cache_size=DEFAULT_REJECTION_CACHE_SIZE, stamp_ttl=DEFAULT_STAMP_TTL_SECONDS)

Open a repository, auto-provisioning infrastructure if needed.

This is the recommended entry point for most applications. Namespace is the primary parameter — stack name defaults to "zae-limiter" and is rarely needed.

Auto-provision behavior:

  • If the DynamoDB table doesn't exist, deploys a new stack with default options (aggregator enabled).
  • If the table exists but the namespace isn't registered, registers it.
  • The "default" namespace is always registered.
  • Version check and Lambda auto-update run on every call.

For custom infrastructure options (permission boundaries, Lambda config, IAM role naming), use Repository.builder() instead.

Stack resolution: stack arg → ZAEL_STACK env var → "zae-limiter".

Namespace resolution: namespace arg → ZAEL_NAMESPACE env var → "default".

Parameters:

Name Type Description Default
namespace str | None

Namespace to open. Defaults to ZAEL_NAMESPACE env var or "default".

None
stack str | None

Stack name. Defaults to ZAEL_STACK env var or "zae-limiter".

None
region str | None

AWS region (e.g., "us-east-1").

None
endpoint_url str | None

Custom endpoint URL (e.g., LocalStack).

None
config_cache_ttl int

Config cache TTL in seconds (default: 60, 0 to disable).

60
auto_update bool

Auto-update Lambda on version mismatch (default: True).

True
rejection_cache_ttl float

Seconds a cached bucket state may be used to reject a request without a DynamoDB call (default: 1.0, 0 to disable). See ADR-147.

DEFAULT_REJECTION_CACHE_TTL
rejection_cache_size int

Maximum cached bucket states (default: 10,000).

DEFAULT_REJECTION_CACHE_SIZE
stamp_ttl float

Seconds a bypass or soft stamp this repository writes stands after the config read that justified it (default: 60, at least 10). See ADR-151 decision 8.

DEFAULT_STAMP_TTL_SECONDS

Returns:

Type Description
Repository

Fully initialized Repository ready for use.

Raises:

Type Description
IncompatibleSchemaError

If schema migration is required.

VersionMismatchError

If auto_update is False and versions differ.

Example::

# Most users
repo = await Repository.open("my-app")

# Multi-tenant
repo_alpha = await Repository.open("tenant-alpha")

# Explicit stack
repo = await Repository.open("my-app", stack="custom-stack")

# Simplest (stack="zae-limiter", namespace="default")
repo = await Repository.open()

connect async classmethod

connect(namespace=None, *, stack=None, region=None, endpoint_url=None, config_cache_ttl=60, rejection_cache_ttl=DEFAULT_REJECTION_CACHE_TTL, rejection_cache_size=DEFAULT_REJECTION_CACHE_SIZE, stamp_ttl=DEFAULT_STAMP_TTL_SECONDS)

Connect to existing infrastructure without provisioning anything.

Use this when the CloudFormation stack, the DynamoDB table, and the namespace registry are managed outside the library — a packaged CloudFormation template, Terraform, or CDK. Unlike :meth:open, connect() never creates or mutates infrastructure: it issues reads only and raises when something it needs is absent.

Differences from :meth:open:

============================ =============== ========================== Situation open() connect() ============================ =============== ========================== Table missing Deploys stack InfrastructureNotFound Namespace unregistered Registers it NamespaceNotFoundError Version record missing Writes it InfrastructureNotFound Lambda version behind client Updates Lambda VersionMismatchError ============================ =============== ==========================

Stack resolution: stack arg → ZAEL_STACK env var → "zae-limiter".

Namespace resolution: namespace arg → ZAEL_NAMESPACE env var → "default".

Parameters:

Name Type Description Default
namespace str | None

Namespace to connect to. Defaults to ZAEL_NAMESPACE env var or "default".

None
stack str | None

Stack name. Defaults to ZAEL_STACK env var or "zae-limiter".

None
region str | None

AWS region (e.g., "us-east-1").

None
endpoint_url str | None

Custom endpoint URL (e.g., LocalStack).

None
config_cache_ttl int

Config cache TTL in seconds (default: 60, 0 to disable).

60
rejection_cache_ttl float

Seconds a cached bucket state may be used to reject a request without a DynamoDB call (default: 1.0, 0 to disable). See ADR-147.

DEFAULT_REJECTION_CACHE_TTL
rejection_cache_size int

Maximum cached bucket states (default: 10,000).

DEFAULT_REJECTION_CACHE_SIZE
stamp_ttl float

Seconds a bypass or soft stamp this repository writes stands after the config read that justified it (default: 60, at least 10). See ADR-151 decision 8.

DEFAULT_STAMP_TTL_SECONDS

Returns:

Type Description
Repository

Repository bound to the existing infrastructure.

Raises:

Type Description
InfrastructureNotFoundError

If the table doesn't exist, or exists but was never initialized by zae-limiter deploy.

NamespaceNotFoundError

If the namespace isn't registered.

VersionMismatchError

If the deployed Lambda version is behind the client version. Redeploy your stack to resolve.

IncompatibleSchemaError

If schema migration is required.

Example::

# Infrastructure deployed by your own CloudFormation
repo = await Repository.connect("my-app")
limiter = RateLimiter(repository=repo)

namespace async

namespace(name, *, on_unavailable=None, bucket_ttl_multiplier=None)

Return a scoped Repository for the given namespace.

The scoped repo shares the DynamoDB client, entity cache, and namespace cache with the parent, but has its own ConfigCache and namespace identity. Calling close() on a scoped repo is a no-op (it does not close the shared client).

Parameters:

Name Type Description Default
name str

Namespace name to resolve.

required
on_unavailable OnUnavailableAction | None

Override on_unavailable behavior for this namespace ("allow" or "block"). Persisted via set_system_defaults().

None
bucket_ttl_multiplier int | None

Override bucket TTL multiplier for this scoped repo. Defaults to the parent's value.

None

Returns:

Type Description
Repository

A new Repository scoped to the resolved namespace.

Raises:

Type Description
NamespaceNotFoundError

If the namespace is not registered.

close async

close()

Close the DynamoDB client.

No-op for scoped repos (created via namespace()).

create_table async

create_table()

Create the DynamoDB table if it doesn't exist.

delete_table async

delete_table()

Delete the DynamoDB table.

delete_stack async

delete_stack()

Delete the CloudFormation stack and all associated resources.

Permanently removes the stack including DynamoDB table, Lambda aggregator, IAM roles, and CloudWatch log groups. Waits for deletion to complete. No-op if stack doesn't exist.

Raises:

Type Description
StackOperationError

If deletion fails.

ensure_infrastructure async

ensure_infrastructure()

Ensure DynamoDB infrastructure exists.

.. deprecated:: Use Repository.builder(...).build() instead, which handles infrastructure creation during the build step.

Creates CloudFormation stack using stack_options passed to the constructor. No-op if stack_options was not provided.

Raises:

Type Description
StackOperationError

If CloudFormation stack creation fails

create_stack async

create_stack(stack_options=None)

Create DynamoDB infrastructure via CloudFormation.

.. deprecated:: 0.6.0 Use :meth:ensure_infrastructure instead. Pass stack_options to the Repository constructor. Will be removed in v1.0.0.

Parameters:

Name Type Description Default
stack_options StackOptions | None

Configuration for CloudFormation stack. If None, uses the stack_options passed to the constructor.

None

Raises:

Type Description
StackOperationError

If CloudFormation stack creation fails

register_namespace async

register_namespace(namespace)

Register a namespace in the registry (idempotent).

Creates forward (name -> ID) and reverse (ID -> name) mappings. Idempotent: returns existing ID if namespace already registered.

Parameters:

Name Type Description Default
namespace str

Namespace name to register.

required

Returns:

Type Description
str

The namespace_id (either newly created or existing).

Raises:

Type Description
ValidationError

If namespace is the reserved namespace "_".

register_namespaces async

register_namespaces(namespaces)

Bulk-register multiple namespaces.

Registers all namespaces in DynamoDB (forward + reverse records each).

Parameters:

Name Type Description Default
namespaces list[str]

List of namespace names to register.

required

Returns:

Type Description
dict[str, str]

Mapping of {name: namespace_id} for all namespaces.

Raises:

Type Description
ValidationError

If any namespace is the reserved namespace "_".

get_namespace async

get_namespace(namespace)

Get details for a single namespace by name.

Parameters:

Name Type Description Default
namespace str

Namespace name to look up.

required

Returns:

Type Description
dict[str, str] | None

Dict with {name, namespace_id, status, created_at}

dict[str, str] | None

or None if not found.

list_namespaces async

list_namespaces()

List all active namespaces with their IDs.

Performs a Query on PK = "_/SYSTEM#" with SK begins_with "#NAMESPACE#" (forward records only).

Returns:

Type Description
list[dict[str, str]]

List of {name, namespace_id, created_at} dicts.

delete_namespace async

delete_namespace(namespace)

Soft-delete a namespace. O(1) for data plane.

Removes the forward record and marks the reverse record as status="deleted". Data items are NOT deleted — they remain orphaned under the namespace's random ID prefix.

No-op if the namespace does not exist.

Parameters:

Name Type Description Default
namespace str

Namespace name to delete.

required

Raises:

Type Description
ValidationError

If namespace is the reserved namespace "_".

recover_namespace async

recover_namespace(namespace_id)

Recover a deleted namespace by its ID.

Reads the reverse record to find the original name, re-creates the forward record, and marks the reverse record as active.

Parameters:

Name Type Description Default
namespace_id str

Opaque namespace ID to recover.

required

Returns:

Type Description
str

The recovered namespace name.

Raises:

Type Description
EntityNotFoundError

If the reverse record does not exist.

ValidationError

If the namespace name is reserved.

NamespaceStateError

If the namespace is active or being purged.

list_orphan_namespaces async

list_orphan_namespaces()

List deleted namespaces with orphaned data.

Queries reverse records (SK begins_with "#NSID#") and filters for status="deleted".

Returns:

Type Description
list[dict[str, str]]

List of {namespace_id, namespace, deleted_at} dicts.

purge_namespace async

purge_namespace(namespace_id)

Purge all orphaned data for a deleted namespace.

Verifies the namespace is in "deleted" status, transitions to "purging", queries GSI4 to find all items, deletes them in batches, then removes the reverse record.

Safe to call on a non-existent namespace_id (no-op).

Parameters:

Name Type Description Default
namespace_id str

Opaque namespace ID to purge.

required

Raises:

Type Description
NamespaceStateError

If the namespace is "active" (cannot purge an active namespace).

RateLimiterUnavailable

DynamoDB kept some deletes unprocessed after the retries. The namespace stays "purging"; re-run the purge to finish.

require_reset_after_readers async

require_reset_after_readers(limits)

Refuse a reset_after limit passed to acquire(limits=...) (#638 A).

An override is never stored as config, but the slow path writes it onto the bucket item (b_{name}_rsa beside a zero refill rate) — exactly the item a pre-v0.15 aggregator's shard clone mints a fresh share on. So it is gated like a config write, but on the hot path, so at no cost when it passes: the lambda_version that open(), connect() or builder() already read is trusted. Only a refusal re-reads, with one strongly consistent GetItem, so a stack upgraded since the repository was opened is not refused on stale knowledge.

A repository that never read the record (the deprecated constructor) reads it once here, then trusts the cache like everyone else — one read per repository lifetime, against failing every such caller closed with no remedy on the same object. The ratchet does not run: nothing is stored as config, so no older client is made to misread a level.

Raises:

Type Description
VersionMismatchError

as for the config writers.

create_entity async

create_entity(entity_id, name=None, parent_id=None, cascade=False, metadata=None, principal=None)

Create a new entity.

Parameters:

Name Type Description Default
entity_id str

Unique identifier for the entity

required
name str | None

Optional display name (defaults to entity_id)

None
parent_id str | None

Optional parent entity ID (for hierarchical limits)

None
cascade bool

If True, acquire() will also consume from parent entity

False
metadata dict[str, str] | None

Optional key-value metadata

None
principal str | None

Caller identity for audit logging

None

Returns:

Type Description
Entity

The created Entity

Raises:

Type Description
InvalidIdentifierError

If entity_id or parent_id is invalid

EntityExistsError

If entity already exists

get_entity async

get_entity(entity_id, *, consistent_read=False)

Get an entity by ID.

consistent_read asks for a strongly consistent read (1 RCU instead of 0.5): for a caller that must see a write it just made.

delete_entity async

delete_entity(entity_id, principal=None)

Delete an entity and all its related records.

Deletes the entity's reset and top-up operation records, then every bucket item GSI3 indexes under it, then every item under the entity's PK (configs, usage snapshots, and #META last). The audit event records how many items this call deleted (records_deleted) and whether the entity is now gone (complete).

Parameters:

Name Type Description Default
entity_id str

ID of the entity to delete

required
principal str | None

Caller identity for audit logging

None

Raises:

Type Description
RateLimiterUnavailable

DynamoDB kept some deletes unprocessed after the retries (#742). What this call deleted is audited with complete: False, and what is left can still be found, so calling delete_entity again finishes it and audits the rest.

get_children async

get_children(parent_id)

Get all children of a parent entity.

set_parent async

set_parent(entity_id, parent_id, *, principal=None)

Move an entity to a new parent, or to no parent (ADR-150).

Rewrites the entity's META record and its GSI1 parent -> children keys, bumping its parent generation, then restamps parent_id and the effective cascade policy on every bucket the entity owns through the ADR-146 cascade fan-out. Whether the entity debits its new parent is the same decision as before the move: the per-resource cascade policy (ADR-146), falling back to the entity's own cascade flag, which a move does not change. An entity with no parent never cascades.

Nothing already debited moves: consumption charged to the old parent, including by leases open across the move, stays there.

Idempotent: repeating a move re-runs the fan-out, which is also how a move that raised FanoutIncomplete is finished.

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.

None

Returns:

Type Description
int

Number of bucket items stamped.

Raises:

Type Description
ValidationError

The entity would become its own ancestor, or the new parent's chain is deeper than 32 levels.

EntityNotFoundError

The entity or the new parent does not exist.

VersionMismatchError

The stack's Lambdas predate parent moves.

FanoutIncomplete

The META record moved but a bucket write failed part-way; re-run the same call to finish.

get_bucket async

get_bucket(entity_id, resource, limit_name, shard_id=0)

Get a single limit's bucket from the composite item.

get_buckets async

get_buckets(entity_id, resource=None, shard_id=0)

Get all buckets for an entity, optionally filtered by resource.

With pre-shard buckets (v0.9.0+), each item lives on its own partition key PK={ns}/BUCKET#{id}#{resource}#{shard}. When resource is specified, fetches the single bucket at the given shard_id. When resource is None, uses GSI3 (KEYS_ONLY) to discover all bucket PKs, then BatchGetItem to fetch full items.

The internal wcu infrastructure limit is filtered from the returned bucket states.

Parameters:

Name Type Description Default
entity_id str

Entity to query buckets for.

required
resource str | None

Resource name filter, or None for all resources.

None
shard_id int

Shard index (used only when resource is specified).

0

Returns:

Type Description
list[BucketState]

List of BucketState objects (one per application limit).

batch_get_buckets async

batch_get_buckets(keys)

Batch get composite buckets in a single DynamoDB call.

With composite items, each (entity_id, resource, shard) is a single DynamoDB item containing all limits. Returns individual BucketStates keyed by (entity_id, resource, limit_name) for backward compatibility.

Parameters:

Name Type Description Default
keys list[tuple[str, str, int]]

List of (entity_id, resource, shard_id) tuples. The shard is the one the acquire selected (issue #439); it is never assumed to be 0.

required

Returns:

Type Description
dict[tuple[str, str, str], BucketState]

Dict mapping (entity_id, resource, limit_name) to BucketState.

dict[tuple[str, str, str], BucketState]

Missing composite items are not included in the result.

Note

DynamoDB BatchGetItem supports up to 100 items per request. For larger batches, this method automatically chunks the requests.

batch_get_entity_and_buckets async

batch_get_entity_and_buckets(entity_id, bucket_keys)

Fetch entity metadata and composite buckets in a single BatchGetItem.

With composite items, each (entity_id, resource, shard) is a single DynamoDB item. Includes the entity's #META record alongside bucket records to avoid a separate get_entity() round trip.

Parameters:

Name Type Description Default
entity_id str

Entity whose META record to include

required
bucket_keys list[tuple[str, str, int]]

List of (entity_id, resource, shard_id) for composite buckets — the shard the acquire selected (issue #439)

required

Returns:

Type Description
Entity | None

Tuple of (entity_or_none, bucket_dict) where bucket_dict maps

dict[tuple[str, str, str], BucketState]

(entity_id, resource, limit_name) to BucketState.

Note

DynamoDB BatchGetItem supports up to 100 items per request. The META key counts toward that limit.

batch_get_configs async

batch_get_configs(keys, disabled_out=None, cascade_out=None)

Batch get config items in a single DynamoDB call.

Fetches config records (entity, resource, system level) in a single BatchGetItem request and returns deserialized limits.

Parameters:

Name Type Description Default
keys list[tuple[str, str]]

List of (PK, SK) tuples identifying config items

required
disabled_out dict[tuple[str, str], bool | str | None] | None

Optional dict to receive the tri-state disabled value of every key actually requested here, so a caller that also needs the disable walk can reuse this read instead of issuing an identical second BatchGetItem (ADR-125). Keys that resolve to no item are recorded as None ("no explicit value"), which is exactly what the walk needs — a requested-but-absent level is still a fresh answer. Only keys present in this dict may be reused; anything served from the config cache must not be, since caching the gate would let a first acquire with no bucket yet be admitted to a disabled resource permanently.

None
cascade_out dict[tuple[str, str], bool | str | None] | None

The same, for the tri-state cascade policy (ADR-146), and for the same reason: a bucket created from a stale cached policy and then only ever hit on the fast path keeps it.

None

Returns:

Type Description
dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]

Dict mapping (PK, SK) to (limits, on_unavailable) tuples.

dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]

on_unavailable is extracted from system config items (None for others).

dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]

Missing items are not included in the result.

Note

DynamoDB BatchGetItem supports up to 100 items per request. For larger batches, this method automatically chunks the requests. Uses eventually consistent reads (0.5 RCU per item).

get_or_create_bucket async

get_or_create_bucket(entity_id, resource, limit)

Get an existing bucket or create a new one with the given limit.

If the bucket exists, it is returned. If not, a new bucket is created with capacity set to the limit's capacity.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name (e.g., "gpt-4")

required
limit Limit

Limit configuration for the bucket

required

Returns:

Type Description
BucketState

Existing or newly created BucketState

build_bucket_put_item

build_bucket_put_item(state, ttl_seconds=86400)

Build a PutItem for a composite bucket (for use in transactions).

Wraps build_composite_create for backward compatibility with protocol.

build_bucket_update_item

build_bucket_update_item(entity_id, resource, limit_name, new_tokens_milli, new_last_refill_ms, expected_tokens_milli=None, shard_id=0)

Build an UpdateItem for a single limit in a composite bucket.

Legacy method — prefer build_composite_normal/retry/adjust for composite writes. This updates one limit's tk within the composite item.

build_owner_check

build_owner_check(entity_id, *, exists, pgen, pinc)

Build the ConditionCheck that pins a bucket create to its owner's META (ADR-150).

Rides in the create's own transaction: the owner's META must still hold the parent generation and incarnation the create read (or still be absent, when the read found none), so a move landing between the read and the create cancels it instead of leaving the old parent on a new bucket. Needs dynamodb:ConditionCheckItem. Writes nothing.

Parameters:

Name Type Description Default
entity_id str

The owner whose bucket the transaction creates.

required
exists bool

Whether the create's read found the owner's META.

required
pgen int | None

The parent generation that read returned.

required
pinc str | None

The incarnation that read returned.

required

build_composite_create

build_composite_create(entity_id, resource, states, now_ms, ttl_seconds=86400, cascade=False, parent_id=None, shard_id=0, shard_count=1, vu=None, rf_ms=None, bypass_until_ms=None, pgen=None, pinc=None)

Build a PutItem for creating a new composite bucket.

Used on first acquire for an entity+resource. Condition ensures no concurrent creation race (attribute_not_exists).

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
states list[BucketState]

BucketState objects for each limit

required
now_ms int

Current timestamp in milliseconds

required
ttl_seconds int | None

TTL in seconds from now, or None to omit TTL

86400
cascade bool

Whether the entity has cascade enabled

False
parent_id str | None

The entity's parent_id (if any)

None
shard_id int

Shard index for this bucket (default 0)

0
shard_count int

Total number of shards (default 1)

1
vu int | None

Valid-until stamp in epoch ms (#222 §2.1) — the earliest instant at which any limit on this item changes effective params. None omits the attribute, which the fast path reads as "no schedule, never expires".

None
rf_ms int | None

The rf to stamp, when the caller has clamped it above now_ms so that rf never sits below a window start the item carries (ADR-140). None stamps now_ms.

None
bypass_until_ms int | None

Stamp the item bypass with this deadline (#311, ADR-151 decision 8): created by a bypassed acquire, from config read uncached on that pass. None stamps nothing.

None
pgen int | None

The owner's parent generation, read from its META with cascade / parent_id (ADR-150), or None when those did not come from the owner's META. A Put cannot pin it (no item exists to compare with); a create that read META before a move is the ADR-125 two-pass race, which the move's second fan-out pass restamps with the higher generation.

None
pinc str | None

The owner's incarnation, read with pgen (ADR-150), or None to stamp none.

None

build_composite_normal

build_composite_normal(entity_id, resource, consumed, refill_amounts, now_ms, expected_rf, ttl_seconds=None, shard_id=0, vu=None, clear_vu=False, windows=None, rf_ms=None, window_lengths=None, seeds=None, pin_shard_count=None, applied_windows=None, grant_counts=None, owner=None, pin_vu=False, expected_vu=None, soft_stamps=None, soft_trusted=frozenset(), owner_pgen=None, owner_pinc=None, read_tokens=None, cleared_top_ups=(), bypass_stamp=None, soft_reads=None)

Build an UpdateItem for the normal write path (ADR-115 path 2).

ADD tk:(refill - consumed), tc:consumed for each limit.

read_tokens (limit name -> tk as read, millitokens): each limit whose refill delta is positive — a drip, a reset, a roll, a window opened — is pinned on tk <= :read. The delta was computed from that read, and a credit landing in between (a refund, a release) moves neither rf nor vu: the refill would stack on top of it. A pin that fails sends the write to the consumption-only retry, as a lost rf lock does. A trim (delta <= 0) stays unpinned so it still clamps. SET rf:now. CONDITION rf = :expected.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
consumed dict[str, int]

Amount consumed per limit (millitokens)

required
refill_amounts dict[str, int]

Refill amount per limit (millitokens)

required
now_ms int

Current timestamp in milliseconds

required
expected_rf int

Expected refill timestamp for optimistic lock

required
ttl_seconds int | None

TTL behavior: - None: Don't change TTL - 0: REMOVE ttl (entity has custom limits) - >0: SET ttl to (now + ttl_seconds)

None
shard_id int

Shard index for this bucket (default 0)

0
vu int | None

Valid-until stamp in epoch ms (#222 §2.1), or None to leave the attribute untouched. None is not "no schedule": it means this pass has nothing to say about the boundary, so a vu already on the item survives.

None
clear_vu bool

REMOVE vu instead of leaving it. Only meaningful with vu=None, and only correct when the caller knows nothing on the item is scheduled — _commit_initial() does, because its group covers every limit sharing the item. This is the half of the #468 fan-out's vu = 0 that makes it self-clearing rather than a permanent fast-path demotion.

False
pin_vu bool

When this write sets or removes vu, require the item's vu to still be expected_vu (absent when None): the value the caller read. A fan-out's vu = 0 stamped after the read is then never erased (#701).

False
expected_vu int | None

The vu the caller read off the item.

None
windows dict[str, tuple[int, int]] | None

Limit name -> (window_start_ms, reset_after_seconds) to stamp as b_{name}_ws and b_{name}_rsa (ADR-139). Only limits whose window rolled on this pass appear; an empty dict or None leaves every ws untouched. This is the only client write that moves a window start — the speculative fast path stays byte-identical — so _commit_initial() is where anchoring is decided, which is what makes "only admitted use anchors" fall out rather than being enforced.

The pair is one argument so that ws can never land without rsa. The aggregator and a new shard's inheritance read only the item, and a resource- or system-level reset_after never reaches an existing bucket through the param sync (#271/#296) — so an item holding ws alone would carry a window whose end nothing but a config-resolving client could compute.

None
rf_ms int | None

The rf to stamp, when the caller has clamped it so that it never moves backward and never sits below a window start the item carries (ADR-140). None stamps now_ms. The lock still compares against expected_rf, the stored value.

None
window_lengths dict[str, int] | None

Limit name -> reset_after_seconds to stamp as b_{name}_rsa alone, for a limit whose window did not move on this pass but whose configured length differs from the one on the item (ADR-139). A resource- or system-level reset_after change never fans out (#271/#296), and windows stamps rsa only when ws moves, so without this the item kept the old length for a whole window: the fast path then read an end the slow path no longer enforces. A name also in windows is skipped — that pair already carries the length, and two SETs on one path are a ValidationException.

None
seeds dict[str, BucketState] | None

Limit name -> the state of a limit missing from this existing item (#633): configured after the item was created, so no b_{name}_* attribute exists to ADD to. Each is SET in full on this same write — tk (its starting share less this acquire's consumption, already on the state), tc, cp/ra/rp and explicit sched/rsched overrides (:meth:_explicit_schedule_attrs) — through :meth:_limit_item_attrs, the helper the create shares. A seeded name must not also appear in consumed. Its window, if any, travels in windows like any other.

Each seed is guarded by attribute_not_exists(cp) OR attribute_not_exists(tk), so it can never overwrite a limit another writer seeded first — the rf lock already serialises two slow-path seeders, but rf does not move when both commit inside one millisecond. cp in the guard is what lets a seed repair tk = 0 left without parameters by an older client. Explicit SET rather than if_not_exists: the guard makes it exact, and the same attribute cannot also be ADDed in one expression (#168).

None
pin_shard_count int | None

The shard count a quota seed's share, or a reset or roll's grant (ADR-145 I4), was sized for, or None when the write grants no quota. Pins the write on attribute_not_exists(shard_count) OR shard_count <= :sized: a quota never drips, so a share sized for a count a doubling has since overtaken would be spent by the fast path (a pure ADD) before any refill clamp trimmed it — the aggregator's proactive doubling can land between this pass's read and its write without moving rf (#633). A lost pin falls to the consumption-only retry, which never seeds a quota, and the next pass reads the new count. <= rather than =: a share sized for a higher count (read off a sibling the item has not caught up with) is the smaller one, so only growth past it is unsafe.

None
applied_windows dict[str, int] | None

Limit name -> the window start this write leaves the limit's balance reflecting, stamped as b_{name}_wa (#640). Every window limit on the write appears, whether this pass opened, rolled, seeded or merely carried its window — the last is what marks an item written before the marker. Always the ws value the pass read or opened, never a copy of the ws path: a rollover fan-out can move ws between this pass's read and its write without touching rf, and that window has to stay unapplied until a pass rolls it. No condition term is needed: the rf lock already serialises every other writer of the marker.

None
soft_stamps dict[str, int | None] | None

Limit name -> the deadline to stamp as b_{name}_soft, or None to REMOVE it (#467, ADR-151 decisions 4 and 8). Only a pass whose limits were read uncached passes it, so a stamp left wrong — by an older client, or a missed fan-out — self-heals on the next fresh slow pass. A seeded name must not appear here; its stamp rides in the seed.

None
soft_reads dict[str, int | None] | None

Limit name -> the deadline the pass read off the item for that stamp (None when absent), for every name in soft_stamps: each stamp write is pinned on it, so a pass that read config before a fan-out cannot undo the fan-out. A lost pin falls to the consumption-only retry. Omitted: no pins.

None
soft_trusted frozenset[str]

Names the caller admitted as soft on authority it trusts over the item's stamp (config read fresh, or an acquire(limits=...) override). Their balance floor is omitted. Every other floor is OR'ed with the item's own soft stamp while it is live (> now_ms), so a limit soft on the item is never rejected here.

frozenset()
owner_pgen int | None

The owner's parent generation, read from its META with owner (ADR-150), or None to stamp no generation. Stamped beside the owner pair and pinned on attribute_not_exists(pgen) OR pgen <= :pgen: the move's fan-out never moves rf, so without the pin a pass that read META before a move could land its owner stamp after the fan-out and put the old parent back for good. Ignored without owner.

None
owner_pinc str | None

The owner's incarnation, read with owner_pgen (ADR-150), stamped beside it. With it the pin passes over a stamp of another incarnation, so a bucket a deleted predecessor left behind cannot outrank the recreated entity.

None
cleared_top_ups Sequence[str]

Quotas whose b_{name}_tu this write removes (ADR-149): a reset or roll on it ends the period the top-up was bought in. Only names the item was read carrying one.

()
bypass_stamp tuple[int | None, int | None] | None

(deadline, read) to SET the item's bypass stamp to deadline or, when it is None, REMOVE it, from bypass config read uncached on this pass (#311, ADR-151 decisions 4 and 8), or None to leave it. read is the deadline the pass read off the item (None when it carried no stamp): the write is pinned on it, so a pass that read config before a fan-out cannot undo the fan-out. A lost pin falls to the consumption-only retry, which writes no stamp.

None

build_composite_retry

build_composite_retry(entity_id, resource, consumed, shard_id=0, seeds=None, soft_trusted=frozenset(), period_guards=None, now_ms=None)

Build an UpdateItem for the retry write path (ADR-115 path 3).

Lost optimistic lock — skip refill, only consume. ADD tk:(-consumed), tc:consumed for each limit. CONDITION: tk >= consumed per limit (prevent negative on acquire).

ReturnValuesOnConditionCheckFailure=ALL_OLD: a failure here is a rejection, and the lease reports it from the item's real balance rather than from the in-memory state that just proved stale (#633). Free — the image rides on the failure response.

Parameters:

Name Type Description Default
seeds dict[str, BucketState] | None

Limit name -> state of a limit missing from the item (#633), exactly as passed to :meth:build_composite_normal. The lock that write lost may have been lost to a writer that does not seed (the aggregator), so the limit can still be missing here: tk is written as if_not_exists(tk, :seed) - :c under attribute_not_exists(tk) OR tk >= :c, and every parameter as if_not_exists. A limit another writer seeded first is therefore debited, never re-seeded.

None
soft_trusted frozenset[str]

As for :meth:build_composite_normal (#467): these soft limits carry no balance condition, only that the item carries their cp and tk; every other limit's condition admits on the item's own soft stamp while it is live at now_ms — and only beside the limit's cp and tk (#771). Either way a limit missing from the item is never debited onto a balance nothing granted. A soft seed carries none either — its soft-ness came from config read uncached on this pass. The retry never writes a stamp, so an expired one admits nothing here.

frozenset()
now_ms int | None

The instant a soft stamp's deadline is judged against (ADR-151 decision 8); None reads the clock.

None
period_guards dict[str, tuple[int | None, int | None]] | None

Limit name -> (reset_edge_ms, window_start_ms) for each quota the lost write was to reset, roll or open (#743). The retry debits the item's balance as it stands, so it must only do so once that period has been applied: a calendar quota requires rf >= reset_edge_ms (a reset pass stamps rf at or past the edge), a session quota b_{name}_wa = window_start_ms. Otherwise it would spend the previous period's balance and leave the reset pending, so the next pass grants a full allowance again.

None

build_composite_adjust

build_composite_adjust(entity_id, resource, deltas, shard_id=0, ceilings=None, trim=False, pin=None, *, tokens=True, counter=True)

Build an UpdateItem for the adjust write path (ADR-115 path 4).

An ADD for post-hoc correction. Can go negative by design. Positive delta = consumed more (subtract tokens, add to counter). Negative delta = consumed less (add tokens, subtract from counter).

A credit is self-trimming (#680, #721). With ceilings (limit name -> CreditCeiling), every credited limit that has one is conditioned on tk <= ceiling - credit: the credit lands only if the balance stays at or below the ceiling. The ceiling is the caller's, computed from a state that may be stale, so the condition also pins everything that state's ceiling was computed from and that a write can lower: the limit's stored cp, gc and schedule overrides, and, from pin, the item's shard_count, vu and item-level schedule strings (each None = absent). When the condition fails — a reset edge, a refill to capacity, a doubling, a re-grant, a changed limit — the caller re-issues the credit with trim=True: the same ADD plus SET vu = 0 in one write. A debit, or a credit with no known ceiling, carries no ceiling condition.

Every adjust is conditioned on attribute_exists(PK): an ADD on an item that is gone (swept by TTL, deleted) would create a stub with no parameters, which the speculative path then rejects forever. A failed condition on a write with no other term therefore means the item is gone, and the caller treats the adjustment as done.

Every refund, release, rollback and speculative compensation is built here, so the shard's cached state is forgotten here (ADR-147): tokens this process gives back must not be hidden by its own cache.

tokens=False writes tc only — a bypassed lease's adjustment, release or rollback, which never debited tk (#311). counter=False writes tk only — the refund of an enforce-shaped write that landed on a bypassed bucket, whose consumption stays counted.

build_bypass_consume

build_bypass_consume(entity_id, resource, consumed, shard_id=0, *, bypass_until_ms)

The slow path's write for a bypassed existing bucket (#311, ADR-151 §4).

ADD tc per limit and the renewed stamp, nothing else: no tk debit, no refill, no rf advance, no reset or window roll, so lifting the bypass needs no reset and leaves no debt. Conditioned on the item carrying a bypass stamp, whatever its deadline: a bypass cleared since this pass read config fails here, and the acquire is planned again, enforced. Only a pass that read bypass config uncached builds this write, so it is also the stamp's renewal (ADR-151 decision 8). Positional tokens (#bc{i}).

Parameters:

Name Type Description Default
consumed dict[str, int]

Limit name -> consumption, millitokens.

required
bypass_until_ms int

The stamp's new deadline, at most T after this pass's uncached config read.

required

build_stamp_fix

build_stamp_fix(entity_id, resource, shard_id=0, *, bypass=None, soft=None)

A rejected slow pass's stamp correction (ADR-151 decisions 4 and 8).

The stamps only: SET a deadline or REMOVE, for bypass and each b_{name}_soft, as the config this pass read uncached says. Nothing else on the item moves (no tk, tc, rf or vu), so it admits nothing. Each stamp is pinned on its value as read, so a fan-out or another pass that changed it since wins and this is a no-op. Positional tokens (#xb, #xo{i}).

Parameters:

Name Type Description Default
bypass tuple[int | None, int | None] | None

(deadline, read): the deadline to set, None to remove; the deadline read off the item, None when it carried none.

None
soft dict[str, tuple[int | None, int | None]] | None

Limit name -> (deadline, read), as for bypass.

None

fix_bucket_stamps async

fix_bucket_stamps(entity_id, resource, shard_id=0, *, bypass=None, soft=None)

Write :meth:build_stamp_fix; False when a pin no longer matched.

Called by a slow pass about to raise RateLimitExceeded, which otherwise writes nothing: an expired stamp config no longer backs then kept every rejection on the slow path until refill (review E1). The shard's cached state is forgotten once it lands (ADR-147), so the next fast rejection is remembered from the corrected item. Any error other than a lost pin propagates.

transact_write async

transact_write(items, client_request_token=None)

Execute a write, using single-item API when possible to halve WCU cost.

Returns the item's ALL_NEW attributes for a single-item update (free on UpdateItem; a refund pins the image, #721), else None.

client_request_token makes a multi-item transaction idempotent for ten minutes (TransactWriteItems' ClientRequestToken), so an SDK retry of one whose response was lost succeeds instead of failing its conditions. A single item ignores it.

write_each async

write_each(items)

Write items independently without cross-item atomicity (1 WCU each).

Each item is dispatched as a single PutItem, UpdateItem, or DeleteItem call. Use for unconditional writes (e.g., ADD adjustments) where partial success is acceptable.

Returns one entry per item: the Attributes an UpdateItem returned (non-empty only when the item asked for ReturnValues), else {}.

speculative_consume async

speculative_consume(entity_id, resource, consume, ttl_seconds=None, shard_id=None, now_ms=None, avoid_shards=frozenset(), avoid_parent_shards=frozenset())

Attempt speculative UpdateItem with condition check.

Checks entity cache for cascade metadata. If cache hit + cascade, issues child+parent UpdateItems concurrently via asyncio.gather and returns nested parent_result.

When shard_id is explicitly provided, targets that shard directly without cascade logic (used for shard retry).

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
consume dict[str, int]

Amount per limit (tokens, not milli)

required
ttl_seconds int | None

TTL in seconds from now, or None for no TTL change

None
shard_id int | None

Explicit shard to target (skips random selection and cascade logic). None means auto-select from entity cache.

None
now_ms int | None

The caller's "now" (issue #430). The limiter passes the same reading it used for its own refill math, so one logical acquire() observes one instant: the ttl stamp, the TTL-expiry guard and the caller's decision all agree. None reads the clock once here, for callers outside an acquire.

None
avoid_shards frozenset[int]

Child shards the rejection cache knows are short (ADR-147); the child shard is drawn among the others.

frozenset()
avoid_parent_shards frozenset[int]

The same for the parent's shards on the parallel cascade write (ADR-147 phase 2, decision 4).

frozenset()

Returns:

Type Description
SpeculativeResult

SpeculativeResult with:

SpeculativeResult
  • On cache hit + cascade + both succeed: parent_result populated
SpeculativeResult
  • On cache miss or non-cascade: parent_result is None
SpeculativeResult
  • On failure: old_buckets from ALL_OLD (or None if bucket missing)

select_shard

select_shard(entity_id, resource, shard_id=None, shard_count=None, avoid=frozenset())

Pick the bucket shard an acquire should target (GHSA-76rv, issue #439).

This is the only place a shard is drawn. The speculative fast path draws here, and the slow path reuses whatever shard the fast path already selected (passing it back as shard_id) so a BUCKET_MISSING on shard N reads and creates shard N — never a second random draw that lands on shard 0 again.

Selection is random.randrange(shard_count) from the cached shard_count, not a hash of the entity id: every call re-picks, which is what spreads one hot entity's writes across all of its shards.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket.

required
resource str

Resource name.

required
shard_id int | None

Explicit shard to honour verbatim, or None to draw one.

None
shard_count int | None

Count observed by the caller (e.g. on a speculative failure image, which never updates the cache); None reads the entity cache.

None
avoid frozenset[int]

Shards the rejection cache knows cannot fit this request (ADR-147). The draw is uniform over the others; when every shard is in it the caller has already rejected, so it is ignored rather than leaving nothing to draw from.

frozenset()

Returns:

Type Description
int

(shard_id, shard_count) with shard_count from the argument or

int

the entity cache (1 when unknown).

bump_shard_count async

bump_shard_count(entity_id, resource, current_count)

Double shard_count on shard 0 via conditional write.

Shard 0 is the source of truth for shard_count. The conditional expression shard_count = :old prevents double-bumping when multiple clients race to double concurrently. Also updates the entity cache with the new shard_count.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket.

required
resource str

Resource name.

required
current_count int

Current shard_count to double.

required

Returns:

Type Description
int

The new shard_count (doubled), or — if another client already

int

doubled (ConditionalCheckFailedException) — the winner's count

int

read from the failed write's ALL_OLD image, so the loser draws

int

from the new shard range instead of caching its stale count and

int

landing back on the exhausted shard (issue #439).

set_limits async

set_limits(entity_id, limits, resource=DEFAULT_RESOURCE, principal=None, *, disabled=PRESERVE_DISABLED, cascade=PRESERVE_CASCADE)

Store limit configs for an entity (composite format, ADR-114).

All limits for an entity+resource are stored in a single composite item with SK '#CONFIG#{resource}'. This reduces cache-miss cost from N GetItem calls to 1 GetItem call.

Parameters:

Name Type Description Default
entity_id str

ID of the entity

required
limits list[Limit]

List of Limit configurations to store

required
resource str

Resource name (defaults to "default")

DEFAULT_RESOURCE
principal str | None

Caller identity for audit logging

None
disabled bool | str | None

Tri-state disabled flag. Defaults to preserving whatever value is already stored (this is a full-replace PutItem, so an explicit value must be passed to change it; see ADR-125). Passing an explicit value also fans out to existing buckets, exactly as disable_entity()/enable_entity() do. "bypass" bypasses the entity (#311): see bypass_entity(); gated at 0.17.0 like a soft limit.

PRESERVE_DISABLED
cascade bool | None

Tri-state cascade policy (ADR-146), preserved the same way. An explicit value is gated on the stack's version and fans out like set_entity_cascade().

PRESERVE_CASCADE

Raises:

Type Description
VersionMismatchError

limits carries a reset_after limit and the stack's Lambdas predate it (#638), or an explicit cascade and they predate the cascade policy. Nothing is written.

reconcile_bucket_to_defaults async

reconcile_bucket_to_defaults(entity_id, resource, effective_limits, stale_limit_names=None)

Reconcile bucket to effective defaults after config deletion (issue #327).

Updates limit fields (cp, ra, rp) to match the new effective limits, sets TTL (since entity is now on defaults), and removes stale limit attributes that no longer exist in the effective config.

No-op if bucket doesn't exist (uses attribute_exists(PK) condition).

Parameters:

Name Type Description Default
entity_id str

ID of the entity

required
resource str

Resource name

required
effective_limits list[Limit]

The new effective limits (resource/system defaults)

required
stale_limit_names set[str] | None

Limit names to REMOVE from bucket (limits that were in the deleted entity config but not in effective defaults)

None

get_limits async

get_limits(entity_id, resource=DEFAULT_RESOURCE)

Get stored limit configs for an entity (composite format, ADR-114).

Reads a single composite item with SK '#CONFIG#{resource}' containing all limits for the entity+resource pair.

get_entity_disabled async

get_entity_disabled(entity_id, resource)

Read the disabled value from an entity config item.

Returns:

Type Description
bool | str | None

True or False when explicitly set, "bypass" for a bypass

bool | str | None

(#311), None when unset (inherit).

get_entity_cascade async

get_entity_cascade(entity_id, resource)

Read the tri-state cascade policy from an entity config item (ADR-146).

Returns:

Type Description
bool | None

True or False when explicitly set, None when unset (inherit).

delete_limits async

delete_limits(entity_id, resource=DEFAULT_RESOURCE, principal=None)

Delete stored limit configs for an entity (composite format, ADR-114).

Deletes the single composite config item for this entity+resource.

Parameters:

Name Type Description Default
entity_id str

ID of the entity

required
resource str

Resource name (defaults to "default")

DEFAULT_RESOURCE
principal str | None

Caller identity for audit logging

None

list_entities_with_custom_limits async

list_entities_with_custom_limits(resource, limit=None, cursor=None)

List all entities that have custom limit configurations for a resource.

Uses GSI3 sparse index for efficient queries. Only entity-level configs have GSI3 attributes, so this query returns only entities with custom limits (not system or resource defaults).

Parameters:

Name Type Description Default
resource str

Resource to filter by (required).

required
limit int | None

Maximum number of entities to return. None for all.

None
cursor str | None

Pagination cursor from previous call. None for first page.

None

Returns:

Type Description
tuple[list[str], str | None]

Tuple of (entity_ids, next_cursor). next_cursor is None if no more results.

list_resources_with_entity_configs async

list_resources_with_entity_configs()

List all resources that have entity-level custom limit configs.

Uses the entity config resources registry (wide column with ref counts) for efficient O(1) lookup. Returns resources with count > 0.

Returns:

Type Description
list[str]

Sorted list of resource names with at least one entity having custom limits

set_resource_defaults async

set_resource_defaults(resource, limits, principal=None, *, disabled=PRESERVE_DISABLED, cascade=PRESERVE_CASCADE)

Store default limit configs for a resource (composite format, ADR-114).

All limits for a resource are stored in a single composite item with SK '#CONFIG'. This reduces cache-miss cost.

Parameters:

Name Type Description Default
resource str

Resource name

required
limits list[Limit]

List of Limit configurations to store

required
principal str | None

Caller identity for audit logging

None
disabled bool | str | None

Tri-state disabled flag. Defaults to preserving whatever value is already stored (this is a full-replace PutItem, so an explicit value must be passed to change it; see ADR-125). Passing an explicit value also fans out to existing buckets, exactly as disable_resource()/enable_resource() do. "bypass" bypasses the resource (#311): see bypass_resource(); gated at 0.17.0 like a soft limit.

PRESERVE_DISABLED
cascade bool | None

Tri-state cascade policy (ADR-146), preserved the same way. An explicit value is gated on the stack's version and fans out like set_resource_cascade().

PRESERVE_CASCADE

Raises:

Type Description
VersionMismatchError

limits carries a reset_after limit and the stack's Lambdas predate it (#638), or an explicit cascade and they predate the cascade policy. Nothing is written.

get_resource_defaults async

get_resource_defaults(resource)

Get stored default limit configs for a resource (composite format, ADR-114).

Reads a single composite item with SK '#CONFIG' containing all limits for the resource.

get_resource_disabled async

get_resource_disabled(resource)

Read the disabled value from a resource config item.

Returns:

Type Description
bool | str | None

True or False when explicitly set, "bypass" for a bypass

bool | str | None

(#311), None when unset (inherit).

get_resource_cascade async

get_resource_cascade(resource)

Read the tri-state cascade policy from a resource config item (ADR-146).

Returns:

Type Description
bool | None

True or False when explicitly set, None when unset (inherit).

delete_resource_defaults async

delete_resource_defaults(resource, principal=None)

Delete stored default limit configs for a resource (composite format, ADR-114).

Deletes the single composite config item for this resource.

Parameters:

Name Type Description Default
resource str

Resource name

required
principal str | None

Caller identity for audit logging

None

list_resources_with_defaults async

list_resources_with_defaults()

List all resources that have default limit configs from the resource registry.

set_system_defaults async

set_system_defaults(limits, on_unavailable=None, principal=None)

Store system-wide default limits and config (composite format, ADR-114).

All system limits and config (on_unavailable) are stored in a single composite item with SK '#CONFIG'. This reduces cache-miss cost.

Parameters:

Name Type Description Default
limits list[Limit]

List of Limit configurations (apply to all resources)

required
on_unavailable OnUnavailableAction | None

Behavior when DynamoDB unavailable ("allow" or "block")

None
principal str | None

Caller identity for audit logging

None

Raises:

Type Description
VersionMismatchError

limits carries a reset_after limit and the stack's Lambdas predate it (#638). Nothing is written.

get_system_defaults async

get_system_defaults()

Get system-wide default limits and config (composite format, ADR-114).

Reads a single composite item with SK '#CONFIG' containing all limits and on_unavailable setting.

Returns:

Type Description
tuple[list[Limit], OnUnavailableAction | None]

Tuple of (limits, on_unavailable). on_unavailable may be None if not set.

delete_system_defaults async

delete_system_defaults(principal=None)

Delete all system-wide default limits and config (composite format, ADR-114).

Deletes the single composite config item for system defaults.

Parameters:

Name Type Description Default
principal str | None

Caller identity for audit logging

None

get_system_limits async

get_system_limits()

Get system-wide default limits (without config).

This is a convenience method that returns only the limits. Use get_system_defaults() to also get on_unavailable config.

get_provisioner_state async

get_provisioner_state()

Get the provisioner state record for this namespace.

Returns:

Type Description
dict[str, Any]

Dict with keys: managed_system, managed_resources, managed_entities,

dict[str, Any]

last_applied, applied_hash. Returns empty state if no record exists.

put_provisioner_state async

put_provisioner_state(state)

Write the provisioner state record for this namespace.

Parameters:

Name Type Description Default
state dict[str, Any]

Dict with keys: managed_system, managed_resources, managed_entities, last_applied, applied_hash.

required

get_version_record async

get_version_record()

Get the infrastructure version record.

Returns:

Type Description
dict[str, Any] | None

Version record with schema_version, lambda_version, etc.

dict[str, Any] | None

None if no version record exists.

ping async

ping()

Check if the DynamoDB table is reachable.

Performs a lightweight GetItem operation to verify connectivity. Does not verify the table is initialized or has valid data.

Returns:

Type Description
bool

True if the table is reachable, False otherwise.

set_version_record async

set_version_record(schema_version, lambda_version=None, client_min_version=None, updated_by=None)

Set the infrastructure version record.

Parameters:

Name Type Description Default
schema_version str

Current schema version (e.g., "1.0.0")

required
lambda_version str | None

Currently deployed Lambda version

None
client_min_version str | None

Minimum compatible client version. None (the default) keeps the stored minimum — "0.0.0" only when there is none — so a Lambda deploy or upgrade never lowers a minimum a reset_after write raised (#638 C). Pass a value only to set the minimum deliberately.

None
updated_by str | None

Identifier of what performed the update

None

get_audit_events async

get_audit_events(entity_id, limit=100, start_event_id=None)

Get audit events for an entity.

Parameters:

Name Type Description Default
entity_id str

ID of the entity to query

required
limit int

Maximum number of events to return

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

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 with filtering and pagination.

Supports two query modes: 1. Entity-scoped: Query by entity_id (uses primary key) 2. Resource-scoped: Query by resource across all entities (uses GSI2)

Parameters:

Name Type Description Default
entity_id str | None

Entity to query (mutually exclusive for efficient queries)

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 str | None

Filter snapshots >= this timestamp (ISO format)

None
end_time str | None

Filter snapshots <= this timestamp (ISO format)

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.

get_usage_summary async

get_usage_summary(entity_id=None, resource=None, window_type=None, start_time=None, end_time=None)

Aggregate usage across snapshots into a summary.

Fetches all matching snapshots (auto-paginates internally) and computes: - Total consumption per limit type - Average consumption per snapshot per limit type - Time range of aggregated data

Parameters:

Name Type Description Default
entity_id str | None

Entity to query

None
resource str | None

Resource name filter

None
window_type str | None

Filter by window type ("hourly", "daily")

None
start_time str | None

Filter snapshots >= this timestamp (ISO format)

None
end_time str | None

Filter snapshots <= this timestamp (ISO format)

None

Returns:

Type Description
UsageSummary

UsageSummary with aggregated statistics

Raises:

Type Description
ValueError

If neither entity_id nor resource is provided

get_resource_buckets async

get_resource_buckets(resource, limit_name=None)

Get all buckets for a resource across all entities.

With composite items, each GSI2 entry is one composite item per entity. Returns individual BucketStates, optionally filtered by limit_name.

resolve_limits async

resolve_limits(entity_id, resource, disabled_out=None, cascade_out=None)

Resolve effective limits using the four-level config hierarchy.

Uses ConfigCache with batched fetch optimization. Falls back to sequential individual GetItem calls if batch resolution fails.

Parameters:

Name Type Description Default
entity_id str

Entity to resolve limits for

required
resource str

Resource being accessed

required
disabled_out dict[tuple[str, str], bool | str | None] | None

Optional dict to receive the tri-state disabled value of each config level this call actually read from DynamoDB. The disable walk's levels are a subset of these, so a caller needing both can reuse this read rather than issuing an identical second BatchGetItem — see resolve_disabled_from_fetched. Levels served from the config cache are deliberately absent from the dict: they are not fresh, and the gate must never be answered from cache.

None
cascade_out dict[tuple[str, str], bool | str | None] | None

The same, for the tri-state cascade policy (ADR-146); see resolve_cascade_from_fetched.

None

Returns:

Type Description
tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]

Tuple of (limits, on_unavailable, config_source)

resolve_on_unavailable async

resolve_on_unavailable()

Resolve on_unavailable from system config, with caching fallback.

Returns the on_unavailable action from system config. Caches the value after first successful load so it's available as fallback when DynamoDB is unreachable. Defaults to "block" if no system config exists and no cached value is available.

resolve_disabled_from_fetched

resolve_disabled_from_fetched(entity_id, resource, fetched)

Answer the disable walk from a config fetch, or decline (ADR-125).

resolve_limits(disabled_out=...) records the tri-state disabled of every level it actually read. The disable walk's levels are a subset of those, so when all of them were read in that same call, the walk can be evaluated here for free instead of issuing an identical second BatchGetItem.

Returns None — meaning "the caller must call resolve_disabled" — if even one level is missing from fetched. A missing level is one the config cache served, and a cached value must never answer this gate: a first acquire() for an entity with no bucket yet would then be admitted on a stale false, and would go on to create an unstamped bucket that the already-finished fan-out will never stamp. That is permanent admission to a disabled resource, which is why resolve_disabled is uncached in the first place.

Note the distinction this relies on: a level present in fetched with value None was genuinely read and has no explicit value (so the walk moves on); a level absent from fetched was not read at all.

resolve_bypass_from_fetched

resolve_bypass_from_fetched(entity_id, resource, fetched)

Whether the walk resolves to bypass, from a config fetch, or decline (#311).

The resolve_disabled_from_fetched contract: None when any level of the walk was served from the config cache, which must never answer it.

resolve_cascade_from_fetched

resolve_cascade_from_fetched(entity_id, resource, fetched)

Answer the cascade-policy walk from a config fetch, or decline (ADR-146).

The resolve_disabled_from_fetched contract, for resolve_limits( cascade_out=...): None means "the caller must call resolve_access", because a level the config cache served must not answer it — a bucket created from a stale cached policy and then only ever hit on the fast path would keep it.

Returns:

Type Description
tuple[bool | None, str | None] | None

(policy, deciding_level); policy is None when no level sets it,

tuple[bool | None, str | None] | None

meaning the entity's own META cascade applies.

limits_read_fresh

limits_read_fresh(entity_id, resource, source, fetched)

Did one config fetch read every level that decided these limits? (#467)

fetched is the disabled_out of the same resolve_limits call: it holds exactly the config keys that call read from DynamoDB rather than from the config cache. The limits were read fresh when the level they came from (source) and every level of higher precedence were all read — a cached higher level could hide a newer definition. A limit's soft-ness rides with the limit (ADR-151), so a fresh read is the authority the slow path re-stamps b_{name}_soft from.

resolve_soft_limits async

resolve_soft_limits(entity_id, resource)

Each resolved limit's soft-ness, read uncached (#467, ADR-151 §6.2).

A bucket the slow path creates or seeds is stamped soft from here when the config cache served this pass's limits: a stale soft stamp is unbounded over-admission (soft -> hard on a bucket that then only ever takes the fast path), where a stale capacity is merely late. One BatchGetItem of the four config levels (2 RCU), paid only on a create or seed whose limits came from the cache.

Returns:

Type Description
dict[str, bool]

Limit name -> soft, for the limits the four-level resolution picks

dict[str, bool]

(the first level defining any). Empty when no level defines one.

resolve_disabled async

resolve_disabled(entity_id, resource)

Resolve the effective disabled state for an entity+resource (ADR-125).

Walks entity(resource) -> entity(default) -> resource and returns the first level that sets disabled explicitly. This walk is independent of the limits walk in resolve_limits(): a level that sets disabled but defines no limits still decides the outcome, which is what lets an entity-level disabled: false re-admit one entity to a disabled resource.

Deliberately uncached — see ADR-125. Called only on the slow path and by the eager fan-out, never on the speculative fast path.

Parameters:

Name Type Description Default
entity_id str

Entity to resolve for

required
resource str

Resource being accessed

required

Returns:

Type Description
bool

(effective_disabled, deciding_level) where deciding_level is

str | None

"entity", "entity_default", "resource", or None if nothing set it.

resolve_access async

resolve_access(entity_id, resource, *, consistent_read=False, include_limits=False, include_system=False)

Resolve disabled and the cascade policy from one uncached read.

Both walks cover the same three config items (ADR-125, ADR-146), so a slow path that needs both — the parent of a cascade, or any level the config cache served — pays for one BatchGetItem, as it did for disabled alone. Never cached, for the reasons resolve_disabled gives. consistent_read is for a caller resolving a level it just wrote: the cascade fan-out (pre-release review: an eventually consistent read returned the old policy and the fan-out stamped every bucket with it). include_limits also returns each resolved limit's soft-ness in limit_soft (#467), resolved from the walk's own items: the slow path stamps a created or seeded bucket from it when the config cache served that pass's limits. include_system adds the system config item to the same batch (+0.5 RCU), for limits the cache resolved from the system level; without it a walk that defines no limit reads as all-hard, the safe direction.

plan_quota_shard async

plan_quota_shard(entity_id, resource, limits, shard_id, shard_count, now_ms)

Plan the grant of every quota in limits for shard shard_id (ADR-145).

Reads the siblings once — one GSI3 KEYS_ONLY query + one BatchGetItem, the #587 reclaim's cost — and decides per quota, with the pure models.plan_quota_grant: a move off the current-period sibling covering this slot, or a fresh grant when none covers it.

The count is the largest of shard_count and every sibling's stored count, and each quota's share is its schedule-effective capacity at now_ms over that count. Before a fresh grant, a sibling still below that count has it raised (design §8 R5) — a write only when a lag is seen — and a legacy (no gc) quota on it has its grant size frozen at the count read, which is also the grant_count every planned debit expects, so the move and the freeze agree. Writes nothing else: the returned debits ride in the acquire's own transaction (I5), built by :meth:build_quota_donor_debits.

Returns:

Type Description
int

(count, {quota_name: QuotaGrant}, debits). Grants and debits

dict[str, QuotaGrant]

are empty when limits holds no quota or shard_count <= 1

list[QuotaDonorDebit]

(no sibling to read); a quota absent from the grants takes its full

tuple[int, dict[str, QuotaGrant], list[QuotaDonorDebit]]

share.

repair_created_quota_shard async

repair_created_quota_shard(entity_id, resource, shard_id, created_count, quota_names)

Raise a quota shard this client just created, if a doubling overtook it (ADR-145).

A create Put is guarded only by attribute_not_exists(PK), not by the count it planned at. Another client can double shard 0 between this client's plan and its Put; that doubling's propagation finds no item at this shard and writes nothing, so the Put lands at the old count. The shard would then reset at the old count every period and cover the slots a shard created at the new count also covers — a recurring over-admission of one new share per period.

So after the Put lands: one strongly consistent projected GetItem of shard 0's shard_count (1 RCU), and, only when it is higher, the grant-size freeze (:meth:_build_quota_count_freeze): SET shard_count = :new, gc = if_not_exists(gc, :old) under shard_count < :new. The current grant keeps the size it was planned at; the next reset grants at the new count. Shard 0 is never written.

Returns:

Type Description
bool

True when the created item was raised.

build_quota_donor_debits

build_quota_donor_debits(entity_id, resource, debits)

The donor side of each ADR-145 move, one Update per donor shard.

ADD tk -x under: the donor still exists; still holds x; its grant count is the one read (or absent and equal to its shard count — a v0.14 item, design §9); and its grant is still the current period (calendar rf >= edge; session wa unchanged). Several quotas moving off one donor share one Update: a transaction may touch an item once. Tokens are positional (#634).

get_shard_window_starts async

get_shard_window_starts(entity_id, resource, limit_names, shard_id=0)

Read one shard's duration-window starts, to seed a shard being created (ADR-140).

Shard 0 by default, because :meth:bump_shard_count already treats it as the source of truth for shard_count. A created shard inherits ws verbatim and stamps it as applied (wa = ws, #640), so ws > wa is false on the new item and it does not immediately re-roll itself: it joins the window in progress rather than opening one. Whether the window read here is still live is the caller's decision, against the rsa of the config it resolved — this returns the stored start and nothing more.

A separate read rather than an extra key in the create path's BatchGetItem: that call returns a dict keyed by (entity_id, resource, limit_name) with no shard component, so shard 0 and shard N would collide on every key. Strongly consistent, so 1 RCU: DynamoDB charges a GetItem on the full item size, not on the projection, and bucket items stay inside the ~1 KB budget (#222 §4.2), well under the 4 KB a single strongly consistent RCU covers. The projection only trims the response. Once per shard ever (≤ 31 per (entity, resource), plus TTL recreations) on a path already priced at 2.5 RCU + 2 WCU.

Consistency is load-bearing, not a nicety. A shard is usually created right after a wcu doubling, which is usually right after shard 0 was written — including the write that rolled its window. An eventually consistent read can return the pre-roll ws, which looks ended, so the caller opens a fresh window at full share instead of taking the ADR-145 move from the window shard 0 just opened: measured at 15 admitted against a quota of 10. The extra 0.5 RCU per shard creation is the whole price of closing that.

A limit absent from the result has no window on that shard — either it carries none, or the shard has been swept. The caller then opens a fresh window, which is the degraded case ADR-139 records under Consequences and which idle-restarting makes correct rather than merely tolerable.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket. On a cascade create this is the entity whose shard is being created — the parent for a parent shard, never the child. Parent and child windows are independent (ADR-139).

required
resource str

Resource name.

required
limit_names list[str]

The limits to look for; only these attributes are projected, so the read stays a fraction of the item.

required
shard_id int

The shard to read. Defaults to 0.

0

Returns:

Type Description
dict[str, int]

{limit_name: window_start_ms} for the limits whose ws is on

dict[str, int]

the item.

Raises:

Type Description
RateLimiterUnavailable

A stored ws is not an integer, the same treatment every other reader gives a corrupt window.

restamp_owner_if_current async

restamp_owner_if_current(entity_id, resource, shard_id, incarnation)

Claim a bucket stamped by another incarnation, if this one is current (ADR-150).

The slow path's owner pin refuses another incarnation's stamp, because its META read is eventually consistent and may be a deleted predecessor's. When that pin is lost to another incarnation, one strongly consistent read of META (1 RCU) settles which is current: if it is incarnation, the bucket is a predecessor's leftover and is restamped through the fan-out's write, which lets the current incarnation overwrite any other; if not, the caller read a stale record and the bucket is left alone.

Returns:

Type Description
bool

Whether the bucket was restamped.

disable_resource async

disable_resource(resource, principal=None)

Disable a resource for all entities without an explicit override (ADR-125).

Writes config first, then eagerly stamps every existing bucket so the change takes effect on the speculative fast path immediately.

Parameters:

Name Type Description Default
resource str

Resource to disable

required
principal str | None

Caller identity for audit logging

None

Returns:

Type Description
int

Number of bucket items stamped.

enable_resource async

enable_resource(resource, principal=None)

Explicitly enable a resource (stores disabled: false).

Returns:

Type Description
int

Number of bucket items unstamped.

clear_resource_disabled async

clear_resource_disabled(resource, principal=None)

Remove the resource's explicit disabled value, reverting to inherit.

Returns:

Type Description
int

Number of bucket items unstamped.

bypass_resource async

bypass_resource(resource, principal=None)

Bypass a resource for every entity without an explicit override (#311).

Every request is admitted and no limit's balance is debited, while consumption is still counted (tc, usage snapshots); only the reserved wcu limit still gates. Stored as disabled: "bypass", the fourth value of the ADR-125 walk, so an entity-level disabled: true still blocks that entity and an entity-level false still enforces it. Writes config, then stamps every existing bucket (bypass). clear_resource_disabled() lifts it.

Raises:

Type Description
VersionMismatchError

the stack's Lambdas predate bypass (0.17.0).

Returns:

Type Description
int

Number of bucket items stamped.

set_resource_cascade async

set_resource_cascade(resource, cascade, principal=None)

Set whether entities cascade to their parent on this resource (ADR-146).

An entity-level policy for the resource, or the entity's own _default_ policy, still outranks it. Writes config, then restamps every existing bucket of the resource (_fanout_cascade).

Raises:

Type Description
VersionMismatchError

the stack's Lambdas predate the cascade policy.

Returns:

Type Description
int

Number of bucket items stamped.

clear_resource_cascade async

clear_resource_cascade(resource, principal=None)

Remove the resource's cascade policy, reverting to inherit (ADR-146).

Returns:

Type Description
int

Number of bucket items restamped.

disable_entity async

disable_entity(entity_id, resource=None, principal=None)

Disable an entity, for one resource or across all of them (ADR-125).

Parameters:

Name Type Description Default
entity_id str

Entity to disable

required
resource str | None

Resource to scope to. None targets the entity's _default_ config, disabling it for every resource.

None
principal str | None

Caller identity for audit logging

None

Returns:

Type Description
int

Number of bucket items written. When scoped to one resource,

int

every written bucket is stamped disabled=True. When unscoped,

int

_fanout_entity re-resolves and (re)writes each of the entity's

int

buckets to its OWN resolved value (which may disagree with this

int

call for a resource with its own override) — see

int

_fanout_entity for why.

enable_entity async

enable_entity(entity_id, resource=None, principal=None)

Explicitly enable an entity, overriding a disabled resource.

Returns:

Type Description
int

Number of bucket items written. When scoped to one resource,

int

every written bucket is unstamped (disabled=False removes the

int

attribute). When unscoped, _fanout_entity re-resolves and

int

(re)writes each of the entity's buckets to its OWN resolved

int

value, which may disagree with this call — see _fanout_entity.

bypass_entity async

bypass_entity(entity_id, resource=None, principal=None)

Bypass an entity, for one resource or across all of them (#311).

Every request the entity makes is admitted and no limit's balance is debited, while consumption is still counted; only the reserved wcu limit still gates. An entity-level value outranks the resource's, so this re-admits one entity to a disabled resource as well. A bypass never extends to the entity's cascade parent, which resolves its own mode (ADR-151 §8). clear_entity_disabled() lifts it.

Parameters:

Name Type Description Default
entity_id str

Entity to bypass

required
resource str | None

Resource to scope to. None targets the entity's _default_ config, bypassing it for every resource.

None
principal str | None

Caller identity for audit logging

None

Raises:

Type Description
VersionMismatchError

the stack's Lambdas predate bypass (0.17.0).

Returns:

Type Description
int

Number of bucket items written, as for disable_entity().

clear_entity_disabled async

clear_entity_disabled(entity_id, resource=None, principal=None)

Remove the entity's explicit disabled value, reverting to inherit.

Returns:

Type Description
int

Number of bucket items restamped to match the inherited value.

set_entity_cascade async

set_entity_cascade(entity_id, cascade, resource=None, principal=None)

Set whether an entity cascades to its parent (ADR-146).

Parameters:

Name Type Description Default
entity_id str

Entity to configure.

required
cascade bool

True to debit the parent on this entity's acquires.

required
resource str | None

Resource to scope to. None targets the entity's _default_ config: every resource without its own policy.

None
principal str | None

Caller identity for audit logging.

None

Raises:

Type Description
VersionMismatchError

the stack's Lambdas predate the cascade policy.

Returns:

Type Description
int

Number of bucket items stamped, each with the policy resolved for

int

its own resource.

clear_entity_cascade async

clear_entity_cascade(entity_id, resource=None, principal=None)

Remove the entity's cascade policy, reverting to inherit (ADR-146).

Returns:

Type Description
int

Number of bucket items restamped.

reset_bucket async

reset_bucket(entity_id, resource, limits=None, principal=None, idempotency_key=None)

Restore an entity's balance on one resource to its full share, now (ADR-149).

Every shard of the (entity, resource) bucket gets one rf-locked, zero-consumption pass in one transaction: every limit on it is materialised as an acquire would (refill, pending reset edge, pending window roll), then each named limit is restored to its full share:

  • a dripping limit to its ceiling (debt is forgiven);
  • a calendar quota to its share, starting a new period;
  • a session quota's current window is marked ended, so the next admitted request opens a fresh one at its full share (the balance itself is left for that opener to restore).

Nothing else changes: the consumption counter, disabled, shard_count, config, the parent's buckets and other resources are left alone, and every shard's next acquire takes one materialising pass. A bucket that does not exist yet is a no-op (the first acquire creates it at full share). Not gated by version.

Parameters:

Name Type Description Default
entity_id str

Entity whose balance to restore.

required
resource str

Resource to restore. One resource at a time.

required
limits Sequence[str] | None

Limit names to restore. None restores every limit resolved for the pair.

None
principal str | None

Caller identity for audit logging.

None
idempotency_key str | None

Names this operation (1-64 characters: letters, digits, ., _, :, -). A retry with the same key within three days is applied once and returns the first call's result, even when that call changed nothing (no bucket yet: it is recorded, result 0). Without a key, an error from this call leaves the outcome unknown: the reset may or may not have landed, and retrying it may apply it twice. One residual: if the entity is deleted and recreated while the call is in flight, its record carries the old incarnation and a retry under the new one is applied again, once. And a record made while the entity had no META record matches every incarnation, both ways: a call with no META reuses any record for the key (right after a delete_entity, the deleted entity's), and a record written before META existed outlives a later delete and recreate (when delete_entity could not find it). Either way the call returns the old result and applies nothing: an under-grant, never an over-grant. Use a fresh key once the entity exists.

None

Returns:

Type Description
BucketOperationResult

The shards written and, per limit, the tokens restored.

Raises:

Type Description
ValidationError

A name is not a limit configured for the pair, or no limit is configured at all.

RateLimiterUnavailable

Another writer kept changing the bucket through every retry.

top_up async

top_up(entity_id, resource, amounts, principal=None, idempotency_key=None)

Add tokens to an entity's balance on one resource, now (ADR-149).

amounts mirrors acquire(consume=...): limit name to whole tokens. Every shard of the bucket gets one rf-locked, zero-consumption pass in one transaction; a pending reset edge or window roll is applied first, in the same write, so the credit lands in the current period.

  • A quota gains exactly the amount, spread over its shards by the share of the allowance each one holds (1 / gc). Every token a shard gains is recorded as allowance topped up this period (b_{name}_tu += its portion), whatever the shard held, and lasts until the next reset or window roll. That needs Lambdas at 0.17.0 or later (an older aggregator clamps it away): the version is checked once per repository and client_min_version is raised to 0.17.0.
  • A session quota with no live window opens one now, so the purchase starts the session.
  • A dripping limit gains at most the room below its ceiling (a refund, not a purchase); the amount granted is returned.

A plan upgrade is set_limits(...) with the new plan followed by top_up() of the difference. A bucket that does not exist yet is created on shard 0 holding its full share plus the quota top-up.

Parameters:

Name Type Description Default
entity_id str

Entity to credit.

required
resource str

Resource to credit.

required
amounts dict[str, int]

Whole tokens to add per limit name (each > 0).

required
principal str | None

Caller identity for audit logging.

None
idempotency_key str | None

Names this operation (1-64 characters: letters, digits, ., _, :, -) — a purchase id, say. A retry with the same key within three days is applied once and returns the first call's result, even when that call changed nothing (no bucket yet, or only dripping limits: it is recorded, result 0). Without a key, an error from this call leaves the outcome unknown: the tokens may or may not have been granted, and retrying may grant them twice. One residual: if the entity is deleted and recreated while the call is in flight, its record carries the old incarnation and a retry under the new one is applied again, once. And a record made while the entity had no META record matches every incarnation, both ways: a call with no META reuses any record for the key (right after a delete_entity, the deleted entity's), and a record written before META existed outlives a later delete and recreate (when delete_entity could not find it). Either way the call returns the old result and applies nothing: an under-grant, never an over-grant. Use a fresh key once the entity exists.

None

Returns:

Type Description
BucketOperationResult

The shards written and, per limit, the tokens granted.

Raises:

Type Description
ValidationError

An amount is not a positive integer, or a name is not a limit configured for the pair.

VersionMismatchError

A quota top-up against Lambdas predating 0.17.0.

ResourceDisabled

The bucket does not exist yet and the resource is disabled for the entity.

RateLimiterUnavailable

Another writer kept changing the bucket through every retry.

build_operation_record

build_operation_record(operation, operation_id, result, now_ms, writer, incarnation)

Build the Put of a reset or top-up's operation record (ADR-149).

One item per call, in the shard-0 bucket's partition, conditioned on attribute_not_exists(PK) and written in the same transaction as the shard writes: only one transaction carrying operation_id can ever commit. It stores the result, which a retry that finds it returns, and writer, the writing call's nonce: a call audits only an operation its own transaction committed, never one a same-key duplicate did. It stores the entity's incarnation too, and may replace a record of an earlier incarnation (ent differs), which reads as absent. "-" (no META) matches every incarnation on both sides: a record written with it is never replaced, and a call made with it never replaces one.

build_bucket_operation

build_bucket_operation(entity_id, resource, write, ttl=None, *, remove_ttl=False)

Build one shard's rf-locked reset or top-up Update (ADR-149 §4.2).

ADD a token delta per limit (never SET: a fast-path debit landing between the read and the write is kept), SET rf monotonic, vu = 0 to force one materialising pass that clamps anything credited in between, plus the grant counts, windows, applied markers and top-ups the plan sets and the tu / wtc it removes. Conditioned on the rf and shard_count read; tc, disabled and the owner stamps are never touched, and ttl only when one is given. The call's operation record rides in the same transaction (:meth:build_operation_record).

Tokens are positional (#634): #ot/:ot deltas, #og/:og grant counts, #ow/:ow + #or/:or windows, #oa/:oa applied markers, #ou/:ou top-ups, #ox / #oy removals, #on/:on the calendar quota reset counters (b_{name}_rn), #ottl/:ottl the ttl (epoch seconds) when one is given: a bucket on resource or system defaults (:meth:_operation_ttl), or REMOVE #ottl with remove_ttl: an entity-level bucket (ADR-136). Never both: the caller passes ttl only for a defaults bucket.

invalidate_config_cache async

invalidate_config_cache()

Invalidate all cached config entries (ADR-122).

get_cache_stats

get_cache_stats()

Get config cache performance statistics (ADR-122).

RepositoryProtocol

The RepositoryProtocol defines the interface for pluggable backends. Implement this protocol to use a different storage backend (e.g., for testing).

Changed in v0.17.0: the adjust, normal and retry writes' contracts (#721)

A custom backend must accept ceilings, trim and pin on build_composite_adjust from v0.17.0. The lease and the limiter pass them on every refund — a rollback, a release(), a negative adjust() or a speculative compensation — so a backend that does not accept them raises TypeError there. It must also accept read_tokens on build_composite_normal and period_guards on build_composite_retry, described below.

Required of every adjust write, whatever the keywords:

  • The item must exist. Fail the write (raise ConditionalCheckFailedException) when the bucket is gone. An unconditional credit onto a swept or deleted item creates a stub that rejects the entity forever.
  • A quota credit stays in its debit's period. For each credited limit whose CreditCeiling.quota_period is set, fail the write unless the period still holds — calendar: the item's rf is before reset_edge_ms and the reset-schedule strings in reset_sched_raw (item rsched, sched_tz, the limit's rsched) are still the item's; session: the limit's window start and applied marker equal window. An unconditional credit lands a refund of one period's debit in the next, which then admits more than its allowance. Applies to the trim=True write too.

Expected for the protection a refund relies on:

  • ceilings: limit name → CreditCeiling. Condition each credited limit on its balance staying at or below ceiling_milli after the credit, and on the stored capacity, grant count and schedule overrides it was computed from. Raise ConditionalCheckFailedException when the condition fails.
  • trim=True: the fallback the caller sends after that failure. Apply the same credit (still subject to the two requirements above) and, in the same write, mark the bucket so the next acquire re-materialises it (SET vu = 0 on DynamoDB).
  • pin: the item's (shard_count, vu, item-level schedule strings) as the ceiling saw them, held beside the ceilings.

build_composite_normal(read_tokens=...): limit name → the tk the slow pass read (millitokens). For every limit whose refill delta is positive — a drip, a reset, a roll, a window opened — fail the write unless the stored balance is still at or below that read. A refund or release landing between the read and the write moves neither rf nor vu, and an unpinned raise stacks on top of it. A trim (delta <= 0) is not pinned. A failure falls to the consumption-only retry, like a lost rf lock.

build_composite_retry(period_guards=...): limit name → (reset_edge_ms, window_start_ms), one of the two set, for each quota the lost write was to reset, roll or open. Fail the retry unless that period has been applied on the item — calendar: rf >= reset_edge_ms; session: the limit's window-applied marker equals window_start_ms — and return the old image on that failure (ALL_OLD). The lease then re-plans once from a fresh read; a second loss is a rejection. Without the guard the retry spends the previous period's balance and leaves the reset pending, so the next pass grants a full allowance again.

transact_write now returns the item's attributes after a single-item update (ALL_NEW), or None. A refund after the consumption-only retry takes its quota period from that image; a backend that returns None makes it fall back to the period the lease read, which can drop a refund after a reset raced the retry but never lands one in the wrong period.

Known gaps (not a backend's to close). A resource- or system-level change never reaches the bucket item (it propagates through the TTL). A lowered schedule there lets a refund land above the new ceiling until the next slow pass clamps it; a reset-schedule change there leaves the period guard following the lease's cron, so a reset under the new cron before the old edge can let the old period's refund land. An entity-level change reaches the item, and any reset-schedule change on the item drops the in-flight quota refunds (one per open lease per change).

Changed in v0.17.0: moving an entity (ADR-150)

A custom backend must implement set_parent, build_owner_check and restamp_owner_if_current. The lease appends one build_owner_check item (a condition-only transaction item that writes nothing) per owner whose bucket a commit creates, after the bucket items and donor debits: it must fail the transaction, with a per-item cancellation reason, when the owner's META no longer holds the parent generation and incarnation the pass read (or now exists when the pass read none). The lease calls restamp_owner_if_current when the slow path's owner stamp lost its pin to another incarnation.

Changed in v0.17.0: soft limits and bypass (ADR-151)

A custom backend must implement build_bypass_consume, resolve_bypass_from_fetched, resolve_soft_limits, limits_read_fresh, _stamp_deadline, bypass_resource and bypass_entity, and accept the new keywords below; the limiter and the lease pass them on every slow pass, so a backend that does not raises TypeError there. fix_bucket_stamps is optional: the limiter skips it on a backend without it.

  • Stamps are deadlines. A bucket's bypass and b_{name}_soft hold an epoch-ms deadline. speculative_consume must admit a declared limit without testing its balance only while b_{name}_soft is live and the item carries the limit, or while bypass is live; fail an expired stamp as SCHEDULE_BOUNDARY (never a rejection); and report bypassed on its result. A bypassed bucket is debited nothing but still counts tc and wcu.
  • build_composite_create(bypass_until_ms=...) stamps bypass with that deadline; each state's soft / soft_until_ms stamps b_{name}_soft.
  • build_composite_normal(soft_stamps=, soft_reads=, soft_trusted=, bypass_stamp=): SET each stamp to its deadline or REMOVE it, each pinned on the value read (soft_reads, the second element of bypass_stamp); waive a balance floor on a live soft stamp, and omit it for a soft_trusted limit.
  • build_composite_retry(soft_trusted=, now_ms=): the same waivers on the consumption-only retry, for its balance floors and its quota period guards; it never writes a stamp except a seed's.
  • build_composite_adjust(tokens=False) writes tc only (a bypassed lease's adjustment); counter=False writes tk only (the bypass refund), still subject to the ceilings and period guards above.
  • resolve_access(include_limits=, include_system=) returns each limit's soft-ness, read uncached, in ConfigAccess.limit_soft; bypass is set when the deciding disabled value is "bypass".
  • get_resource_disabled / get_entity_disabled may return "bypass", and the setters accept disabled="bypass": test disabled is True, never its truthiness.

RepositoryProtocol

Bases: Protocol

Protocol for rate limiter data backends.

All storage backends (DynamoDB, Redis, SQLite, In-Memory) must implement this protocol to work with RateLimiter. The protocol is divided into:

  • Properties: Backend identification and configuration
  • Lifecycle: Connection management
  • Entity operations: CRUD for rate-limited entities
  • Bucket operations: Token bucket state management
  • Limit config: Stored limit configurations at entity/resource/system level
  • Version management: Schema and Lambda version tracking
  • Audit logging: Security audit trail
  • Usage snapshots: Historical consumption tracking
Example

Custom backend implementation

class MyBackend: @property def region(self) -> str | None: return "us-east-1"

async def get_entity(self, entity_id: str) -> Entity | None:
    ...

Duck typing - no inheritance needed

repo = MyBackend() assert isinstance(repo, RepositoryProtocol) # True at runtime

region property

region

AWS region (or None for local/in-memory backends).

endpoint_url property

endpoint_url

Custom endpoint URL (e.g., LocalStack, local DynamoDB).

stack_name property

stack_name

CloudFormation stack name.

table_name property

table_name

DynamoDB table name (same as stack_name).

namespace_name property

namespace_name

The human-readable namespace name.

namespace_id property

namespace_id

The opaque namespace ID used in DynamoDB keys.

capabilities property

capabilities

Declare which extended features this backend supports.

Returns:

Type Description
BackendCapabilities

BackendCapabilities instance with feature flags.

Example

if repo.capabilities.supports_audit_logging: events = await repo.get_audit_events(entity_id)

namespace async

namespace(name, *, on_unavailable=None, bucket_ttl_multiplier=None)

Return a scoped repository for the given namespace.

Parameters:

Name Type Description Default
name str

Namespace name to resolve.

required
on_unavailable OnUnavailableAction | None

Override on_unavailable for this namespace.

None
bucket_ttl_multiplier int | None

Override bucket TTL multiplier.

None

Returns:

Type Description
RepositoryProtocol

A repository scoped to the resolved namespace.

Raises:

Type Description
NamespaceNotFoundError

If the namespace is not registered.

register_namespace async

register_namespace(namespace)

Register a namespace (idempotent). Returns the namespace_id.

register_namespaces async

register_namespaces(namespaces)

Bulk-register namespaces. Returns {name: namespace_id}.

get_namespace async

get_namespace(namespace)

Get namespace details by name. Returns dict or None.

list_namespaces async

list_namespaces()

List active namespaces. Returns [{name, namespace_id, created_at}].

delete_namespace async

delete_namespace(namespace)

Soft-delete a namespace. No-op if not found.

recover_namespace async

recover_namespace(namespace_id)

Recover a deleted namespace. Returns the namespace name.

list_orphan_namespaces async

list_orphan_namespaces()

List deleted namespaces. Returns [{namespace_id, namespace, deleted_at}].

purge_namespace async

purge_namespace(namespace_id)

Purge all data for a deleted namespace. No-op if not found.

close async

close()

Close the backend connection and release resources.

Must be called when the repository is no longer needed. Safe to call multiple times.

ping async

ping()

Check if the backend is reachable.

Returns:

Type Description
bool

True if the backend is accessible, False otherwise.

ensure_infrastructure async

ensure_infrastructure()

Ensure backend infrastructure exists.

For DynamoDB: Creates CloudFormation stack if stack_options was provided to the constructor. For other backends: May create required resources or be a no-op.

Uses the options passed to the constructor. No-op if not provided.

create_entity async

create_entity(entity_id, name=None, parent_id=None, cascade=False, metadata=None, principal=None)

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 for hierarchical limits

None
cascade bool

If True, acquire() will also consume from parent entity

False
metadata dict[str, str] | None

Additional key-value metadata

None
principal str | None

Caller identity for audit logging

None

Returns:

Type Description
Entity

The created Entity

Raises:

Type Description
EntityExistsError

If entity already exists

get_entity async

get_entity(entity_id, *, consistent_read=False)

Get an entity by ID.

Parameters:

Name Type Description Default
entity_id str

Entity identifier

required
consistent_read bool

Read strongly consistently, for a caller that must see a write it just made

False

Returns:

Type Description
Entity | None

Entity if found, None otherwise

delete_entity async

delete_entity(entity_id, principal=None)

Delete an entity and all related records.

Parameters:

Name Type Description Default
entity_id str

Entity to delete

required
principal str | None

Caller identity for audit logging

None

Raises:

Type Description
RateLimiterUnavailable

The backend could not delete every record. What was deleted is audited, and calling delete_entity again finishes the rest.

get_children async

get_children(parent_id)

Get all child entities of a parent.

Parameters:

Name Type Description Default
parent_id str

Parent entity ID

required

Returns:

Type Description
list[Entity]

List of child entities

set_parent async

set_parent(entity_id, parent_id, *, principal=None)

Move an entity to a new parent, or to no parent (ADR-150).

Updates the entity's parent and makes every one of its bucket items name the new parent, so the next acquire debits it wherever the cascade policy (ADR-146) says the entity cascades. Consumption already charged to the old parent stays there.

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.

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 backend's readers predate parent moves.

FanoutIncomplete

The move landed but restamping stopped part-way; repeating the call finishes it.

restamp_owner_if_current async

restamp_owner_if_current(entity_id, resource, shard_id, incarnation)

Claim a bucket stamped by another incarnation, if this one is current (ADR-150).

The slow path's owner stamp only overwrites a stamp of its own incarnation, because its META read is eventually consistent and may be a deleted predecessor's. When that pin is lost to another incarnation, one strongly consistent META read settles it: if incarnation is current, the bucket is a predecessor's leftover and is restamped with the current owner (cascade, parent_id, generation and incarnation), never lowering a newer stamp; if not, it is left alone.

Parameters:

Name Type Description Default
entity_id str

The bucket's owner.

required
resource str

The bucket's resource.

required
shard_id int

The bucket's shard.

required
incarnation str

The incarnation the caller's pass read.

required

Returns:

Type Description
bool

Whether the bucket was restamped.

get_bucket async

get_bucket(entity_id, resource, limit_name, shard_id=0)

Get a token bucket by entity/resource/limit.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name (e.g., "gpt-4")

required
limit_name str

Limit name (e.g., "tpm", "rpm")

required

Returns:

Type Description
BucketState | None

BucketState if found, None otherwise

get_buckets async

get_buckets(entity_id, resource=None, shard_id=0)

Get token buckets for an entity.

When resource is specified, fetches the bucket at the given shard_id. When resource is None, uses GSI3 to discover all buckets across resources and shards.

Parameters:

Name Type Description Default
entity_id str

Entity owning the buckets

required
resource str | None

Resource name filter (None for all resources)

None
shard_id int

Shard to fetch when resource is specified

0

Returns:

Type Description
list[BucketState]

List of bucket states

get_or_create_bucket async

get_or_create_bucket(entity_id, resource, limit)

Get an existing bucket or create a new one with the given limit.

This is the primary method for initializing token buckets. If a bucket exists, it is returned. If not, a new bucket is created with capacity set to the limit's capacity.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name (e.g., "gpt-4")

required
limit Limit

Limit configuration for the bucket

required

Returns:

Type Description
BucketState

Existing or newly created BucketState

batch_get_buckets async

batch_get_buckets(keys)

Batch get composite buckets in a single call.

Parameters:

Name Type Description Default
keys list[tuple[str, str, int]]

List of (entity_id, resource, shard_id) tuples. The shard is part of a bucket item's identity (GHSA-76rv); callers pass the shard the acquire selected rather than assuming shard 0.

required

Returns:

Type Description
dict[tuple[str, str, str], BucketState]

Dict mapping (entity_id, resource, limit_name) to BucketState.

batch_get_entity_and_buckets async

batch_get_entity_and_buckets(entity_id, bucket_keys)

Fetch entity metadata and composite buckets in a single call.

Parameters:

Name Type Description Default
entity_id str

Entity whose metadata to include

required
bucket_keys list[tuple[str, str, int]]

List of (entity_id, resource, shard_id) for composite buckets

required

Returns:

Type Description
tuple[Entity | None, dict[tuple[str, str, str], BucketState]]

Tuple of (entity_or_none, bucket_dict).

select_shard

select_shard(entity_id, resource, shard_id=None, shard_count=None, avoid=frozenset())

Pick the bucket shard an acquire should target (GHSA-76rv, issue #439).

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
shard_id int | None

Explicit shard to honour verbatim, or None to draw one at random from the shard_count

None
shard_count int | None

Count the caller observed (a speculative failure image), or None to read the entity cache

None
avoid frozenset[int]

Shards known not to fit the request (ADR-147); the draw is uniform over the others. Passed only when non-empty.

frozenset()

Returns:

Type Description
int

Tuple of (shard_id, shard_count) where shard_count is the given

int

or cached value for this (entity, resource), defaulting to 1.

get_resource_buckets async

get_resource_buckets(resource, limit_name=None)

Get all buckets for a resource across all entities.

Used for capacity reporting and aggregation.

Parameters:

Name Type Description Default
resource str

Resource name

required
limit_name str | None

Optional filter by limit name

None

Returns:

Type Description
list[BucketState]

List of bucket states

build_bucket_put_item

build_bucket_put_item(state, ttl_seconds=86400)

Build a transaction item for upserting a bucket.

This is a synchronous method used to build transaction payloads.

Parameters:

Name Type Description Default
state BucketState

Bucket state to persist

required
ttl_seconds int

Time-to-live for the record

86400

Returns:

Type Description
dict[str, Any]

Transaction item dict for use with transact_write

build_owner_check

build_owner_check(entity_id, *, exists, pgen, pinc)

Build the ConditionCheck that pins a bucket create to its owner's META (ADR-150).

A transaction item asserting the owner's META still holds the parent generation and incarnation the create read, or is still absent when exists is False. Rides in the create's own transaction, so a move landing between the read and the create cancels it.

build_composite_create

build_composite_create(entity_id, resource, states, now_ms, ttl_seconds=86400, cascade=False, parent_id=None, shard_id=0, shard_count=1, vu=None, rf_ms=None, bypass_until_ms=None, pgen=None, pinc=None)

Build a PutItem for creating a new composite bucket.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
states list[BucketState]

BucketState objects for each limit

required
now_ms int

Current timestamp in milliseconds

required
ttl_seconds int | None

TTL in seconds from now, or None to omit TTL

86400
cascade bool

Whether the entity has cascade enabled

False
parent_id str | None

The entity's parent_id (if any)

None
vu int | None

Valid-until stamp in epoch ms, or None to omit (#222 §2.1)

None
rf_ms int | None

The rf to stamp, or None for now_ms (ADR-140)

None
bypass_until_ms int | None

The bypass stamp's deadline for a bucket created under bypass (#311, ADR-151), or None to stamp none

None
pgen int | None

The owner's parent generation read with cascade / parent_id (ADR-150), or None to stamp none

None
pinc str | None

The owner's incarnation read with pgen, or None

None

build_composite_normal

build_composite_normal(entity_id, resource, consumed, refill_amounts, now_ms, expected_rf, ttl_seconds=None, shard_id=0, vu=None, clear_vu=False, windows=None, rf_ms=None, window_lengths=None, seeds=None, pin_shard_count=None, applied_windows=None, grant_counts=None, owner=None, pin_vu=False, expected_vu=None, soft_stamps=None, soft_trusted=frozenset(), owner_pgen=None, owner_pinc=None, read_tokens=None, cleared_top_ups=(), bypass_stamp=None, soft_reads=None)

Build an UpdateItem for the normal write path (ADR-115 path 2).

read_tokens (limit name -> tk as read) pins every limit whose refill delta is positive on tk <= :read, so a credit landing between the read and the write fails the write rather than having the refill stack on it (#730).

owner is (cascade, parent_id) from the item owner's META, read this pass: when given, both stamps are rewritten so the fast path's denormalised copy is repaired (#684). None leaves them as stored. owner_pgen is the owner's parent generation read with them (ADR-150): stamped beside them, and the write is pinned on not lowering it, so a pass that read META before a move cannot undo it. owner_pinc is the owner's incarnation: the pin passes over a stamp of another incarnation, whose generation is not comparable.

pin_vu: when the write sets or removes vu, require it to still be expected_vu (absent when None), the value the caller read, so a limit-change fan-out's vu = 0 stamped after the read survives (#701).

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
consumed dict[str, int]

Amount consumed per limit (millitokens)

required
refill_amounts dict[str, int]

Refill amount per limit (millitokens)

required
now_ms int

Current timestamp in milliseconds

required
expected_rf int

Expected refill timestamp for optimistic lock

required
ttl_seconds int | None

TTL behavior (None=no change, 0=remove, >0=set)

None
vu int | None

Valid-until stamp in epoch ms, or None to leave it untouched

None
clear_vu bool

REMOVE vu rather than leaving it, for a pass that knows nothing on the item is scheduled (#222 §2.1)

False
windows dict[str, tuple[int, int]] | None

Limit name -> (window_start_ms, reset_after_seconds) (ADR-139). Only limits whose window rolled on this pass appear; None leaves every ws untouched. ws and rsa travel as one pair so neither is ever stamped alone.

None
rf_ms int | None

The rf to stamp, or None for now_ms. The lock still compares against expected_rf (ADR-140)

None
window_lengths dict[str, int] | None

Limit name -> reset_after_seconds to stamp as rsa alone, where the configured length differs from the item's and the window did not move (ADR-139)

None
seeds dict[str, BucketState] | None

Limit name -> state of a limit missing from this existing item (#633), SET in full on this write under attribute_not_exists(cp) OR attribute_not_exists(tk) rather than ADDed. Must not also appear in consumed.

None
pin_shard_count int | None

The count a quota seed's share, or a reset or roll's grant, was sized for; pins attribute_not_exists(shard_count) OR shard_count <= :sized so a racing doubling cannot leave an oversized quota share for the fast path to spend (#633, ADR-145 I4).

None
applied_windows dict[str, int] | None

Limit name -> the window start the write leaves that limit's balance reflecting, stamped as b_{name}_wa (#640) for every window limit on the write. The value read or opened, never a copy of the ws path.

None
grant_counts dict[str, int] | None

Limit name -> the shard count a quota reset or roll on this write re-granted it at, stamped as b_{name}_gc (ADR-145 I3).

None
bypass_stamp tuple[int | None, int | None] | None

(deadline, read): SET the item's bypass stamp to deadline (REMOVE it when None) from bypass config read uncached on this pass, pinned on the deadline read (absent when None) (#311, ADR-151); None leaves it.

None
soft_stamps dict[str, int | None] | None

Limit name -> the b_{name}_soft deadline to SET, or None to REMOVE it, from limits read uncached (#467, ADR-151).

None
soft_reads dict[str, int | None] | None

Limit name -> the soft deadline read off the item (None when absent), pinning each stamp write in soft_stamps.

None

build_composite_retry

build_composite_retry(entity_id, resource, consumed, shard_id=0, seeds=None, soft_trusted=frozenset(), period_guards=None, now_ms=None)

Build an UpdateItem for the retry write path (ADR-115 path 3).

period_guards (limit name -> (reset_edge_ms, window_start_ms)): for each quota the lost write was to reset, roll or open, the retry requires that period to be applied already (rf >= edge, or b_{name}_wa = window_start), so it never spends the previous period's balance (#743).

Must ask for ReturnValuesOnConditionCheckFailure=ALL_OLD: the lease reports a failure here from the item's real balance (#633).

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
consumed dict[str, int]

Amount consumed per limit (millitokens)

required
seeds dict[str, BucketState] | None

Limit name -> state of a limit still missing from the item (#633), written with if_not_exists so a limit another writer seeded first is debited, never re-seeded.

None
now_ms int | None

The instant a soft stamp's deadline is judged against (#467, ADR-151): a balance floor is waived only by a stamp live at it. None reads the clock.

None

build_composite_adjust

build_composite_adjust(entity_id, resource, deltas, shard_id=0, ceilings=None, trim=False, pin=None, *, tokens=True, counter=True)

Build an UpdateItem for the adjust write path (ADR-115 path 4).

An ADD for post-hoc correction. Can go negative by design.

tokens=False writes tc only (a bypassed lease, #311); counter=False writes tk only (a bypass refund). Positive delta = consumed more (subtract tokens, add to counter). Negative delta = consumed less (add tokens, subtract from counter).

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
deltas dict[str, int]

Delta per limit (millitokens, positive=consume, negative=release)

required
ceilings dict[str, CreditCeiling] | None

Limit name -> CreditCeiling. Each credited limit named here is conditioned on its balance staying at or below the ceiling after the credit, and on the stored cp / gc the ceiling was computed from (#721); a failed condition raises ConditionalCheckFailedException and the caller re-issues the credit with trim=True.

None
trim bool

Build the fallback: the same unconditional ADD plus SET vu = 0 in one write, so the speculative fast path cannot spend a balance above the ceiling before a slow pass clamps it.

False
pin CreditPin | None

The item's (shard_count, vu, item-level schedule strings) the ceilings were computed against (None = absent), pinned beside them: a doubling, a schedule change or a reset may have lowered the real ceiling.

None

transact_write async

transact_write(items, client_request_token=None)

Execute a write of one or more items.

Single-item batches use the corresponding single-item API (PutItem, UpdateItem, or DeleteItem) to halve WCU cost. Multi-item batches use TransactWriteItems for atomicity.

Parameters:

Name Type Description Default
items list[dict[str, Any]]

List of transaction items from build_bucket_put_item

required
client_request_token str | None

Idempotency token for a multi-item transaction (ADR-149); ignored for a single item.

None

Returns:

Type Description
dict[str, Any] | None

For a single-item update, the item's attributes after the write

dict[str, Any] | None

(ALL_NEW), which a later refund pins (#721); otherwise, or when

dict[str, Any] | None

a backend cannot say, None.

Raises:

Type Description
TransactionCanceledException

If multi-item transaction fails

ConditionalCheckFailedException

If single-item condition fails

write_each async

write_each(items)

Write items independently without cross-item atomicity.

Each item is dispatched as a single PutItem, UpdateItem, or DeleteItem call (1 WCU each). Use for unconditional writes (e.g., ADD adjustments) where partial success is acceptable.

Parameters:

Name Type Description Default
items list[dict[str, Any]]

List of items to write independently

required

Returns:

Type Description
list[dict[str, Any]] | None

One entry per item: the attributes an UpdateItem returned (when the

list[dict[str, Any]] | None

item asked for ReturnValues), else an empty dict. A backend may

list[dict[str, Any]] | None

return None; no caller depends on the result.

speculative_consume async

speculative_consume(entity_id, resource, consume, ttl_seconds=None, shard_id=None, now_ms=None, avoid_shards=frozenset(), avoid_parent_shards=frozenset())

Attempt speculative UpdateItem with condition check.

Issues an UpdateItem with ADD -consumed and condition attribute_exists(PK) AND tk >= consumed for each limit. Uses ReturnValuesOnConditionCheckFailure=ALL_OLD to return the current item state on failure.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
consume dict[str, int]

Amount per limit (tokens, not milli)

required
ttl_seconds int | None

TTL in seconds from now, or None for no TTL change

None
shard_id int | None

Explicit shard to target (skips random selection and cascade logic). None means auto-select from entity cache.

None
now_ms int | None

The caller's "now" (issue #430), so one logical acquire() observes one instant. None reads the clock via _now_ms() once inside.

None
avoid_shards frozenset[int]

Shards the caller knows cannot fit the request (ADR-147); the shard is drawn among the others. Passed only when non-empty, and only by a limiter whose repository keeps a rejection cache, so a backend without one never receives it.

frozenset()
avoid_parent_shards frozenset[int]

The same for the parent's shards on a parallel cascade write (ADR-147 phase 2), on the same terms.

frozenset()

Returns:

Type Description
SpeculativeResult

SpeculativeResult with success flag and either:

SpeculativeResult
  • On success: buckets, cascade, parent_id from ALL_NEW
SpeculativeResult
  • On failure with ALL_OLD: old_buckets from ALL_OLD
SpeculativeResult
  • On failure without ALL_OLD: old_buckets is None (bucket missing)

bump_shard_count async

bump_shard_count(entity_id, resource, current_count)

Double shard_count on shard 0 via conditional write.

Also updates the entity cache with the new shard_count.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
current_count int

Expected current shard_count

required

Returns:

Type Description
int

The new shard_count, or current_count if another client already doubled.

plan_quota_shard async

plan_quota_shard(entity_id, resource, limits, shard_id, shard_count, now_ms)

Plan the grant of every quota in limits for shard shard_id (ADR-145).

Reads the siblings once and decides per quota a move off the current-period sibling covering this slot, or a fresh grant when none does (models.plan_quota_grant). The count is the largest of shard_count and every sibling's stored count; a lagging sibling has it raised before a fresh grant (design §8 R5). Writes nothing else: the debits ride in the acquire's own transaction.

Parameters:

Name Type Description Default
entity_id str

Entity owning the shards

required
resource str

Resource the shards belong to

required
limits Sequence[Limit]

The limits being created or seeded; non-quotas are ignored

required
shard_id int

The shard being created or seeded

required
shard_count int

The caller's shard count

required
now_ms int

The pass's clock reading

required

Returns:

Type Description
int

(count, {quota_name: QuotaGrant}, debits); a quota absent from

dict[str, QuotaGrant]

the grants takes its full share.

repair_created_quota_shard async

repair_created_quota_shard(entity_id, resource, shard_id, created_count, quota_names)

Raise a just-created quota shard whose count a doubling overtook (ADR-145).

One strongly consistent read of shard 0's shard_count; when it is higher than created_count, the created item's count is raised with its current grant size frozen (gc = if_not_exists(gc, :old)).

Returns:

Type Description
bool

True when the created item was raised.

build_quota_donor_debits

build_quota_donor_debits(entity_id, resource, debits)

The donor side of each ADR-145 move, one Update per donor shard.

Conditioned on the donor still holding the tokens, still at the grant count read (or carrying none and at that shard count), and its grant still in the current period. Several quotas moving off one donor share one Update.

get_shard_window_starts async

get_shard_window_starts(entity_id, resource, limit_names, shard_id=0)

Read one shard's duration-window starts, to seed a shard being created (ADR-140).

A shard created mid-window joins the window in progress rather than opening its own, so the create path reads the ws it would inherit from shard 0 — the source of truth for shard_count already.

The read must be strongly consistent: a stale pre-roll ws looks ended, and the caller would then grant the new shard a fresh full share instead of the ADR-145 move from the window shard 0 just opened.

Parameters:

Name Type Description Default
entity_id str

Entity whose shard is being created. On a cascade create this is the parent for the parent's shard, never the child.

required
resource str

Resource name

required
limit_names list[str]

The limits to look for

required
shard_id int

The shard to read. Defaults to 0.

0

Returns:

Type Description
dict[str, int]

{limit_name: window_start_ms}. A limit absent from the result

dict[str, int]

has no window on that shard, or the shard does not exist.

reconcile_bucket_to_defaults async

reconcile_bucket_to_defaults(entity_id, resource, effective_limits, stale_limit_names=None)

Reconcile bucket to effective defaults after config deletion (issue #327).

Updates limit fields to match effective limits, sets TTL, and removes stale limit attributes. No-op if bucket doesn't exist.

Parameters:

Name Type Description Default
entity_id str

Entity owning the bucket

required
resource str

Resource name

required
effective_limits list[Limit]

New effective limits (resource/system defaults)

required
stale_limit_names set[str] | None

Limit names to REMOVE from bucket

None

set_limits async

set_limits(entity_id, limits, resource='_default_', principal=None, *, disabled=PRESERVE_DISABLED, cascade=PRESERVE_CASCADE)

Store limit configs for an entity.

Parameters:

Name Type Description Default
entity_id str

Entity to configure

required
limits list[Limit]

Limit configurations

required
resource str

Resource these limits apply to

'_default_'
principal str | None

Caller identity for audit logging

None
cascade bool | None

Tri-state cascade policy (ADR-146); defaults to preserving the stored value. An explicit value fans out to buckets.

PRESERVE_CASCADE

get_limits async

get_limits(entity_id, resource='_default_')

Get stored limit configs for an entity.

Parameters:

Name Type Description Default
entity_id str

Entity to query

required
resource str

Resource to get limits for

'_default_'

Returns:

Type Description
list[Limit]

List of limits (empty if none configured)

delete_limits async

delete_limits(entity_id, resource='_default_', principal=None)

Delete stored limit configs for an entity.

Parameters:

Name Type Description Default
entity_id str

Entity to delete limits for

required
resource str

Resource to delete limits for

'_default_'
principal str | None

Caller identity for audit logging

None

list_entities_with_custom_limits async

list_entities_with_custom_limits(resource, limit=None, cursor=None)

List all entities that have custom limit configurations for a resource.

Uses GSI3 sparse index for efficient queries.

Parameters:

Name Type Description Default
resource str

Resource to filter by (required).

required
limit int | None

Maximum number of entities to return. None for all.

None
cursor str | None

Pagination cursor from previous call. None for first page.

None

Returns:

Type Description
tuple[list[str], str | None]

Tuple of (entity_ids, next_cursor). next_cursor is None if no more results.

list_resources_with_entity_configs async

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

set_resource_defaults async

set_resource_defaults(resource, limits, principal=None, *, disabled=PRESERVE_DISABLED, cascade=PRESERVE_CASCADE)

Store default limits for a resource.

Resource defaults apply to all entities accessing this resource unless overridden at the entity level.

Parameters:

Name Type Description Default
resource str

Resource name

required
limits list[Limit]

Default limits

required
principal str | None

Caller identity for audit logging

None

get_resource_defaults async

get_resource_defaults(resource)

Get default limits for a resource.

Parameters:

Name Type Description Default
resource str

Resource name

required

Returns:

Type Description
list[Limit]

List of limits (empty if none configured)

delete_resource_defaults async

delete_resource_defaults(resource, principal=None)

Delete default limits for a resource.

Parameters:

Name Type Description Default
resource str

Resource name

required
principal str | None

Caller identity for audit logging

None

list_resources_with_defaults async

list_resources_with_defaults()

List all resources that have default limits configured.

Returns:

Type Description
list[str]

List of resource names

set_system_defaults async

set_system_defaults(limits, on_unavailable=None, principal=None)

Store system-wide default limits and config.

System defaults apply to ALL resources unless overridden.

Parameters:

Name Type Description Default
limits list[Limit]

Global default limits

required
on_unavailable OnUnavailableAction | None

Behavior when backend unavailable ("allow" or "block")

None
principal str | None

Caller identity for audit logging

None

get_system_defaults async

get_system_defaults()

Get system-wide default limits and config.

Returns:

Type Description
tuple[list[Limit], OnUnavailableAction | None]

Tuple of (limits, on_unavailable). on_unavailable may be None.

delete_system_defaults async

delete_system_defaults(principal=None)

Delete all system-wide defaults.

Parameters:

Name Type Description Default
principal str | None

Caller identity for audit logging

None

get_version_record async

get_version_record()

Get the infrastructure version record.

Returns:

Type Description
dict[str, Any] | None

Version record with schema_version, lambda_version, etc.

dict[str, Any] | None

None if no version record exists.

require_reset_after_readers async

require_reset_after_readers(limits)

Refuse reset_after limits the stack's Lambdas cannot read (#638).

Called by acquire(limits=...) before anything is written. Free when no limit carries reset_after, and free when the backend already knows its readers are new enough.

Raises:

Type Description
VersionMismatchError

the readers predate reset_after.

set_version_record async

set_version_record(schema_version, lambda_version=None, client_min_version=None, updated_by=None)

Set the infrastructure version record.

Parameters:

Name Type Description Default
schema_version str

Current schema version (e.g., "1.0.0")

required
lambda_version str | None

Currently deployed Lambda version

None
client_min_version str | None

Minimum compatible client version. None keeps the stored minimum ("0.0.0" when there is none), so a deploy or upgrade never lowers it (#638).

None
updated_by str | None

Identifier of what performed the update

None

get_audit_events async

get_audit_events(entity_id, limit=100, start_event_id=None)

Get audit events for an entity.

Parameters:

Name Type Description Default
entity_id str

Entity to query

required
limit int

Maximum events to return

100
start_event_id str | None

Pagination cursor (event ID to start after)

None

Returns:

Type Description
list[AuditEvent]

List of audit events, most recent first

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 with filtering and pagination.

Parameters:

Name Type Description Default
entity_id str | None

Entity to query (uses primary key)

None
resource str | None

Resource filter (required if entity_id is None)

None
window_type str | None

Filter by "hourly" or "daily"

None
start_time str | None

Filter snapshots >= this timestamp (ISO format)

None
end_time str | None

Filter snapshots <= this timestamp (ISO format)

None
limit int

Maximum items per page

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

get_usage_summary async

get_usage_summary(entity_id=None, resource=None, window_type=None, start_time=None, end_time=None)

Aggregate usage across snapshots into a summary.

Parameters:

Name Type Description Default
entity_id str | None

Entity to query

None
resource str | None

Resource filter

None
window_type str | None

Filter by "hourly" or "daily"

None
start_time str | None

Filter snapshots >= this timestamp

None
end_time str | None

Filter snapshots <= this timestamp

None

Returns:

Type Description
UsageSummary

UsageSummary with total, average, and time range

Raises:

Type Description
ValueError

If neither entity_id nor resource is provided

resolve_limits async

resolve_limits(entity_id, resource, disabled_out=None, cascade_out=None)

Resolve effective limits using the four-level config hierarchy.

Resolution order (first non-empty wins): 1. Entity-level config for this specific resource 2. Entity-level _default_ config (fallback for all resources) 3. Resource-level defaults 4. System-level defaults

Backend implementations should use native caching and resolution strategies. For DynamoDB, this uses ConfigCache with batched fetch optimization.

Parameters:

Name Type Description Default
entity_id str

Entity to resolve limits for

required
resource str

Resource being accessed

required
disabled_out dict[tuple[str, str], bool | str | None] | None

Optional dict receiving the tri-state disabled of each config level this call actually read, so a caller needing the disable walk too can reuse the read rather than issuing a second, identical one (ADR-125). Levels served from a backend cache must be omitted — see resolve_disabled_from_fetched.

None
cascade_out dict[tuple[str, str], bool | str | None] | None

The same, for the tri-state cascade policy (ADR-146); see resolve_cascade_from_fetched.

None

Returns:

Type Description
list[Limit] | None

Tuple of (limits, on_unavailable, config_source) where:

OnUnavailableAction | None
  • limits: Resolved limits or None if nothing found at any level
ConfigSource | None
  • on_unavailable: System-level unavailability behavior (if any)
tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]
  • config_source: "entity", "entity_default", "resource", "system", or None if no config found

resolve_disabled async

resolve_disabled(entity_id, resource)

Resolve the effective disabled state for an entity+resource (ADR-125).

resolve_access async

resolve_access(entity_id, resource, *, consistent_read=False, include_limits=False, include_system=False)

Resolve disabled and the cascade policy from one uncached read (ADR-146).

Both walks cover the same config items, so a backend answers them from one read. Never from cache, for the reasons resolve_disabled gives. consistent_read is for a caller resolving a level it just wrote. include_limits also returns each resolved limit's soft-ness in ConfigAccess.limit_soft from the same read (#467); include_system adds the system level to it.

limits_read_fresh

limits_read_fresh(entity_id, resource, source, fetched)

Whether one config fetch read every level that decided the limits (#467).

fetched is the disabled_out of that resolve_limits call. A fresh read is the authority the slow path re-stamps soft-ness from.

resolve_soft_limits async

resolve_soft_limits(entity_id, resource)

Each resolved limit's soft-ness, read uncached (#467, ADR-151 §6.2).

Used when the slow path creates or seeds a bucket from limits the config cache served: a created bucket's soft stamp must never be stale.

resolve_cascade_from_fetched

resolve_cascade_from_fetched(entity_id, resource, fetched)

Answer the cascade-policy walk from a config fetch, or decline (ADR-146).

Returns (policy, deciding_level), policy None when no level sets it; or None when any level is absent from fetched, meaning the caller must fall back to resolve_access.

get_entity_disabled async

get_entity_disabled(entity_id, resource)

Read the tri-state disabled flag from an entity config item (ADR-125).

Returns True/False when set explicitly, None when unset (inherit). In the contract because cli.py reads it directly.

get_resource_disabled async

get_resource_disabled(resource)

Read the tri-state disabled flag from a resource config item (ADR-125).

Returns True/False when set explicitly, None when unset (inherit). In the contract because cli.py reads it directly.

resolve_disabled_from_fetched

resolve_disabled_from_fetched(entity_id, resource, fetched)

Answer the disable walk from a config fetch, or decline (ADR-125).

Returns None when any level of the walk is absent from fetched, meaning the caller must fall back to resolve_disabled. A backend must never answer this gate from cached config: with no bucket yet, the fast-path guard cannot fire, so a stale value would admit an entity to a disabled resource and leave behind an unstamped bucket that the completed fan-out will never revisit.

build_bypass_consume

build_bypass_consume(entity_id, resource, consumed, shard_id=0, *, bypass_until_ms)

The slow path's tc-only write for a bypassed bucket (#311).

Conditioned on the item carrying a bypass stamp (any deadline), so a bypass cleared since the pass read config fails the write rather than admitting unenforced. Renews the stamp to bypass_until_ms (ADR-151 decision 8).

fix_bucket_stamps async

fix_bucket_stamps(entity_id, resource, shard_id=0, *, bypass=None, soft=None)

Correct a bucket's stamps before a slow-path rejection (ADR-151, review E1).

SET or REMOVE bypass and each b_{name}_soft as config read uncached says, each given as (deadline, read) and pinned on read; nothing else is written. False when a pin no longer matched. Optional: the limiter looks it up with getattr, so a backend without it simply skips the correction.

resolve_bypass_from_fetched

resolve_bypass_from_fetched(entity_id, resource, fetched)

Whether the walk resolves to bypass, from a config fetch, or decline (#311).

disable_resource async

disable_resource(resource, principal=None)

Disable a resource for all entities without an explicit override (ADR-125).

bypass_resource async

bypass_resource(resource, principal=None)

Bypass a resource: admit everything, debit nothing, count usage (#311).

enable_resource async

enable_resource(resource, principal=None)

Explicitly enable a resource (stores disabled: false).

clear_resource_disabled async

clear_resource_disabled(resource, principal=None)

Remove the resource's explicit disabled value, reverting to inherit.

disable_entity async

disable_entity(entity_id, resource=None, principal=None)

Disable an entity, for one resource or across all of them (ADR-125).

enable_entity async

enable_entity(entity_id, resource=None, principal=None)

Explicitly enable an entity, overriding a disabled resource.

bypass_entity async

bypass_entity(entity_id, resource=None, principal=None)

Bypass an entity, for one resource or across all of them (#311).

clear_entity_disabled async

clear_entity_disabled(entity_id, resource=None, principal=None)

Remove the entity's explicit disabled value, reverting to inherit.

get_resource_cascade async

get_resource_cascade(resource)

Read the tri-state cascade policy from a resource config item (ADR-146).

get_entity_cascade async

get_entity_cascade(entity_id, resource)

Read the tri-state cascade policy from an entity config item (ADR-146).

set_resource_cascade async

set_resource_cascade(resource, cascade, principal=None)

Set a resource's cascade policy and restamp its buckets (ADR-146).

clear_resource_cascade async

clear_resource_cascade(resource, principal=None)

Remove the resource's cascade policy, reverting to inherit (ADR-146).

set_entity_cascade async

set_entity_cascade(entity_id, cascade, resource=None, principal=None)

Set an entity's cascade policy, for one resource or all (ADR-146).

clear_entity_cascade async

clear_entity_cascade(entity_id, resource=None, principal=None)

Remove the entity's cascade policy, reverting to inherit (ADR-146).

reset_bucket async

reset_bucket(entity_id, resource, limits=None, principal=None, idempotency_key=None)

Restore an entity's balance on one resource to its full share, now (ADR-149).

Every shard, one rf-locked zero-consumption pass, one transaction; the named limits (all when None) back to their full share and a new period.

top_up async

top_up(entity_id, resource, amounts, principal=None, idempotency_key=None)

Add whole tokens per limit to an entity's balance on one resource, now (ADR-149).

A quota gains exactly the amount (above its plan for this period, behind the 0.17.0 version gate); a dripping limit at most up to its ceiling.

resolve_on_unavailable async

resolve_on_unavailable()

Resolve on_unavailable from system config, with caching fallback.

Returns the on_unavailable action. Caches the value after first successful load for use as fallback when DynamoDB is unreachable. Defaults to "block" if no system config and no cached value.

invalidate_config_cache async

invalidate_config_cache()

Invalidate all cached config entries.

Backends with caching should evict all entries. Backends without caching (e.g., Redis with native TTL) should no-op.

Call after modifying config to force immediate refresh.

get_cache_stats

get_cache_stats()

Get config cache performance statistics.

Returns statistics for monitoring cache behavior: - hits: Cache hits (avoided backend reads) - misses: Cache misses (backend reads performed) - size: Current entry count - ttl: TTL in seconds

Backends without caching should return zero-valued stats.

Returns:

Type Description
CacheStats

CacheStats object with cache metrics