ЁЯПл The SchoolтА║ЁЯУР System DesignтА║ЁЯУм рдзрдбрд╛ 07 тАФ Queues рдЖрдгрд┐ async рдХрд╛рдо: рдкрдЯрдХрди рдЙрддреНрддрд░ рджреНрдпрд╛, рд╕рд╛рд╡рдХрд╛рд╢ рдХрд╛рдо рдирдВрддрд░ рдХрд░рд╛
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯУм рдзрдбрд╛ 07 тАФ Queues рдЖрдгрд┐ async рдХрд╛рдо: рдкрдЯрдХрди рдЙрддреНрддрд░ рджреНрдпрд╛, рд╕рд╛рд╡рдХрд╛рд╢ рдХрд╛рдо рдирдВрддрд░ рдХрд░рд╛

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


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

рдзрдбреЗ 01тАУ06, рдЖрдгрд┐ queue. рдПрдХ рддрд╛рддрдбреАрдЪреА рд╕реВрдЪрдирд╛ 5 million рдкрд╛рд▓рдХрд╛рдВрдкрд░реНрдпрдВрдд SMS рдиреЗ рдкреЛрд╣реЛрдЪрд╛рдпрд▓рд╛ рд╣рд╡реА тАФ рддреЗ рдХрд╛рдо рд╢рд┐рдХреНрд╖рд┐рдХреЗрдЪреНрдпрд╛ request рдЪреНрдпрд╛ рдЖрдд рдЪрд╛рд▓рддрд╛ рдХрд╛рдорд╛ рдирдпреЗ. Request рд╕реВрдЪрдирд╛ save рдХрд░рддреЛ, queue рд╡рд░ рдПрдХ job рдареЗрд╡рддреЛ рдЖрдгрд┐ 201 рдЙрддреНрддрд░ рджреЗрддреЛ. Workers jobs рдШреЗрддрд╛рдд, batches рдордзреНрдпреЗ рдкрд╛рдард╡рддрд╛рдд, backoff рд╕рд╣ retry рдХрд░рддрд╛рдд рдЖрдгрд┐ рдЕрдкрдпрд╢реА jobs dead-letter queue (DLQ) рдордзреНрдпреЗ рдареЗрд╡рддрд╛рдд. Delivery at-least-once рдЕрд╕рддреЗ, рдореНрд╣рдгреВрди рдкреНрд░рддреНрдпреЗрдХ consumer idempotent рдЕрд╕рд╛рдпрд▓рд╛ рд╣рд╡рд╛. design/demo.py рдордзреАрд▓ queues() workers рдЪреА рд╕рдВрдЦреНрдпрд╛ рд╡реЗрдЧ рдХрд╕рд╛ рдард░рд╡рддреЗ рддреЗ рджрд╛рдЦрд╡рддреЗ.

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

рдПрдХ рд╢рд┐рдХреНрд╖рд┐рдХрд╛ рддрд╛рддрдбреАрдЪреА рд╕реВрдЪрдирд╛ рдШреЗрдКрди рдпреЗрддреЗ: "рдЙрджреНрдпрд╛ рд╢рд╛рд│рд╛ рдмрдВрдж тАФ рдкреНрд░рддреНрдпреЗрдХ рдкрд╛рд▓рдХрд╛рд▓рд╛ рдХрд│рд╡рд╛." рдЦрд┐рдбрдХреАрд╡рд░рдЪреНрдпрд╛ рдХрд╛рд░рдХреБрдирд╛рдиреЗ рд╕реНрд╡рддрдГрдЪ 5 million рдкрд╛рд▓рдХрд╛рдВрдирд╛ рдлреЛрди рдХреЗрд▓реЗ, рддрд░ рд╢рд┐рдХреНрд╖рд┐рдХрд╛ рджрд┐рд╡рд╕реЗрдВрджрд┐рд╡рд╕ рдЦрд┐рдбрдХреАрд╡рд░ рдерд╛рдВрдмреВрди рд░рд╛рд╣реАрд▓, рдЖрдгрд┐ рддрд┐рдЪреНрдпрд╛ рдорд╛рдЧрдЪреА рд░рд╛рдВрдЧ рдлрд╛рдЯрдХрд╛рдмрд╛рд╣реЗрд░ рдЬрд╛рдИрд▓.

