zeroae / zeroae/zae-limiter

📋 Write an ADR for how write sharding divides a limit across shards

Open
#477 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

api-design area/limiter
Dominant language
Python
Stars
0
Forks
0
Avg merge
6h 51m
Merged PRs (30d)
104

Description

Description

Write sharding (GHSA-76rv-2r9v-c5m6, ADR-133, ADR-134) currently divides both capacity and refill by shard_count: each shard refills toward capacity_milli // shard_count at refill_amount_milli // shard_count (BucketState.effective_capacity_milli / effective_refill_amount_milli in src/zae_limiter/models.py). That keeps the steady-state rate correct and the total ceiling correct, but it shrinks per-shard burst as shard_count grows, with two user-visible consequences:

  1. A single request larger than capacity // shard_count is unadmittable on every shard. At capacity=1000 and shard_count=32 a shard holds ~31 tokens, so a 100-token request can never be admitted even though the entity's nominal limit is 1000.
  2. The caller sees a rejection whose retry_after_seconds never pays off — waiting does not make the request admissible, because no shard will ever hold enough.

This is recorded as a known limitation in ADR-133 ("Known limitation — per-shard request ceiling") and its observability is tracked in #475. schema.MAX_SHARD_COUNT = 32 bounds how small a share can get but does not solve it.

This issue is to write an ADR deciding how the library should handle it, and to expose the choice as a library option rather than hardcoding one behaviour. Use /adr create <title>; the next free number is 135 (see Numbering below).

Implementation is explicitly OUT of scope. File a follow-up issue once the ADR is accepted.

Why v1.0.0

This adds a public configuration option that changes admission semantics, so it belongs before the v1.0.0 API freeze rather than after — v1.0.0 is "API freeze, documentation, positioning, and additive schema enhancements", and a new per-limit config field stored in the ADR-114 composite attributes and denormalized onto bucket items is exactly an additive schema enhancement. Landing the decision after the freeze would mean either shipping v1.0 with the shrinking-burst behaviour locked in, or breaking admission semantics in a minor release.

If a different milestone fits better, say so: the plausible alternative is v0.13.0, the release carrying ADR-133 and ADR-134 (#439, #468), on the argument that the ADR should be written alongside the ADRs it amends. That was not chosen because v0.13.0 is scoped to correctness/CI repairs, and this is a new public option rather than a repair. v2.0.0 (Schema v2 / breaking changes) does not fit as long as the option is additive with a backward-compatible default.

Options to evaluate

Each option must carry an RT / RCU / WCU line and a correctness assessment (does it preserve the nominal ceiling? the nominal long-run rate? can it over-admit?). The project expects cost numbers in every design write-up.

# Option Sketch
1 Divide both (today) Correct ceiling, correct rate, shrinking burst. Zero cost. The baseline to beat.
2 Replicate capacity, divide refill only Every shard keeps the full capacity; only refill_amount is divided. Long-run rate stays correct (the refills sum to the nominal rate) and any single request up to the full capacity is admissible on any shard. Cost unchanged — no extra round trip. Trade-off to state plainly: total admittable burst becomes up to shard_count × capacity, so capacity stops being a hard ceiling and becomes "burst is elastic up to N×". Precedent worth citing: the reserved wcu limit is already replicated un-divided across shards (BucketState.shard_count docstring: "The reserved wcu limit is per-partition and is never divided"), so per-limit non-division is not a new concept in the schema.
3 Oversized-request escape hatch Keep dividing, but when a request exceeds a shard's share take a coordinated path — e.g. debit several shards in one TransactWriteItems, or route oversized requests to shard 0 which holds a full-capacity share. Costs extra RT/WCU on that path only; must not over-admit, and must interact correctly with the rf optimistic lock and with ADR-134's random selection.
4 Cap shard_count by the largest request Never shard beyond capacity // max_single_request, where max_single_request is a per-limit config hint. Cheap and predictable, but it caps throughput scaling exactly for entities that send large requests, and requires the user to declare something they may not know.
5 Asymmetric shards One full-capacity shard for large requests plus N-1 small shards for throughput. A hybrid of 1 and 3; adds a second shard-selection rule, which cuts against ADR-134's single random draw.
6 Aggregator rebalancing Do not divide on write; let the aggregator periodically reconcile so the shares sum to at most capacity. Eventual consistency on the ceiling, and it reintroduces the aggregator dependency that ADR-133 deliberately removed — note that tension.

Additional options may surface while writing; the ADR should record them rather than silently narrowing to these six.

Questions the ADR must settle

  • Where the option lives — a per-limit field on Limit alongside capacity / refill_amount / refill_period_seconds, versus a stack- or namespace-level setting.
  • How it is stored — the ADR-114 composite config attributes are l_{name}_cp / _ra / _rp; a new suffix would be needed. It must also be denormalized onto bucket items the way shard_count / cascade / disabled are, since the speculative fast path never reads config.
  • The default — changing the default changes admission behaviour for existing deployments. Is that acceptable pre-1.0?
  • Declarative round trip — whether the YAML manifest and the CloudFormation Custom::ZaeLimiterLimits resource carry it (in both directions, as Disabled does per ADR-125).
  • Error reporting — what RateLimitExceeded reports under each mode: today statuses report the per-shard share, not the undivided config, which is what makes the rejection "honest" but also what makes retry_after_seconds useless in case (2) above.

Acceptance Criteria

  • docs/adr/135-*.md exists with **Status:** Proposed and the standard sections (Context, Decision, Rationale/Consequences, Alternatives Considered)
  • The ADR enumerates at least the six options in the table above, each with an explicit RT / RCU / WCU figure and a stated verdict on ceiling correctness, long-run rate correctness, and over-admission
  • The ADR states a decision: exactly one option is selected as the default behaviour, and the option surface (name and allowed values) is named
  • The ADR answers all five questions under Questions the ADR must settle as statements, not open questions
  • ADR-133's "Known limitation — per-shard request ceiling" paragraph links to the new ADR
  • The write-sharding section of docs/performance.md (currently around the "pre-shard buckets" text, line ~240) links to the new ADR
  • #475 carries a comment or body edit referencing the new ADR number
  • A follow-up implementation issue exists and is linked from this issue (filed after the ADR is accepted)
  • uv run pytest and pre-commit pass (docs-only change; no src/ edits in this issue)

Numbering

ADR numbers 126-132 are the multi-region set, and 133/134 land with #439 (fix/439-client-shard-create, not yet on main). Next free is therefore 135 — re-verify at write time, and note that #320's title still claims "ADR-133" for the noun-first CLI restructure, which is stale.

Out of Scope

  • Implementing the chosen option (config field, storage, denormalization, manifest round trip, RateLimitExceeded changes) — follow-up issue
  • Observability for the per-shard ceiling — #475
  • Changing MAX_SHARD_COUNT

Related

ADR-133, ADR-134, GHSA-76rv-2r9v-c5m6, GHSA-w6c2-33wf-qfwf, #439, #468, #475, #116

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading ADR-133, ADR-134, and the write-sharding section of docs/performance.md, then use /adr create with the next verified number. Produce docs/adr/135-*.md with the required options, cost and correctness analysis, one selected default, and resolved configuration questions; update the linked references and run uv run pytest and pre-commit.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
documentation
Issue type
Documentation
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.