📋 Write an ADR for how write sharding divides a limit across shards
Nobody has claimed this yet.
- 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:
- A single request larger than
capacity // shard_countis unadmittable on every shard. Atcapacity=1000andshard_count=32a shard holds ~31 tokens, so a 100-token request can never be admitted even though the entity's nominal limit is 1000. - The caller sees a rejection whose
retry_after_secondsnever 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
Limitalongsidecapacity/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 wayshard_count/cascade/disabledare, 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::ZaeLimiterLimitsresource carry it (in both directions, asDisableddoes per ADR-125). - Error reporting — what
RateLimitExceededreports under each mode: today statuses report the per-shard share, not the undivided config, which is what makes the rejection "honest" but also what makesretry_after_secondsuseless in case (2) above.
Acceptance Criteria
-
docs/adr/135-*.mdexists with**Status:** Proposedand 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 pytestand pre-commit pass (docs-only change; nosrc/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,
RateLimitExceededchanges) — 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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