рдореНрд╣рдгреВрди рджреАрдкрд┐рдХрд╛ рдЦрд┐рдбрдХреАрд╡рд░ рдПрдХ рдЯреНрд░реЗ ЁЯУе рдареЗрд╡рддреЗ. рдХрд╛рд░рдХреВрди рдПрдХ рдЪрд┐рдареНрдареА рд▓рд┐рд╣рд┐рддреЗ тАФ "рд╕реВрдЪрдирд╛ 42 рдмрджреНрджрд▓ рдкреНрд░рддреНрдпреЗрдХ рдкрд╛рд▓рдХрд╛рд▓рд╛ рдлреЛрди рдХрд░рд╛" тАФ рддреА рдЯреНрд░реЗрдордзреНрдпреЗ рдЯрд╛рдХрддреЗ, рдЖрдгрд┐ рд╢рд┐рдХреНрд╖рд┐рдХреЗрд▓рд╛ рд╕рд╛рдВрдЧрддреЗ: "рдЭрд╛рд▓реЗ, рддреЗ рдорд╛рд░реНрдЧрд╛рд╡рд░ рдЖрд╣реЗ." рд╢рд┐рдХреНрд╖рд┐рдХрд╛ рдПрдХрд╛ рдорд┐рдирд┐рдЯрд╛рдд рдирд┐рдШреВрди рдЬрд╛рддреЗ.

рдорд╛рдЧрдЪреНрдпрд╛ рдЦреЛрд▓реАрдд, рдорджрддрдиреАрд╕ рдЯреНрд░реЗрдордзреВрди рдЪрд┐рдареНрдареНрдпрд╛ рдШреЗрддрд╛рдд рдЖрдгрд┐ рдлреЛрди рдХрд░рддрд╛рдд. рдЬрд╛рд╕реНрдд рдорджрддрдиреАрд╕, рдЬрд▓рдж рдлреЛрди. рдПрдЦрд╛рджреА рдорджрддрдиреАрд╕ рдЪрд┐рдареНрдареА рдЕрд░реНрдзрд╡рдЯ рдЕрд╕рддрд╛рдирд╛ рдЪрдХреНрдХрд░ рдпреЗрдКрди рдкрдбрд▓реА, рддрд░ рдереЛрдбреНрдпрд╛ рд╡реЗрд│рд╛рдиреЗ рддреА рдЪрд┐рдареНрдареА рдкрд░рдд рдЯреНрд░реЗрдордзреНрдпреЗ рдЬрд╛рддреЗ, рдЖрдгрд┐ рджреБрд╕рд░реА рдорджрддрдиреАрд╕ рддреА рдШреЗрддреЗ тАФ рдореНрд╣рдгреВрди рдХрд╛рд╣реА рдкрд╛рд▓рдХрд╛рдВрдирд╛ рдлреЛрди рджреЛрдирджрд╛ рдпреЗрдК рд╢рдХрддреЛ. рдкреНрд░рддреНрдпреЗрдХ рдорджрддрдиреАрд╕ рдЖрдзреА рдПрдХ рдпрд╛рджреА рддрдкрд╛рд╕рдд рдЕрд╕реЗрд▓ рддрд░ рд╣реЗ рдареАрдХ рдЖрд╣реЗ: "рд╕реВрдЪрдирд╛ 42 рдмрджреНрджрд▓ рдпрд╛ рдкрд╛рд▓рдХрд╛рд▓рд╛ рдЖрдзреАрдЪ рдХреЛрдгреА рдлреЛрди рдХреЗрд▓рд╛ рдЖрд╣реЗ рдХрд╛?"

рдкреБрдиреНрд╣рд╛ рдкреБрдиреНрд╣рд╛ рдЕрдкрдпрд╢реА рд╣реЛрдгрд╛рд░реА рдЪрд┐рдареНрдареА (рдЪреБрдХреАрдЪрд╛ рдирдВрдмрд░) рдХрд╛рдпрдордЪреА рдлрд┐рд░рдд рд░рд╛рд╣рдд рдирд╛рд╣реА. рддреА рдПрдЦрд╛рджреНрдпрд╛ рд╡реНрдпрдХреНрддреАрдиреЗ рдкрд╛рд╣рд╛рд╡реА рдореНрд╣рдгреВрди рдмрд╛рдЬреВрдЪреНрдпрд╛ рдЯреНрд░реЗрдордзреНрдпреЗ рдЬрд╛рддреЗ.

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

