ЁЯПл The SchoolтА║ЁЯЫОя╕П API GatewayтА║ЁЯЪж рдзрдбрд╛ 07 тАФ Throttling рдЖрдгрд┐ usage plans: рджрд╛рд░рд╛рд╡рд░рдЪрд╛ token bucket
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯЪж рдзрдбрд╛ 07 тАФ Throttling рдЖрдгрд┐ usage plans: рджрд╛рд░рд╛рд╡рд░рдЪрд╛ token bucket

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 07 ┬╖ рдорд╛рдЧреЗ: lesson-06-auth ┬╖ рдкреБрдвреЗ: lesson-08-caching


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ06, рдЖрдгрд┐ office рдЧрд░реНрджреА рдХрд╢реА рдерд╛рдВрдмрд╡рддреЗ рддреЗ: token bucket (rate рдЖрдгрд┐ burst), рдкреНрд░рддреНрдпреЗрдХ stage рд╕рд╛рдареА рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ client рд╕рд╛рдареА рдорд░реНрдпрд╛рджрд╛ (API keys рдЖрдгрд┐ рджреИрдирд┐рдХ quota рдЕрд╕рд▓реЗрд▓реЗ usage plans), рдЖрдгрд┐ 429 Too Many Requests caller рд▓рд╛ рдХрд╛рдп рдХрд░рд╛рдпрд▓рд╛ рд╕рд╛рдВрдЧрддреЛ. apigw/demo.py рдордзрд▓реЗ throttle() рдПрдХрд╛рдЪ рдХреНрд╖рдгреА рдЖрда visitors рдкрд╛рдард╡рддреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рджрд╛рд░рд╛рд╡рд░ tokens рдЪреА рдмрд╛рджрд▓реА ЁЯкг рдареЗрд╡рд▓реЗрд▓реА рдЖрд╣реЗ. рдЖрдд рдпреЗрдгреНрдпрд╛рд╕рд╛рдареА рдкреНрд░рддреНрдпреЗрдХ visitor рдиреЗ рдПрдХ token рдШреНрдпрд╛рдпрд▓рд╛рдЪ рд╣рд╡рд╛. рдмрд╛рджрд▓реАрдд рдЬрд╛рд╕реНрддреАрдд рдЬрд╛рд╕реНрдд 5 tokens рдорд╛рд╡рддрд╛рдд (рд╣рд╛ burst). рдПрдХ рдорджрддрдиреАрд╕ рджрд░ рд╕реЗрдХрдВрджрд╛рд▓рд╛ 5 рдирд╡реЗ tokens рдмрд╛рджрд▓реАрдд рдЯрд╛рдХрддреЛ (рд╣рд╛ rate) тАФ рдкрдг рдмрд╛рджрд▓реАрдд рдорд╛рд╡рддреАрд▓ рддреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд рдХрдзреАрдЪ рдирд╛рд╣реА.

рдЖрда visitors рдПрдХрд╛рдЪ рдХреНрд╖рдгреА рдпреЗрддрд╛рдд. рдкрд╣рд┐рд▓реЗ рдкрд╛рдЪ рдЬрдг рдкрд╛рдЪрд╣реА tokens рдШреЗрддрд╛рдд рдЖрдгрд┐ рдЖрдд рдЬрд╛рддрд╛рдд. рд╕рд╣рд╛рд╡рд╛, рд╕рд╛рддрд╡рд╛ рдЖрдгрд┐ рдЖрдард╡рд╛ рдпрд╛рдВрдирд╛ рдмрд╛рджрд▓реА рд░рд┐рдХрд╛рдореА рджрд┐рд╕рддреЗ: "рдХреГрдкрдпрд╛ рдерд╛рдВрдмрд╛ рдЖрдгрд┐ рдкрд░рдд рдпрд╛" (429). рдПрдХрд╛ рд╕реЗрдХрдВрджрд╛рдирдВрддрд░ рдмрд╛рджрд▓реАрдд рдкреБрдиреНрд╣рд╛ рдкрд╛рдЪ рддрд╛рдЬреЗ tokens рдЕрд╕рддрд╛рдд.

