feat(0157): default key limits of 1 req/s + 100k monthly quota - #195
feat(0157): default key limits of 1 req/s + 100k monthly quota#195adamkoot wants to merge 4 commits into
Conversation
Branched from `docs/0156_self-service-auth-assumptions` rather than `develop`: PR #187 rewrites this task's quota-reset section — the calendar alignment of the monthly quota is unverified, so the single-date rework boundary is our design intent until 0171 #7 measures it. Building from `develop` would implement the superseded spec. Committed here instead of pushed straight to `develop` (the usual promote-task path) because PR #187 modifies the same file in `backlog/`; moving it on `develop` would leave that PR with a modify/delete conflict.
The deployed usage plan gave a key 100 req/s and 10 000/day — a shape
designed for one hand-issued partner key, not a default for keys anybody
can mint by signing in with Discord. Self-issued keys now get 1 req/s
sustained, burst 5, and 100 000 requests per calendar month.
One plan, renamed in place, rather than a second plan beside it. Every
property of AWS::ApiGateway::UsagePlan is Update-requires-No-interruption,
so the rename carries no replacement risk, and the partner key served 14
requests in 30 days — all our own verification curls — so there was no
working partner to protect. The plan's logical id (ApiUsagePlanDBBE8AB1)
is unchanged, so CloudFormation updates the deployed plan in place rather
than creating a second one.
Config fields renamed so scope comes first, then property:
apiKeyRateLimit and apiKeyBurstLimit become selfServicePlanRateLimit and
selfServicePlanBurstLimit, apiGatewayPartnerDailyQuota becomes
selfServicePlanMonthlyQuota. apiGateway* is now reserved for gateway- and
stage-level settings. CloudFormation sees only the values, so nothing in
AWS is touched by the rename.
Validation drops the old apiGatewayThrottleRate >= apiKeyRateLimit check
(justified as letting a key reach its per-key SLA — self-service carries
no SLA) for a proportionality ratio: a plan limit may be at most one
tenth of the stage's default per-method limit, applied to rate and burst.
At 200/400 that permits 1/5 and rejects 100. Its job is narrow — stop the
design doc's 100 req/s being reinstated by typo.
The plan id is published as /prices/{env}/pricing-api-free-plan-id
because the backend that will issue keys (0160) lives in ComputeStack, a
dependency of ApiGatewayStack, so it cannot read the plan object without
closing the cycle.
BREAKING CHANGE: the API key construct id changes from PartnerApiKey to
SelfServiceApiKey, which changes its logical id — CloudFormation removes
prices-production-partner-key and adds prices-production-selfservice-key
with a new value. On deploy every holder of the old key is cut off. This
is deliberate: the old key's value was exposed by get-usage-plan-keys
while checking its usage, so it needed rotating regardless.
The seven utoipa 429 descriptions are a consumer surface outside the repo — they ship in the OpenAPI document served at GET /api-docs-json — so "Rate limit or daily quota exceeded" was a public statement the deploy makes false. Same for the auth and config doc comments that named 100 req/s as the gateway-side limit. docs/runbooks/manual-api-key-tier.md is the procedure for the tier above self-service: which plan a hand-made key attaches to, that payment is handled by bank transfer outside the product, and a registry table for what was issued. A hand-made plan never shows up in cdk diff and nobody reviews it, so recording the resource is a required step rather than an afterthought. The runbook also pins the credential-handling rule the usage check ran into: read keys with get-api-keys without --include-values, never get-usage-plan-keys, which returns the plaintext value with no flag to suppress it. The loadtest README gains the constraint 0121 now has to work around: after this change no key in the account can sustain 100 req/s, so a run against pricing-api-free-production measures our own throttle and spends a third of the month's allowance. 0121 must provision its own plan.
The task shipped past its own Summary and the record now says so. Three things the plan asserted turned out wrong and are corrected in place rather than quietly dropped: - Two plans became one, renamed. UsagePlanName is No-interruption, so the replacement risk that justified leaving partner-plan alone did not exist, and nobody was using the partner key anyway. - The stage throttle is per-method, not an aggregate stage pool (DD 5b). Three pieces of reasoning built on the aggregate reading had to be rewritten, and the epic's "10 keys saturate the 1000 req/s global burst" premise joins db.t4g.micro and the $900 figure as premises that do not map onto this deployment. The conclusion is unaffected. - The key rotates via the construct-id change (remove + add), not via ApiKey.Name replacement — corrected against the synthesized template. Also separated two claims that were previously asserted as one fact: a cached response still costs the caller a request is documented for billing, but that the quota decrements before the cache lookup is our inference — AWS's documented throttling order never mentions the cache. 0121 and 0047 pick up the consequence: no key in the account can sustain 100 req/s after this, so the load test has to provision its own plan and say which plan the key was on.
Review — no correctness bug foundVerified by synthesizing both the base and head templates for All 7 Findings below are all documentation/operability rather than logic. Medium1. §6 still states The pointer added at line 164 compounds it: it says 0157 supersedes the 100 req/s in §2.1/§7, but §2.1 is line 164 (already corrected) and §7 states no numeric limit. So a reader following the pointer finds nothing, while the number that is now 100× wrong sits in a section the pointer doesn't mention. 2. Step 7 re-runs step 2, which creates the replacement with the identical name Concrete failure: a customer stops paying mid-rotation, the operator recovers Low3. Below a stage rate of 10 the error message cannot be satisfied. If an operator clamps 4. "Change or revoke" states it runs in a shell with none of step 1's variables, then re-exports only 5.
6. Still describes Notes
|
Summary
pricing-api-free-<env>) rather than a second plan beside it — every property ofAWS::ApiGateway::UsagePlanis No-interruption, and the partner key served 14 requests in 30 days, all our own verification curls.apiKeyRateLimit/apiKeyBurstLimit→selfServicePlanRateLimit/BurstLimit,apiGatewayPartnerDailyQuota→selfServicePlanMonthlyQuota.apiGateway*is now reserved for gateway- and stage-level settings.apiGatewayThrottleRate >= apiKeyRateLimitwith a proportionality ratio — a plan limit may be at most one tenth of the stage's default per-method limit. Its job is narrow: stop the design doc's 100 req/s being reinstated by typo./prices/{env}/pricing-api-free-plan-idso the onboarding backend (0160) can read it without closing the Compute → ApiGateway cycle.docs/runbooks/manual-api-key-tier.mdfor the hand-issued tier above self-service, plus a docs sweep: the sevenutoipa429 descriptions ship in the public OpenAPI document, so "daily quota" was a public statement this deploy makes false.Deploy impact
The production API key rotates. The construct id changes
PartnerApiKey→SelfServiceApiKey, so CloudFormation removesprices-production-partner-keyand addsprices-production-selfservice-keywith a new value. Every holder of the old key is cut off on deploy. Deliberate — the old key's value was exposed byget-usage-plan-keysduring the usage check, so it needed rotating regardless.Base
Stacked on #187, not
develop— that PR rewrites this task's quota-reset section and is still open. Merge #187 first.