flowchart LR
    t["тЬНя╕П teacher: POST urgent notice"] --> api["ЁЯФМ API<br/>save notice + 1 job<br/>тЖТ 201 in ~50 ms"]
    api --> q[["ЁЯУм queue"]]
    q --> w1["ЁЯС╖ worker"]
    q --> w2["ЁЯС╖ worker"]
    q --> w3["ЁЯС╖ тАж 50 workers"]
    w1 --> sms["ЁЯУ▒ SMS provider<br/>batches of 500"]
    w2 --> sms
    w3 --> sms
    w1 -.->|"no 'done' in time тЖТ visible again"| q
    q -.->|"failed 5 times"| dlq[["ЁЯз║ DLQ"]]
    note["50 workers ├Ч 50 SMS/s<br/>тЖТ 5M in 33.3 min"]

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

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

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг рдХрд╛рдЧрдж (рдзрдбрд╛ 01) рдореНрд╣рдгрддреЛ "post p99 < 500 ms" рдЖрдгрд┐ "рддрд╛рддрдбреАрдЪреНрдпрд╛ рд╕реВрдЪрдирд╛рдВрд╕рд╛рдареА рдкрд╛рд▓рдХрд╛рд▓рд╛ notification рдорд┐рд│рддреЗ". Request рдордзреНрдпреЗ 5 million SMS рдкрд╛рдард╡рд▓реНрдпрд╛рдиреЗ рдкрд╣рд┐рд▓реЗ рд╡рдЪрди рдореЛрдбрддреЗ. Queue рджреЛрдиреНрд╣реА рдкрд╛рд│рддреЗ: рд╢рд┐рдХреНрд╖рд┐рдХреЗрдЪрд╛ request рдЬрд▓рдж, рдЖрдгрд┐ рдкрд╛рдард╡рдгреЗ рддреБрдореНрд╣реА рдкреИрд╕реЗ рджреЗрддрд╛ рддрд┐рддрдХреНрдпрд╛ workers рдЗрддрдХреЗ рдЬрд▓рдж. рддреЗ рдЕрдкрдпрд╢ рд╡реЗрдЧрд│реЗ рдареЗрд╡рддреЗ: SMS provider 10 рдорд┐рдирд┐рдЯреЗ рдмрдВрдж рдЕрд╕рд▓рд╛, рддрд░ jobs queue рдордзреНрдпреЗ рдерд╛рдВрдмрддрд╛рдд; рд╕реВрдЪрдирд╛ post рдХрд░рдгреЗ рдЪрд╛рд▓реВрдЪ рд░рд╛рд╣рддреЗ.

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

рдпрд╛ рдзрдбреНрдпрд╛рд╕рд╛рдареА lab рдордзреНрдпреЗ queue class рдирд╛рд╣реА тАФ model рдореНрд╣рдгрдЬреЗ рдЧрдгрд┐рдд, рдЖрдгрд┐ рддреЛрдЪ рдореБрджреНрджрд╛ рдЖрд╣реЗ: рд╡реЗрд│ = jobs ├╖ (workers ├Ч рдкреНрд░рддреНрдпреЗрдХ worker рдЪрд╛ рд╡реЗрдЧ). design/demo.py рдордзреАрд▓ queues() рдкреНрд░рддреНрдпреЗрдХреА рд╕реЗрдХрдВрджрд╛рд▓рд╛ 50 SMS рдпрд╛ рд╡реЗрдЧрд╛рдиреЗ 10, 50 рдЖрдгрд┐ 200 workers рд╕рд╛рдареА рддреЗ рдЫрд╛рдкрддреЗ. Snippet рд╡реЗрдЧ рдмрджрд▓рддреЛ, jobs рдмрд╛рд╣реЗрд░ рдЬрд╛рдгреНрдпрд╛рдкреЗрдХреНрд╖рд╛ рдЬрд▓рдж рдпреЗрддрд╛рдд рдЕрд╕реЗ рдПрдХ рдЧрд░реНрджреАрдЪреЗ рдорд┐рдирд┐рдЯ рдЬреЛрдбрддреЛ (backlog), рдЖрдгрд┐ рдПрдХрдЪ job рджреЛрдирджрд╛ рдорд┐рд│рдгрд╛рд░рд╛ рдПрдХ idempotent consumer рдЪрд╛рд▓рд╡рддреЛ.

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