Partner рд╢рд╛рд│рд╛рдВрдирд╛ рддреНрдпрд╛рдВрдЪреА рд╕реНрд╡рддрдГрдЪреА рдЫреЛрдЯреА рдмрд╛рджрд▓реА рдЖрдгрд┐ рддреНрдпрд╛рдВрдЪреНрдпрд╛ visitor pass рд╡рд░ рджреИрдирд┐рдХ рдорд░реНрдпрд╛рджрд╛ рдорд┐рд│рддреЗ тАФ рджрд┐рд╡рд╕рд╛рд▓рд╛ 1,000 рднреЗрдЯреА. 1,001 рд╡реА рднреЗрдЯ рдЙрджреНрдпрд╛рдкрд░реНрдпрдВрдд рдирд╛рдХрд╛рд░рд▓реА рдЬрд╛рддреЗ.

рдмрд╛рджрд▓реА рдХреЛрдгрд╛рд▓рд╛рд╣реА рд╢рд┐рдХреНрд╖рд╛ рдХрд░рдд рдирд╛рд╣реА. рддреА kitchen рд▓рд╛ рдЧрд░реНрджреАрдд рдмреБрдбрдгреНрдпрд╛рдкрд╛рд╕реВрди рд╡рд╛рдЪрд╡рддреЗ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    crowd["ЁЯСе 8 requests<br/>same instant"] --> b["ЁЯкг stage dev bucket<br/>burst 5 ┬╖ refill 5/s"]
    b -->|"tokens 5тЖТ0"| ok["тЬЕ 200 ├Ч 5"]
    b -->|"empty"| no["тП│ 429 ├Ч 3<br/>Too Many Requests"]
    key["ЁЯФС x-api-key: key-partner-1"] --> plan["ЁЯУЛ usage plan partner-basic<br/>2 rps ┬╖ burst 2 ┬╖ 1,000/day"]
    plan -->|"over the rate"| r["429 Too Many Requests"]
    plan -->|"over the quota"| q["429 Limit Exceeded"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/apigateway/lesson-diagrams.html#l07

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдПрдХрдЪ рдмреЗрд▓рдЧрд╛рдо client тАФ mobile app рдордзрд▓рд╛ рдПрдЦрд╛рджрд╛ loop, partner рдЪрд╛ batch job тАФ рд╕рдВрдкреВрд░реНрдг рд╡рд╛рдЯрд▓реЗрд▓реА account limit рднрд░реВрди рдЯрд╛рдХреВ рд╢рдХрддреЛ рдЖрдгрд┐ Region рдордзрд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ API рдмрдВрдж рдкрд╛рдбреВ рд╢рдХрддреЛ. рджрд╛рд░рд╛рд╡рд░рдЪреНрдпрд╛ рдорд░реНрдпрд╛рджрд╛ kitchen рд▓рд╛ рдЬрд┐рд╡рдВрдд рдареЗрд╡рддрд╛рдд рдЖрдгрд┐ рдХреНрд╖рдорддрд╛ рдиреНрдпрд╛рдпреНрдпрдкрдгреЗ рд╡рд╛рдЯрддрд╛рдд. Usage plans "рдПрдХ partner" рдЪреЗ рд░реВрдкрд╛рдВрддрд░ contract рдордзреНрдпреЗ рдорд╛рдиреНрдп рдХрд░рддрд╛ рдпреЗрддреАрд▓ рдЕрд╢рд╛ рдЖрдХрдбреНрдпрд╛рдВрдордзреНрдпреЗ рдХрд░рддрд╛рдд: рджрд░ рд╕реЗрдХрдВрджрд╛рд▓рд╛ рдЗрддрдХреЗ, рджрд┐рд╡рд╕рд╛рд▓рд╛ рдЗрддрдХреЗ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

apigw/gateway.py рдордзрд▓рд╛ TokenBucket рдлрдХреНрдд рдирдК рдУрд│реАрдВрдЪрд╛ рдЖрд╣реЗ: take(now) (now - t) * rate tokens рдЬреЛрдбрддреЛ (рдЬрд╛рд╕реНрддреАрдд рдЬрд╛рд╕реНрдд burst) рдЖрдгрд┐ рд╢рдХреНрдп рдЕрд╕рд▓реНрдпрд╛рд╕ рдПрдХ рдШреЗрддреЛ. рдкреНрд░рддреНрдпреЗрдХ stage рд▓рд╛ рд╕реНрд╡рддрдГрдЪреА bucket рдорд┐рд│рддреЗ (dev: 5 рдЖрдгрд┐ 5; prod: defaults RATE = 10_000, BURST = 5_000). Gateway.usage_plan() рдкреНрд░рддреНрдпреЗрдХ plan рд╕рд╛рдареА bucket рдЖрдгрд┐ quota рдмрдирд╡рддреЛ рдЖрдгрд┐ API keys рддреНрдпрд╛рд▓рд╛ рдЬреЛрдбрддреЛ. api_key=True рдЕрд╕рд▓реЗрд▓реНрдпрд╛ routes рд╕рд╛рдареА _handle() key рддрдкрд╛рд╕рддреЛ (рдЕрдиреЛрд│рдЦреА тЖТ 403), quota (429 Limit Exceeded) рдЖрдгрд┐ plan рдЪреА bucket (429 Too Many Requests) тАФ stage bucket рдЪреНрдпрд╛ рдЖрдзреА.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 apigw/demo.py throttle
python3 - <<'EOF'
import sys; sys.path.insert(0, "apigw"); from demo import office, Clock
from gateway import TokenBucket
b = TokenBucket(rate=5, burst=5, now=0)
print("t=0.0 ", [b.take(0) for _ in range(6)])
print("t=0.2 ", [b.take(0.2) for _ in range(2)])
print("t=10  ", b.take(10), "tokens left:", b.tokens)
c = Clock(); gw, _ = office(c)
gw.usage_plan("trial", rate=100, burst=100, quota_per_day=3, keys=["key-trial"])
print([gw.handle("GET", "/partner/students/9", {"x-api-key": "key-trial"})[0] for _ in range(5)], gw.handle("GET", "/partner/students/9", {"x-api-key": "key-trial"})[2])
print("no key:", gw.handle("GET", "/partner/students/9")[0::2])
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

throttle рдЖрдзреА stage dev: rate 5/s, burst 5 тАФ 8 requests in the same instant: рдЫрд╛рдкрддреЗ, рдордЧ 200 200 200 200 200 429 429 429, рдордЧ one second later: 200 200 200 200 200 429. Usage plan рд╕рд╛рдареА: 200 200 429 429 ┬╖ unknown key тЖТ 403.

рддреБрдордЪрд╛ snippet t=0.0 [True, True, True, True, True, False] рдЫрд╛рдкрддреЛ (рдкрд╛рдЪ tokens, рдордЧ рд░рд┐рдХрд╛рдореА), t=0.2 [True, False] (0.2 s ├Ч 5/s = рдПрдХ рдирд╡рд╛ token), t=10 True tokens left: 4 (рджрд╣рд╛ рд╢рд╛рдВрдд рд╕реЗрдХрдВрджрд╛рдВрдирдВрддрд░рд╣реА рдмрд╛рджрд▓реАрдд 5 рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд tokens рдХрдзреАрдЪ рдирд╕рддрд╛рдд), рдордЧ trial quota 3 рд╕рд╛рдареА [200, 200, 200, 429, 429] {'message': 'Limit Exceeded'}, рдЖрдгрд┐ no key: (403, {'message': 'Forbidden'}).

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рддреБрдореНрд╣реА рдкреНрд░рддреНрдпреЗрдХ 429 рдЖрдзреАрдЪ рд╕рд╛рдВрдЧреВ рд╢рдХрддрд╛: bucket рдордзрд▓реЗ tokens рдореЛрдЬрд╛, refill рдЬреЛрдбрд╛, рдЖрдгрд┐ quota рддрдкрд╛рд╕рд╛. рдкрд╣рд┐рд▓рд╛ рдХреНрд╖рдг burst рдард░рд╡рддреЛ; рддреНрдпрд╛рдирдВрддрд░рдЪрд╛ рдкреНрд░рддреНрдпреЗрдХ рд╕реЗрдХрдВрдж rate рдард░рд╡рддреЛ.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ рдЖрдзреА stage limits, рдордЧ рдПрдХрд╛ partner рд╕рд╛рдареА key рд╕рд╣ usage plan:

aws apigateway update-stage --rest-api-id abc123 --stage-name prod --patch-operations \
    op=replace,path=/*/*/throttling/rateLimit,value=500 \
    op=replace,path=/*/*/throttling/burstLimit,value=1000
aws apigateway create-usage-plan --name partner-basic \
    --throttle burstLimit=2,rateLimit=2 --quota limit=1000,period=DAY \
    --api-stages apiId=abc123,stage=prod
aws apigateway create-api-key --name partner-1 --enabled
aws apigateway create-usage-plan-key --usage-plan-id pl4n01 --key-id k3y001 --key-type API_KEY

Terraform:

resource "aws_api_gateway_usage_plan" "partner_basic" {
  name = "partner-basic"
  api_stages {
    api_id = aws_api_gateway_rest_api.school.id
    stage  = aws_api_gateway_stage.prod.stage_name
  }
  throttle_settings {
    rate_limit  = 2
    burst_limit = 2
  }
  quota_settings {
    limit  = 1000
    period = "DAY"
  }
}

ЁЯПн Production рдордзреНрдпреЗ рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдЖрд╣реЗ: back end рдЦрд░реЛрдЦрд░ рдХрд┐рддреА рд╕рд╣рди рдХрд░реВ рд╢рдХрддреЛ рддреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рдХрдореА stage limits рдареЗрд╡рд╛ (рддреЗ рдореЛрдЬрд╛), рдкреНрд░рддреНрдпреЗрдХ partner рд▓рд╛ usage plan рджреНрдпрд╛, рдЖрдгрд┐ 429s рд╡рд░ alarm рд▓рд╛рд╡рд╛, рдореНрд╣рдгрдЬреЗ customer рддрдХреНрд░рд╛рд░ рдХрд░рдгреНрдпрд╛рдЖрдзреАрдЪ рддреБрдореНрд╣рд╛рд▓рд╛ рдорд░реНрдпрд╛рджрд╛ рджрд┐рд╕реЗрд▓.

тПня╕П рдкреБрдвреЗ

рдЧрд░реНрджреА рдирд┐рдпрдВрддреНрд░рдгрд╛рдд рдЖрд▓реА. рдЖрддрд╛ рддреЛрдЪ рдкреНрд░рд╢реНрди kitchen рд▓рд╛ рддреНрд░рд╛рд╕ рди рджреЗрддрд╛ рд╕реЛрдбрд╡рд╛: рд╕реВрдЪрдирд╛ рдлрд▓рдХ тАФ caching.

git checkout lesson-08-caching

ЁЯЪж Lesson 07 тАФ Throttling & usage plans: a token bucket at the door

ЁЯУН You are here: Lesson 07 of 12 ┬╖ Previous: lesson-06-auth ┬╖ Next: lesson-08-caching


ЁЯУж What's in this branch

Lessons 01тАУ06, plus how the office stops a crowd: the token bucket (rate and burst), limits per stage and per client (usage plans with API keys and a daily quota), and what 429 Too Many Requests asks the caller to do. throttle() in apigw/demo.py sends eight visitors at the same instant.

ЁЯзТ Explain like I'm 5

At the door stands a bucket of tokens ЁЯкг. Every visitor must take one token to come in. The bucket holds at most 5 tokens (the burst). A helper drops 5 new tokens every second into it (the rate) тАФ but never more than the bucket holds.

Eight visitors arrive at the same moment. The first five take the five tokens and walk in. Visitors six, seven and eight find the bucket empty: "please wait and come back" (429). One second later there are five fresh tokens again.

Partner schools get their own small bucket and a daily limit on their visitor pass тАФ 1,000 visits a day. Visit 1,001 is refused until tomorrow.

The bucket does not punish anyone. It protects the kitchen from being flooded.

ЁЯЧ║я╕П Diagram

flowchart LR
    crowd["ЁЯСе 8 requests<br/>same instant"] --> b["ЁЯкг stage dev bucket<br/>burst 5 ┬╖ refill 5/s"]
    b -->|"tokens 5тЖТ0"| ok["тЬЕ 200 ├Ч 5"]
    b -->|"empty"| no["тП│ 429 ├Ч 3<br/>Too Many Requests"]
    key["ЁЯФС x-api-key: key-partner-1"] --> plan["ЁЯУЛ usage plan partner-basic<br/>2 rps ┬╖ burst 2 ┬╖ 1,000/day"]
    plan -->|"over the rate"| r["429 Too Many Requests"]
    plan -->|"over the quota"| q["429 Limit Exceeded"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/apigateway/lesson-diagrams.html#l07

тЭУ What

ЁЯдФ Why

Because one runaway client тАФ a loop in a mobile app, a partner's batch job тАФ can fill the shared account limit and take down every API in the Region. Limits at the door keep the kitchen alive and share capacity fairly. Usage plans turn "a partner" into numbers you can agree on in a contract: so many per second, so many per day.

ЁЯФз How (in this repo)

TokenBucket in apigw/gateway.py is nine lines: take(now) adds (now - t) * rate tokens (at most burst) and takes one if it can. Each stage gets its own bucket (dev: 5 and 5; prod: the defaults RATE = 10_000, BURST = 5_000). Gateway.usage_plan() makes a per-plan bucket and a quota and maps API keys to it. For routes with api_key=True, _handle() checks the key (unknown тЖТ 403), the quota (429 Limit Exceeded) and the plan's bucket (429 Too Many Requests) тАФ before the stage bucket.

ЁЯзк Try it

python3 apigw/demo.py throttle
python3 - <<'EOF'
import sys; sys.path.insert(0, "apigw"); from demo import office, Clock
from gateway import TokenBucket
b = TokenBucket(rate=5, burst=5, now=0)
print("t=0.0 ", [b.take(0) for _ in range(6)])
print("t=0.2 ", [b.take(0.2) for _ in range(2)])
print("t=10  ", b.take(10), "tokens left:", b.tokens)
c = Clock(); gw, _ = office(c)
gw.usage_plan("trial", rate=100, burst=100, quota_per_day=3, keys=["key-trial"])
print([gw.handle("GET", "/partner/students/9", {"x-api-key": "key-trial"})[0] for _ in range(5)], gw.handle("GET", "/partner/students/9", {"x-api-key": "key-trial"})[2])
print("no key:", gw.handle("GET", "/partner/students/9")[0::2])
EOF

тЬЕ Verify тАФ what you should see

throttle prints stage dev: rate 5/s, burst 5 тАФ 8 requests in the same instant: then 200 200 200 200 200 429 429 429, then one second later: 200 200 200 200 200 429. For the usage plan: 200 200 429 429 ┬╖ unknown key тЖТ 403.

Your snippet prints t=0.0 [True, True, True, True, True, False] (five tokens, then empty), t=0.2 [True, False] (0.2 s ├Ч 5/s = one new token), t=10 True tokens left: 4 (the bucket never holds more than 5, even after ten quiet seconds), then [200, 200, 200, 429, 429] {'message': 'Limit Exceeded'} for the trial quota of 3, and no key: (403, {'message': 'Forbidden'}).

ЁЯПБ What you just proved

You can predict every 429: count the tokens in the bucket, add the refill, and check the quota. The burst decides the first moment; the rate decides every second after.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ stage limits, then a usage plan for a partner with a key:

aws apigateway update-stage --rest-api-id abc123 --stage-name prod --patch-operations \
    op=replace,path=/*/*/throttling/rateLimit,value=500 \
    op=replace,path=/*/*/throttling/burstLimit,value=1000
aws apigateway create-usage-plan --name partner-basic \
    --throttle burstLimit=2,rateLimit=2 --quota limit=1000,period=DAY \
    --api-stages apiId=abc123,stage=prod
aws apigateway create-api-key --name partner-1 --enabled
aws apigateway create-usage-plan-key --usage-plan-id pl4n01 --key-id k3y001 --key-type API_KEY

Terraform:

resource "aws_api_gateway_usage_plan" "partner_basic" {
  name = "partner-basic"
  api_stages {
    api_id = aws_api_gateway_rest_api.school.id
    stage  = aws_api_gateway_stage.prod.stage_name
  }
  throttle_settings {
    rate_limit  = 2
    burst_limit = 2
  }
  quota_settings {
    limit  = 1000
    period = "DAY"
  }
}

ЁЯПн Why this matters in production: set stage limits below what the back end can really handle (measure it), give every partner a usage plan, and alarm on 429s so you see a limit before a customer reports it.

тПня╕П Next

The crowd is under control. Now answer the same question without bothering the kitchen: the notice board тАФ caching.

git checkout lesson-08-caching
тЖР PreviousauthNext тЖТcaching

This page is the lesson's README from the lesson-07-throttling branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.