python3 design/demo.py queues
python3 - <<'EOF'
jobs = 5_000_000
for workers, rate in ((50, 50), (50, 25), (100, 50), (50, 5)):
    print(f"{workers:>3} workers ├Ч {rate:>2} SMS/s тЖТ {jobs / (workers * rate) / 60:6.1f} min")
arrive, drain = 3_000, 2_500           # per second, for one busy minute
print(f"backlog after 60 s: {(arrive - drain) * 60:,} jobs waiting ┬╖ cleared {(arrive - drain) * 60 / drain:.0f} s after the rush")
# at-least-once: message 2 is delivered twice (the worker crashed before it said 'done')
deliveries = ["sms-1", "sms-2", "sms-2", "sms-3"]
sent, done = [], set()
for m in deliveries:
    if m in done: continue                  # idempotent consumer: already handled
    sent.append(m); done.add(m)
print("delivered", len(deliveries), "┬╖ sent", len(sent), sent)
EOF

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

queues рдЫрд╛рдкрддреЗ:

тФАтФА 'send 5M urgent SMS' must not run inside the request
   request: save the notice, put ONE job on a queue, answer 201 in ~50 ms
   workers: take jobs, send in batches of 500, retry with backoff, park failures in a DLQ
    10 workers ├Ч 50 SMS/s тЖТ all 5M sent in  166.7 min
    50 workers ├Ч 50 SMS/s тЖТ all 5M sent in   33.3 min
   200 workers ├Ч 50 SMS/s тЖТ all 5M sent in    8.3 min
   queues decouple speed: the request is fast, the work is as fast as you pay for ┬╖ delivery is at-least-once

рддреБрдордЪрд╛ snippet рдЫрд╛рдкрддреЛ:

 50 workers ├Ч 50 SMS/s тЖТ   33.3 min
 50 workers ├Ч 25 SMS/s тЖТ   66.7 min
100 workers ├Ч 50 SMS/s тЖТ   16.7 min
 50 workers ├Ч  5 SMS/s тЖТ  333.3 min
backlog after 60 s: 30,000 jobs waiting ┬╖ cleared 12 s after the rush
delivered 4 ┬╖ sent 3 ['sms-1', 'sms-2', 'sms-3']

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

рд╢рд┐рдХреНрд╖рд┐рдХреЗрд▓рд╛ рдЙрддреНрддрд░ рдорд┐рд│рдгреНрдпрд╛рдЪреА рд╡реЗрд│ рдпрд╛рдВрдкреИрдХреА рдХреЛрдгрддреНрдпрд╛рдЪ рдЖрдХрдбреНрдпрд╛рд╡рд░ рдЕрд╡рд▓рдВрдмреВрди рдирд╛рд╣реА тАФ рддреЛ рдПрдХ queue write рдЖрд╣реЗ. рдХрд╛рдорд╛рдЪреА рд╡реЗрд│ рдЕрд╡рд▓рдВрдмреВрди рдЖрд╣реЗ: рд╡реЗрдЧ рдирд┐рдореНрдорд╛ рдХрд░рд╛ рдЖрдгрд┐ рддреА рджреБрдкреНрдкрдЯ рд╣реЛрддреЗ; workers рджреБрдкреНрдкрдЯ рдХрд░рд╛ рдЖрдгрд┐ рддреА рдирд┐рдореНрдореА рд╣реЛрддреЗ; рд╣рд│реВ provider (5 SMS/s) 33 рдорд┐рдирд┐рдЯрд╛рдВрдЪреЗ 5.5 рддрд╛рд╕ рдХрд░рддреЛ. рдЧрд░реНрджреА рдПрдХ backlog рдорд╛рдЧреЗ рдареЗрд╡рддреЗ рдЬреЛ рдЧрд░реНрджреАрдЪреНрдпрд╛ рд╡реЗрд│реА requests рдЕрдкрдпрд╢реА рдХрд░рдгреНрдпрд╛рдРрд╡рдЬреА рдЧрд░реНрджреА рдирдВрддрд░ рд░рд┐рдХрд╛рдорд╛ рд╣реЛрддреЛ. рдЖрдгрд┐ рджреЛрдирджрд╛ рдкреЛрд╣реЛрдЪрд▓реЗрд▓рд╛ job рдПрдХрджрд╛рдЪ рдкрд╛рдард╡рд▓рд╛ рдЧреЗрд▓рд╛, рдХрд╛рд░рдг consumer рдиреЗ рдЖрдзреА рддрдкрд╛рд╕рд▓реЗ.

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

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

On a real account тАФ 5 рдкреНрд░рдпрддреНрдирд╛рдВрдирдВрддрд░ DLQ рдЕрд╕рд▓реЗрд▓реА, рдЖрдгрд┐ рдПрдХрд╛ batch рдкреЗрдХреНрд╖рд╛ рдЬрд╛рд╕реНрдд visibility timeout рдЕрд╕рд▓реЗрд▓реА SQS queue:

aws sqs create-queue --queue-name urgent-sms-dlq \
    --attributes MessageRetentionPeriod=1209600
aws sqs create-queue --queue-name urgent-sms --attributes '{
  "VisibilityTimeout": "120",
  "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:ap-south-1:123456789012:urgent-sms-dlq\",\"maxReceiveCount\":\"5\"}"
}'

Batch рдордзрд▓реЗ рдлрдХреНрдд рдЕрдкрдпрд╢реА messages рдХрд│рд╡рдгрд╛рд░рд╛ Lambda consumer (рдореНрд╣рдгрдЬреЗ рдЪрд╛рдВрдЧрд▓реЗ messages рдкреБрдиреНрд╣рд╛ retry рд╣реЛрдд рдирд╛рд╣реАрдд), SMS provider рдЪреА рдорд░реНрдпрд╛рджрд╛ рдкрд╛рд│рдгреНрдпрд╛рд╕рд╛рдареА concurrency рд╡рд░ рдХрдорд╛рд▓ рдорд░реНрдпрд╛рджреЗрд╕рд╣:

aws lambda create-event-source-mapping --function-name send-sms \
    --event-source-arn arn:aws:sqs:ap-south-1:123456789012:urgent-sms \
    --batch-size 10 --function-response-types ReportBatchItemFailures \
    --scaling-config MaximumConcurrency=50

Idempotency рддрдкрд╛рд╕рдгреА, database рдордзреАрд▓ unique key рдЪреНрдпрд╛ рд░реВрдкрд╛рдд:

CREATE TABLE sms_sent (notice_id bigint, parent_id bigint, PRIMARY KEY (notice_id, parent_id));
INSERT INTO sms_sent VALUES ($1, $2) ON CONFLICT DO NOTHING;   -- 0 rows inserted тЖТ already sent, skip

рдЕрдзрд┐рдХ рдЦреЛрд▓рд╛рдд: Scaling рд╢рд╛рд│рд╛, рдзрдбреЗ 12 (SQS, backlog рдиреБрд╕рд╛рд░ рд╡рд╛рдврдгрд╛рд░реЗ workers) рдЖрдгрд┐ 13 (idempotency, DLQs).

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдлрдХреНрдд queue рдЪреНрдпрд╛ рд▓рд╛рдВрдмреАрд╡рд░ рдирд╛рд╣реА, рддрд░ ApproximateAgeOfOldestMessage рд╡рд░ alarm рд▓рд╛рд╡рд╛. "рд╕рд░реНрд╡рд╛рдд рдЬреБрдирд╛ рддрд╛рддрдбреАрдЪрд╛ SMS 20 рдорд┐рдирд┐рдЯреЗ рдерд╛рдВрдмрд▓рд╛ рдЖрд╣реЗ" рд╣рд╛ рдкрд╛рд▓рдХрд╛рд▓рд╛ рдЬрд╛рдгрд╡рдгрд╛рд░рд╛ рдЖрдХрдбрд╛ рдЖрд╣реЗ.

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

рдПрдХ рд╕реВрдЪрдирд╛, рдЖрдгрд┐ рдЖрддрд╛ рдЪрд╛рд░ jobs рд╡реНрд╣рд╛рдпрд▓рд╛ рд╣рд╡реЗрдд: SMS, email, search index, analytics. API рдиреЗ рдкреНрд░рддреНрдпреЗрдХрд╛рд▓рд╛ call рдХрд░рд╛рд╡рд╛ рдХрд╛? рдирд┐рдпреЛрдЬрди рдХрд╛рд░реНрдпрд╛рд▓рдп рдПрдХрд╛ рдШрдЯрдиреЗрдЪреА рдШреЛрд╖рдгрд╛ рдХрд░рддреЗ рдЖрдгрд┐ рдкреНрд░рддреНрдпреЗрдХ рдРрдХрдгрд╛рд▒реНрдпрд╛рд▓рд╛ рдкреНрд░рддрд┐рд╕рд╛рдж рджреЗрдК рджреЗрддреЗ тАФ рддреА рдХрдзреАрд╣реА рди рдЧрдорд╛рд╡рддрд╛.

git checkout lesson-08-events

ЁЯУм Lesson 07 тАФ Queues & async work: answer fast, do the slow work later

ЁЯУН You are here: Lesson 07 of 18 ┬╖ Previous: lesson-06-load-balancing ┬╖ Next: lesson-08-events


ЁЯУж What's in this branch

Lessons 01тАУ06, plus the queue. An urgent notice must reach 5 million parents by SMS тАФ that work must not run inside the teacher's request. The request saves the notice, puts one job on a queue and answers 201. Workers take jobs, send in batches, retry with backoff and park failures in a dead-letter queue (DLQ). Delivery is at-least-once, so every consumer must be idempotent. queues() in design/demo.py shows how the number of workers sets the speed.

ЁЯзТ Explain like I'm 5

A teacher brings an urgent notice: "School closed tomorrow тАФ tell every parent." If the clerk at the counter phones 5 million parents herself, the teacher waits at the counter for days, and the queue behind her goes out of the gate.

So Dipika puts a tray ЁЯУе on the counter. The clerk writes one slip тАФ "phone every parent about notice 42" тАФ drops it in the tray, and says to the teacher: "Done, it is on its way." The teacher leaves after one minute.

In the back room, helpers take slips from the tray and make the calls. More helpers, faster calls. If a helper faints halfway through a slip, the slip goes back into the tray after a while, and another helper takes it тАФ so a few parents may get the call twice. That is fine if every helper first checks a list: "did someone already call this parent about notice 42?"

A slip that fails again and again (a wrong number) does not go round forever. It goes to a side tray for a person to look at.

ЁЯЧ║я╕П Diagram

flowchart LR
    t["тЬНя╕П teacher: POST urgent notice"] --> api["ЁЯФМ API<br/>save notice + 1 job<br/>тЖТ 201 in ~50 ms"]
    api --> q[["ЁЯУм queue"]]
    q --> w1["ЁЯС╖ worker"]
    q --> w2["ЁЯС╖ worker"]
    q --> w3["ЁЯС╖ тАж 50 workers"]
    w1 --> sms["ЁЯУ▒ SMS provider<br/>batches of 500"]
    w2 --> sms
    w3 --> sms
    w1 -.->|"no 'done' in time тЖТ visible again"| q
    q -.->|"failed 5 times"| dlq[["ЁЯз║ DLQ"]]
    note["50 workers ├Ч 50 SMS/s<br/>тЖТ 5M in 33.3 min"]

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

тЭУ What

ЁЯдФ Why

Because the sheet (lesson 01) says "post p99 < 500 ms" and "a parent gets a notification for urgent notices". Sending 5 million SMS in the request breaks the first promise. The queue keeps both: the teacher's request is fast, and the sending is as fast as the number of workers you pay for. It also isolates failure: if the SMS provider is down for 10 minutes, jobs wait in the queue; posting notices keeps working.

ЁЯФз How (in this repo)

The lab has no queue class for this lesson тАФ the model is arithmetic, and that is the point: time = jobs ├╖ (workers ├Ч rate per worker). queues() in design/demo.py prints it for 10, 50 and 200 workers at 50 SMS a second each. The snippet changes the rate, adds a busy minute where jobs arrive faster than they leave (a backlog), and runs an idempotent consumer that receives one job twice.

ЁЯзк Try it

python3 design/demo.py queues
python3 - <<'EOF'
jobs = 5_000_000
for workers, rate in ((50, 50), (50, 25), (100, 50), (50, 5)):
    print(f"{workers:>3} workers ├Ч {rate:>2} SMS/s тЖТ {jobs / (workers * rate) / 60:6.1f} min")
arrive, drain = 3_000, 2_500           # per second, for one busy minute
print(f"backlog after 60 s: {(arrive - drain) * 60:,} jobs waiting ┬╖ cleared {(arrive - drain) * 60 / drain:.0f} s after the rush")
# at-least-once: message 2 is delivered twice (the worker crashed before it said 'done')
deliveries = ["sms-1", "sms-2", "sms-2", "sms-3"]
sent, done = [], set()
for m in deliveries:
    if m in done: continue                  # idempotent consumer: already handled
    sent.append(m); done.add(m)
print("delivered", len(deliveries), "┬╖ sent", len(sent), sent)
EOF

тЬЕ Verify тАФ what you should see

queues prints:

тФАтФА 'send 5M urgent SMS' must not run inside the request
   request: save the notice, put ONE job on a queue, answer 201 in ~50 ms
   workers: take jobs, send in batches of 500, retry with backoff, park failures in a DLQ
    10 workers ├Ч 50 SMS/s тЖТ all 5M sent in  166.7 min
    50 workers ├Ч 50 SMS/s тЖТ all 5M sent in   33.3 min
   200 workers ├Ч 50 SMS/s тЖТ all 5M sent in    8.3 min
   queues decouple speed: the request is fast, the work is as fast as you pay for ┬╖ delivery is at-least-once

Your snippet prints:

 50 workers ├Ч 50 SMS/s тЖТ   33.3 min
 50 workers ├Ч 25 SMS/s тЖТ   66.7 min
100 workers ├Ч 50 SMS/s тЖТ   16.7 min
 50 workers ├Ч  5 SMS/s тЖТ  333.3 min
backlog after 60 s: 30,000 jobs waiting ┬╖ cleared 12 s after the rush
delivered 4 ┬╖ sent 3 ['sms-1', 'sms-2', 'sms-3']

ЁЯПБ What you just proved

The teacher's answer time does not depend on any of these numbers тАФ it is one queue write. The work time does: halve the rate and it doubles; double the workers and it halves; a slow provider (5 SMS/s) turns 33 minutes into 5.5 hours. A burst leaves a backlog that drains after the rush instead of failing requests during it. And a job delivered twice was sent once, because the consumer checked first.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ an SQS queue with a DLQ after 5 attempts and a visibility timeout longer than one batch:

aws sqs create-queue --queue-name urgent-sms-dlq \
    --attributes MessageRetentionPeriod=1209600
aws sqs create-queue --queue-name urgent-sms --attributes '{
  "VisibilityTimeout": "120",
  "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:ap-south-1:123456789012:urgent-sms-dlq\",\"maxReceiveCount\":\"5\"}"
}'

A Lambda consumer that reports only the failed messages of a batch (so the good ones are not retried), with a ceiling on concurrency to respect the SMS provider's limit:

aws lambda create-event-source-mapping --function-name send-sms \
    --event-source-arn arn:aws:sqs:ap-south-1:123456789012:urgent-sms \
    --batch-size 10 --function-response-types ReportBatchItemFailures \
    --scaling-config MaximumConcurrency=50

The idempotency check, as a unique key in the database:

CREATE TABLE sms_sent (notice_id bigint, parent_id bigint, PRIMARY KEY (notice_id, parent_id));
INSERT INTO sms_sent VALUES ($1, $2) ON CONFLICT DO NOTHING;   -- 0 rows inserted тЖТ already sent, skip

Go deeper: the Scaling school, lessons 12 (SQS, workers that scale on backlog) and 13 (idempotency, DLQs).

ЁЯПн Why this matters in production: alarm on ApproximateAgeOfOldestMessage, not only on queue length. "The oldest urgent SMS has waited 20 minutes" is the number a parent feels.

тПня╕П Next

One notice, and now four jobs want to happen: SMS, email, the search index, analytics. Should the API call each one? The planning office announces a fact and lets each listener react тАФ without ever losing it.

git checkout lesson-08-events
тЖР Previousload balancingNext тЖТevents

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