~learn.

Side quest 28Email at scale

Gmail.

A Billion and a Half Mailboxes, Fifteen Billion Unwanted Emails a Day, and Mail That Must Never Be Lost

ADVANCED48 MIN READ6 PHASES · 45 MIN INTERVIEW+120 XP

Mission brief

Design an email service for billions of mailboxes

Quest log0/7 stages · 0/9 checkpoints · 0/8 quick checks
people using Gmail every month (Google, February 2019)
1.5 billion
unwanted emails blocked every day; more than 99.9% of spam, phishing and malware kept out of inboxes (Google, October 2023)
Nearly 15 billion
the line above which senders to Gmail must use SPF, DKIM and DMARC and one-click unsubscribe (Gmail Help, from February 2024)
5,000 a day:
per account, shared across Gmail, Drive and Photos (Google One Help)
15 GB

Company names and logos are trademarks of their owners, shown only to identify each company. This course isn't affiliated with or endorsed by any of them.

watch it first · newThe whole design in a 16-minute animated lecture4 quiz checkpoints · ask the AI about any moment · captions, chapters and speed
Optional · timed run · 6 phases · 45 min

Want it harder? Hide the model answer and work each phase in a scratchpad against its time budget, then reveal and score yourself. Or simply read on: the checkpoints below work either way.

Intro · 3 min

How Do You Say "Yes, I Have It" to the Whole Internet, and Never Be Wrong?

It is the evening of 31 July in Pune, the usual last day to file income tax returns. Meera Iyer, a chartered accountant, has eleven returns left. At 9:40 pm a client in Nashik, Ravi Kulkarni, finally emails his Form 16 and bank statements: a 19 MB scan, from his office account at an engineering firm whose mail is hosted elsewhere. Seconds later another message arrives: the Income Tax Department has approved a refund of ₹18,450, and she should "verify her bank account" through a link. Its domain was registered two days ago, one letter off the real tax portal's name.

At 9:55 she searches twelve years of mail for last year's return of another client. Her account is at 14.6 GB of its 15 GB. And that morning her firm sent a filing reminder to 6,200 clients, about 5,400 of them on Gmail, which made it, for one day, a bulk sender. The service has to take Ravi's 19 MB from a server it has never seen, keep the fake refund out of her inbox, keep the scan for ten years, find the old PDF in a second, and decide whether her firm's own reminder deserves an inbox at all.

Gmail, from published numbers
  • 1.5 billion — people using Gmail every month (Google Workspace blog, 7 February 2019)
  • Nearly 15 billion — unwanted emails Gmail blocks every day; "more than 99.9% of spam, phishing and malware" kept from inboxes (Google, 3 October 2023)
  • Under 0.1% / under 0.05% — spam in the average Gmail inbox, and wanted mail landing in the spam folder (Gmail blog, July 2015)
  • 38%, 19.4%, 83% — better spam detection, fewer false positives and less TPU use after Gmail's classifier moved to RETVec (Google Security Blog, November 2023)
  • 5,000 messages a day — above it, senders to Gmail need SPF, DKIM, DMARC and one-click unsubscribe (Gmail Help, from 1 February 2024)
  • 0.02% — of Gmail users whose mail a storage software bug deleted from several copies in February 2011; Google restored it from tape (Gmail blog)

For context:
Google has published Gmail's limits, sender rules and spam results, and that Gmail uses Colossus and Spanner; not its internal architecture
Google doesn't publish daily message counts; every rate below is a labelled assumption
Everything not in the list above is an illustrative design, not Gmail's internals

This is the system we'll design today.

ExplainerThe whole quest: Gmail

Google said 1.5 billion people use Gmail every month and that it blocks nearly 15 billion unwanted emails a day, keeping more than 99.9% of spam, phishing and malware out of inboxes. On stated assumptions that is ~60 billion messages a day, ~1.4 million a second and ~830 Gbps at peak, and ~3.75 EB of stored mail: ~11 EB of disk at three copies, ~5.9 EB tiered. The SMTP edge refuses cheaply (IP reputation, recipient, size, SPF and DKIM), writes each message to a queue in 3 zones and waits for 2 before answering 250 OK, and answers 451 under load so senders keep the mail. The filter runs after acceptance as a funnel, cheap signals first, models on the rest; Gmail's move to RETVec raised detection 38%, cut false positives 19.4% and TPU use 83%. DMARC ties SPF or DKIM to the visible From:, and since 2024 senders of more than 5,000 a day need it and one-click unsubscribe. Everything is partitioned by mailbox: metadata, a ~66 MB search index, quotas and limits; bodies are blobs stored once by hash, erasure-coded as they age, with an offline copy because a bug runs on every replica, as in 2011. The deep dives end with a deadline evening, a mailing list that breaks DKIM and a bad storage release.

The Interview Begins#

You're interviewing at a company that runs email for millions of businesses. The interviewer pastes the raw headers of one message into the shared document. Near the top: Received: from mail.example-engineering.in, then Authentication-Results: spf=pass dkim=pass dmarc=pass, then X-Spam-Verdict: inbox. At the bottom, an attachment of 26 MB.

Interviewer

"Every line in that header is a decision someone's system made in the second before the user saw the message. Design the system that makes them: receiving, filtering, storing, searching, and the rules for who gets to send to us."

Design an email service for billions of mailboxes

Users get an address, receive mail from anyone on the internet over SMTP, read it on the web and in apps, organise it with labels, search all of it, and send mail out. The service must keep spam and phishing out of the inbox without losing real mail, keep every accepted message for as long as the user wants it, and enforce storage and rate limits per account.

Requirements
  • Receive mail over SMTP from any server on the internet; never lose a message once accepted
  • Classify every message: reject, spam folder, inbox with a warning, or inbox
  • Authenticate senders (SPF, DKIM, DMARC) and track their reputation
  • Store mail for years, cheaply, with deletes, labels and threads
  • Search one user's whole mailbox in under a second
  • Enforce storage quotas and sending and receiving limits per account
Interviewer

"Leave out the compose UI, calendar invites, chat and the AI features. Assume the client apps exist and talk to an API."

Phase 1 · Stage 1 of 7 · 5 min budget · 2 min read

Phase 1: Requirements Clarification (5 minutes)#

You

"Email is old, federated and hostile: the protocol is fixed, the senders are millions of servers we don't control, and much of what arrives is trying to trick someone. I'll ask about each."

Your Questions#

Transcript
You

"How many mailboxes?"

Interviewer

"Google wrote in February 2019 that 1.5 billion people use Gmail every month. It doesn't publish daily users or message counts."

You

"How much of what arrives is unwanted?"

Interviewer

"Google said in October 2023 that Gmail blocks nearly 15 billion unwanted emails a day and keeps more than 99.9% of spam, phishing and malware out of inboxes. In 2015 the Gmail team wrote that less than 0.1% of the mail in the average inbox is spam, and wanted mail in the spam folder is rarer still, under 0.05%."

You

"So the mistake of hiding wanted mail gets the tighter bar. How much does each user keep?"

Interviewer

"Up to 15 GB per Google Account, shared with Drive and Photos. Gmail launched on 1 April 2004 with 1 GB because, Google said, 'people should be able to hold onto their mail forever'. Over quota for two years, content may be removed."

You

"Who's allowed to send to us?"

Interviewer

"Anyone, under rules since February 2024: every sender needs SPF or DKIM, valid reverse DNS, TLS and a spam rate under 0.3%; senders of more than 5,000 messages a day to Gmail also need DMARC, an aligned From: domain and one-click unsubscribe."

You

"And limits per account?"

Interviewer

"A personal account hits a limit at about 500 emails a day, or 500 recipients in one email. A Workspace account can send 2,000 messages a day over a rolling 24 hours, and receive 60 a minute, 3,600 an hour and 86,400 a day."

You

"Last one: how durable?"

Interviewer

"In February 2011 a storage software update deleted several copies of mail at once for 0.02% of Gmail users; Google restored it from tape. Design so that a bug can't take every copy."

Functional Requirements#

Functional requirements
  • Receive: SMTP from any server; reject unknown recipients and abusive senders early; once accepted, deliver or report failure
  • Classify: reject, spam, inbox with a warning, or inbox; "Report spam" and "Not spam" feed back
  • Store: messages, threads, labels, read state for years; Trash kept 30 days
  • Search: every field and word of one mailbox, with operators (from:, has:attachment, older_than:)
  • Send: sign outgoing mail, deliver with retries, enforce sending limits
  • Quotas: storage per account; rates per account, per sending domain and per IP

Non-Functional Requirements#

Non-functional requirements
Scale (assumptions, built on published numbers)
  • 1.5 billion mailboxes receiving mail
  • ~60 billion inbound messages a day, ~700,000 a second on average (estimated below)
Latency (design targets)
  • SMTP accept p99 < 2 s after the last byte of the message
  • Accepted to visible in the inbox p99 < 10 s
  • Search p99 < 1 s for a mailbox of any size
Durability and correctness
  • Zero accepted messages lost, including to software bugs that reach every online copy
  • Wanted mail in spam < 0.05%; spam in the inbox < 0.1% (Gmail's 2015 figures as targets)

Phase 2 · Stage 2 of 7 · 5 min budget · 3 min read

Phase 2: Back of the Envelope Estimation (5 minutes)#

You

"I'll take Google's 1.5 billion monthly users as 1.5 billion mailboxes, anchor on 15 billion blocked messages a day, and label every other number as mine."

Your turn · checkpoint 1not answered

Before the numbers: how much mail arrives?

Assume each of the 1.5 billion mailboxes receives 30 wanted messages a day, newsletters and receipts included. Gmail blocks nearly 15 billion unwanted emails a day. How many messages arrive a day in total, how many a second on average, and what's a peak at twice the average? If an average message is 75 KB, how many bytes a second come in?

Think it through, or jot it down. Then compare.

One strong answer

  • Wanted: 1.5 billion × 30 = 45 billion a day.
  • Total: 45 + 15 = ~60 billion a day; ÷ 86,400 s ≈ ~700,000 a second, ~1.4 million at a 2× peak. Design for 2 million.
  • Bytes: 60 billion × 75 KB = 4.5 PB a day, ~52 GB/s, about 420 Gbps on average and ~830 Gbps at peak, before TLS and protocol overhead.
  • So a quarter of what the edge receives, on these assumptions, is mail nobody wants.

Inbound Mail#

WorksheetReceiving (every rate is an assumption: Google doesn't publish message counts)
Value
Mailboxes (taken as Gmail's monthly users, Google, 2019) 1.5 billion
Wanted messages per mailbox per day (assumed) 30 → 45 billion a day
Unwanted emails blocked (Google, 2023) nearly 15 billion a day
Inbound messages ~60 billion a day, ~700,000/s, ~1.4 million/s at peak
Average message size as received (assumed) 75 KB
Inbound bandwidth ~420 Gbps average, ~830 Gbps peak
Messages per SMTP connection (assumed) ~5 → ~140,000 new TLS connections a second on average

Conclusion: ~1.4 million messages a second at peak, a quarter of them hostile. The cheapest place to drop bad mail is before it's received at all.

Storage#

WorksheetWhat's kept (sizes are assumptions)
  • New mail kept: 45 billion × 75 KB ≈ 3.4 PB a day; if identical attachments and bodies are stored once (assume a quarter saved), ~2.5 PB a day, nearly 1 EB a year
  • Average Gmail data per mailbox (assumed): 2.5 GB → 1.5 billion × 2.5 GB ≈ 3.75 EB of mail
  • Messages: 2.5 GB ÷ 75 KB ≈ 33,000 per mailbox, ~50 trillion in all
  • Metadata (labels, thread, flags, header summary), ~500 bytes a message: ~25 PB
  • Raw disk at 3 full copies: 3.75 EB × 3 ≈ 11 EB; with recent mail at 3 copies and older mail erasure-coded at 1.5×: 30 days × 2.5 PB × 3 + 3.75 EB × 1.5 ≈ ~5.9 EB

Conclusion: tiering saves about 5 EB of disk. Most bytes are old, rarely read and never modified: they should be cheap, and still never lost.

Reads, Search and Sending#

WorksheetUsers at work (assumptions labelled)
  • Daily users (assumed): 1 billion; mailbox and message reads: 40 each a day → 40 billion, ~460,000/s, ~1 million/s at peak
  • Searches: a quarter of daily users search twice a day → 500 million a day, ~5,800/s, ~12,000/s at peak
  • Search index: ~2 KB per message (assumed) → ~66 MB per mailbox, ~100 PB in all; ~520,000 new messages a second to index on average
  • Outbound: 2 messages sent per daily user → 2 billion a day, ~23,000/s, each one signed, queued and delivered to another provider with retries

Conclusion: the index is huge in total but tiny per mailbox; no query spans users.

Key Metrics Summary#

Metric Value
Mailboxes (taken as Gmail's monthly users, Google, 2019) 1.5 billion
Unwanted emails blocked (Google, 2023) nearly 15 billion a day
Inbound messages (assumed) ~60 billion a day, ~1.4 million/s at peak
Inbound bandwidth (assumed 75 KB a message) ~830 Gbps at peak
New mail stored (after dedup, assumed) ~2.5 PB a day
Total mail (assumed 2.5 GB a mailbox) ~3.75 EB, ~50 trillion messages
Raw disk, tiered vs 3 copies ~5.9 EB vs ~11 EB
Searches at peak (assumed) ~12,000/s, each over one ~66 MB index
Quick check3 questions · +5 XP each

Requirements and estimation

  1. Q1/3 On these assumptions ~60 billion messages arrive a day. Roughly how many a second on average?

    Answer & explanation

    Answer: B

    60 billion ÷ 86,400 ≈ 694,000. At a 2× peak that's about 1.4 million a second.

  2. Q2/3 Why tier storage instead of keeping three full copies of every message?

    Answer & explanation

    Answer: B

    Recent mail stays on fast replicas; older mail is erasure-coded on high-capacity drives. Durability stays; cost halves.

  3. Q3/3 Search runs ~12,000 times a second at peak. Why isn't that a hard search problem?

    Answer & explanation

    Answer: B

    A per-user index turns one huge search problem into a billion small ones.

Phase 3 · Stage 3 of 7 · 10 min budget · 4 min read

Phase 3: High-Level Design (10 minutes)#

Your turn · checkpoint 2not answered

Sketch it first: Ravi's 19 MB at 9:40 pm

Ravi's office mail server looks up where to send mail for Meera's address and opens a connection. Which DNS record tells it where to go? At what moment does your system take responsibility for the message, and what must already be true at that moment? Where do the authentication checks and the spam verdict happen, before or after that moment? Where do the 19 MB end up, and what makes the message searchable at 9:55?

Think it through, or jot it down. Then compare.

One strong answer

  • DNS MX records name the SMTP edge servers for the domain.
  • Responsibility moves at "250 OK" after the data: RFC 5321 says the server is then "accepting responsibility for delivering or relaying the message". So the message must already be written to replicas in more than one zone.
  • Cheap checks before 250 (IP reputation, recipient exists, size, SPF, DKIM): a 5xx there leaves the sender responsible. The full spam verdict runs after, on the queued copy: accepted mail goes to Spam, never to nowhere.
  • Storage: the scan goes to a blob store once, by hash; the mailbox gets a small metadata row pointing at it.
  • Search: the delivery event feeds Meera's own index within seconds; the scan's text follows a little later.
You

"Four planes: an SMTP edge that refuses cheaply, a durable queue that holds every accepted message, a filter that authenticates and classifies, and a mailbox plane, partitioned by user, that stores, indexes and serves. Outbound mail is a smaller pipeline going the other way."

System Architecture#

DiagramAn email service: accept, queue, filter, file, serve
%% title: An email service: accept, queue, filter, file, serve
flowchart TD
  subgraph L1["Layer 1 · The internet"]
    direction LR
    SND@{ icon: "sd:globe", label: "Sending servers<br/>millions, not ours" }
    DNS@{ icon: "sd:dns", label: "DNS<br/>MX, SPF, DKIM, DMARC" }
  end
  subgraph L2["Layer 2 · SMTP edge"]
    direction LR
    MX@{ icon: "sd:mail", label: "MX servers<br/>TLS, IP reputation, RCPT checks" }
    LIM["Limits<br/>per IP, domain, mailbox"]:::service
  end
  subgraph L3["Layer 3 · Accept and filter"]
    direction LR
    Q@{ icon: "sd:queue", label: "Delivery queue<br/>replicated before 250 OK" }
    FILT["Spam and phishing filter<br/>auth, reputation, content, user"]:::hot
    ML@{ icon: "sd:ml", label: "Models<br/>trained on spam reports" }
  end
  subgraph L4["Layer 4 · Mailbox, partitioned by user"]
    direction LR
    MBX["Mailbox service<br/>labels, threads, quotas"]:::core
    META@{ icon: "sd:database", label: "Metadata store<br/>per user, strongly consistent" }
    IDX@{ icon: "sd:index", label: "Search index<br/>one per mailbox" }
  end
  subgraph L5["Layer 5 · Bytes, users and the way out"]
    direction LR
    BLOB@{ icon: "sd:object-storage", label: "Blob store<br/>hot replicas, cold erasure-coded" }
    BAK@{ icon: "sd:file", label: "Offline backup<br/>out of software's reach" }
    APP@{ icon: "sd:browser", label: "Web and apps" }
    OUT["Outbound MTA<br/>DKIM signing, retries"]:::service
  end
  SND -.->|"MX lookup"| DNS
  SND ==> MX
  MX --> LIM
  MX -.->|"SPF, DKIM keys"| DNS
  MX ==>|"accept"| Q
  Q ==> FILT
  ML -.-> FILT
  FILT ==> MBX
  MBX --> META
  MBX --> BLOB
  MBX -->|"new message"| IDX
  BLOB -.-> BAK
  APP ==> MBX
  APP -->|"send"| OUT
  OUT ==> SND
  class SND external
  class DNS,MX edge
  class Q queue
  class ML service
  class META,BLOB,BAK store
  class IDX cache
  class APP client

KEY INSIGHTS:

  • "250 OK" is a promise. Before it, refuse cheaply; after it, deliver or report failure. The durable queue sits exactly on that line
  • Partition everything by mailbox. Metadata, index, quotas and limits are per user, so no request needs two users' data
  • Bytes and metadata live apart. Tiny, constantly changing labels and huge, never-changing bodies get different stores

Data Flow: Ravi's Scan Reaches Meera#

DiagramRavi's 19 MB scan, from his office server to Meera's inbox
%% title: Ravi's 19 MB scan, from his office server to Meera's inbox
sequenceDiagram
  autonumber
  participant R as Ravi's mail server
  participant D as DNS
  participant M as MX edge
  participant Q as Delivery queue
  participant F as Filter
  participant B as Mailbox service
  participant I as Search index
  R->>D: MX for Meera's domain?
  D-->>R: mx edge hosts, by preference
  R->>M: EHLO, STARTTLS, MAIL FROM, RCPT TO Meera
  M->>M: IP reputation ok, mailbox exists, under limits
  R->>M: DATA, 26 MB with base64
  M->>M: SPF pass, DKIM pass
  M->>Q: Write to 3 zones, wait for 2
  Q-->>M: Durable
  M-->>R: 250 OK, queued as q7f3
  Q->>F: Message q7f3
  F->>F: DMARC pass, sender known, content clean
  F->>B: Deliver to Inbox
  B-->>I: Index new message

MEANWHILE, IN PARALLEL:

  • The fake refund notice arrives on another connection. Its domain is two days old and has no history; its display name claims a government department its domain has nothing to do with. It is accepted, scored as phishing, and filed in Spam with a warning, never shown in the inbox
  • Meera's phone gets a push notification for Ravi's message; the scan's text is extracted and added to her index a few seconds after the message itself
Quick check3 questions · +5 XP each

High-level design

  1. Q1/3 When does the receiving system become responsible for a message?

    Answer & explanation

    Answer: C

    RFC 5321: a server that sends 250 OK after DATA is accepting responsibility for delivering the message, so it must already be durable.

  2. Q2/3 The fake refund notice is accepted and then scored as phishing. What happens to it?

    Answer & explanation

    Answer: C

    After 250 OK the system must deliver or report failure. Refuse cheaply before accepting; after accepting, file it in Spam.

  3. Q3/3 Why keep labels and read state in a different store from message bodies?

    Answer & explanation

    Answer: B

    Mutable metadata wants a strongly consistent database; immutable bytes want a cheap blob store.

Phase 4 · Stage 4 of 7 · 20 min budget · 23 min read

Phase 4: Deep Dives (20 minutes)#

Deep Dive 1: The SMTP Front Door: Accept, Queue, Never Lose (Week 3 Concepts: Queues and Backpressure; Week 2: Idempotency)#

You

"SMTP has retries built in. A temporary 4xx tells the sender to keep the message and try later; a 250 makes it ours. The design is about saying 250 only when it's safe, and 4xx early instead of falling over."

This builds on Week 3, Day 1 · Queue vs Stream, Week 3, Day 3 · Backpressure and Flow Control and Week 2, Day 2 · Idempotency in Practice.

Your turn · checkpoint 3not answered

The 250 that was never written down

An MX server sends 250 OK for Ravi's 26 MB and crashes with the message still in memory: what went wrong? Opposite case: the message is written, but the 250 is lost on Ravi's network: what happens next? And when the queue's disks are 90% busy, what should the edge tell new senders?

Think it through, or jot it down. Then compare.

One strong answer

  • A lost acknowledged message: the sender has deleted its copy, so nobody has it. Never answer 250 before the message is on replicas in at least two zones.
  • A lost 250: Ravi's server retries, and the queue gets the message twice. A key of (Message-ID, body hash, recipient), remembered for a few days, turns the retry into a no-op that still answers 250.
  • Overload: answer 421 to new connections or 451 to new messages. Senders must queue and retry (RFC 5321: at least 30 minutes apart, for at least 4 to 5 days), so the internet's mail servers become our buffer. A 4xx costs a delay; falling over costs mail.

What is published: RFC 5321 says a server that sends "250 OK" after DATA "MUST NOT lose the message for frivolous reasons, such as because the host later crashes or because of a predictable resource shortage", that retries "SHOULD be at least 30 minutes" apart and that "the give-up time generally needs to be at least 4-5 days". A DNS lookup of gmail.com in October 2026 listed five MX hosts at different preferences, so senders have fallbacks (our observation, not a Google statement). Gmail's sender guidelines require TLS, and tell senders rate-limited with 4.7.28 to stop for at least 10 minutes, then reconnect with one connection and add more one at a time.

DiagramOne message at the edge: refuse cheaply, accept durably
%% title: One message at the edge: refuse cheaply, accept durably
flowchart TD
  C["Connection from an IP"]:::service
  REP{"IP reputation<br/>and rate ok?"}
  RCPT{"Recipient exists,<br/>mailbox under limits?"}
  DATA["Receive data<br/>size cap, SPF, DKIM"]:::service
  DUP{"Seen this<br/>message key?"}
  W@{ icon: "sd:queue", label: "Queue write<br/>3 zones, wait for 2" }
  OK(["250 OK<br/>now our responsibility"]):::ok
  T(["4xx · try later<br/>sender keeps it"]):::bad
  P(["5xx · refused<br/>sender reports it"]):::bad
  C ==> REP
  REP -->|"overloaded or throttled"| T
  REP -->|"known spam source"| P
  REP ==> RCPT
  RCPT -->|"no such user"| P
  RCPT ==> DATA ==> DUP
  DUP -->|"yes"| OK
  DUP ==>|"no"| W ==> OK
  class W queue
  • Refuse at the cheapest moment: a known-bad IP at connection, an unknown recipient at RCPT TO (550 5.1.1), an oversized message when its size is declared, all before receiving megabytes or running models.
  • One message, many recipients, one copy: received once, stored once, queued as one small delivery per recipient.
  • Partition the queue by recipient mailbox shard, so a flood to one address backs up only its own partition.
  • Backpressure through the protocol: as queue write latency rises, the edge answers 451 to a growing share of new mail, low-reputation senders first. Mail waits on the senders' disks, not ours.
  • A queue, not a log: each delivery is consumed once, retried with backoff, dead-lettered with an alert if it keeps failing, and finally bounced to the sender, the "report failure" half of RFC 5321's promise.
pythonmessage_key · 29 lines
# smtp/accept.py

"""
The end of SMTP DATA (illustrative): 250 only once durable in two zones; a retry
with the same key is acknowledged, not queued twice; under pressure, 451.
"""

import hashlib

QUORUM = 2                      # of 3 zone replicas


def message_key(headers: dict, body: bytes, rcpt: str) -> str:
    mid = headers.get("Message-ID", "")
    return hashlib.sha256(f"{mid}|{rcpt}|".encode() + hashlib.sha256(body).digest()).hexdigest()


def on_data_end(conn, headers: dict, body: bytes, rcpts: list, queue, seen, pressure: float) -> str:
    if pressure > 0.9 or (pressure > 0.7 and conn.reputation < 0.5):
        return "451 4.3.2 Try again later"                       # backpressure, sender retries
    blob_id = queue.put_blob(body, replicas=3, wait_for=QUORUM)    # written once, shared by all rcpts
    if blob_id is None:
        return "451 4.3.0 Temporary storage failure"
    for rcpt in rcpts:
        k = message_key(headers, body, rcpt)
        if seen.add_if_absent(k, ttl_days=5):                       # False if a retry already queued it
            queue.enqueue(shard_of(rcpt), {"rcpt": rcpt, "blob": blob_id, "key": k,
                                           "auth": conn.auth_results, "ip": conn.ip})
    return f"250 2.0.0 OK queued as {blob_id[:8]}"

Deep Dive 2: Spam and Phishing: Fifteen Billion No's a Day (Week 9 Concepts: Security Architecture; Week 8: Event Ingestion)#

You

"The filter makes two mistakes and they don't cost the same. Spam in the inbox is annoying; phishing in the inbox can empty someone's bank account; a bank statement in the spam folder can cost someone a deadline. So it's a funnel: cheap signals first, expensive models only on what's left, and every verdict nudged by what users tell us."

This builds on Week 9, Day 5 · Security Architecture and Week 8, Day 1 · Event Ingestion at Scale.

Your turn · checkpoint 4not answered

The refund that isn't

The fake refund passes SPF and DKIM for its own two-day-old domain. Its text says "lncome Tax" with a lowercase L and hides "refund" behind zero-width characters. Which signals catch it, and which would you not trust? Then 30 of the 5,400 Gmail recipients of Meera's firm's reminder click "Report spam". What changes, and for whom?

Think it through, or jot it down. Then compare.

One strong answer

  • Authentication isn't trust. Passing SPF and DKIM proves the mail came from that domain, not that the domain is honest; a fresh domain passes both.
  • What catches it: a two-day-old domain sending in a burst; a display name claiming a government body; a link asking for bank details; text built to dodge filters, which a character-level model (what Google built RETVec for) reads straight through.
  • Verdict: phishing: Spam with a warning, or rejected if the score is very high.
  • The 30 reports: 30 ÷ 5,400 ≈ 0.56%, above the 0.3% Gmail tells bulk senders never to reach. The firm's domain reputation drops for everyone, and those 30 users' own filters learn they don't want its mail.

What is published: in 2015 the Gmail team wrote that "Report spam" and "Not spam" clicks train the filters, that it used "an artificial neural network" for "especially sneaky spam", and that the filter could "reflect these individual preferences". In 2019 Google added TensorFlow models alongside its "existing ML and rules-based protections", blocking "around 100 million additional spam messages every day", among them image-based messages, hidden embedded content and "messages from newly created domains that try to hide a low volume of spammy messages within legitimate traffic". In 2023 Google's security blog wrote that attackers "use homoglyphs, invisible characters, and keyword stuffing", and that switching Gmail's spam classifier to the ~200,000-parameter RETVec vectorizer raised detection by 38%, cut false positives by 19.4% and cut TPU usage by 83%. Since 2013 Gmail has served images "through Google's own secure proxy servers", checked "for known viruses or malware", and it blocks executable attachments such as .exe, .js and .msi, also inside archives.

DiagramThe filter funnel: cheap signals first, models on what's left
%% title: The filter funnel: cheap signals first, models on what's left
flowchart TD
  IN["Accepted message<br/>from the queue"]:::service
  AUTH["Authentication<br/>SPF, DKIM, DMARC result"]:::service
  REP@{ icon: "sd:database", label: "Reputation<br/>domain, IP, URL, age" }
  CONT["Content models<br/>text, links, attachments, images"]:::hot
  USR["Per-user signals<br/>contacts, past reports"]:::service
  V{"Score"}
  INB(["Inbox"]):::ok
  WARN(["Inbox with a warning"]):::ok
  SPAM(["Spam folder"]):::bad
  FB@{ icon: "sd:stream", label: "Report spam, Not spam<br/>events" }
  IN ==> AUTH ==> CONT
  REP -.-> AUTH
  CONT ==> USR ==> V
  V -->|"low"| INB
  V -->|"middle"| WARN
  V ==>|"high"| SPAM
  FB -.->|"minutes"| REP
  FB -.->|"retraining"| CONT
  class REP store
  class FB queue
  • Order by cost. Reputation and authentication are a few cache reads; content models run only on what those can't settle. Much bulk spam never gets past the SMTP edge.
  • Reputation is keyed by authenticated identity. An IP shared by thousands of senders at an email service provider says little; a domain that passed DMARC is an identity that can earn or lose standing.
  • New is suspicious. A domain's age, first-seen time and volume curve are features, the case Google's 2019 post singles out.
  • Read like an attacker writes. Filters that tokenise into dictionary words miss homoglyphs and invisible characters; character-level models like RETVec don't, and the 83% TPU saving shows model cost is a design constraint at this volume.
  • Links and attachments are content too. Links are checked against known-bad pages, dangerous file types are refused, and images come through a proxy, so opening a message doesn't reveal the reader's IP.
  • Feedback is a stream. Spam reports, aggregated per sending domain over minutes, move reputation fast; over days they become training labels; a user's own reports tune that user's verdicts.
  • When unsure, deliver with a warning. Wanted mail in spam has the tighter bar (under 0.05%), so a middle band of scores gets a banner instead of being hidden.
pythonSignals · 37 lines
# filter/verdict.py

"""
A filter verdict (illustrative): cheap signals settle what they can; the content
score is adjusted by reputation and the user's history; phishing has a lower bar.
"""

from dataclasses import dataclass


@dataclass
class Signals:
    dmarc: str            # "pass", "fail" or "none"
    domain_rep: float     # 0 (bad) .. 1 (good); 0.5 for unknown
    domain_age_days: int
    in_contacts: bool
    user_reported_sender: bool


def verdict(s: Signals, content_model) -> str:
    if s.domain_rep < 0.05 and s.dmarc != "pass":
        return "SPAM"                                   # settled without the model
    if s.in_contacts and s.dmarc == "pass" and s.domain_rep > 0.8:
        return "INBOX"                                  # settled without the model
    spam, phish = content_model.score()                 # the expensive step, on what's left
    prior = (0.5 - s.domain_rep) * 0.4
    if s.domain_age_days < 7:
        prior += 0.15                                   # new domains start untrusted
    if s.user_reported_sender:
        prior += 0.3                                    # this user doesn't want this sender
    if phish + prior > 0.6:
        return "SPAM_WITH_WARNING"                      # phishing: lower bar, loud warning
    if spam + prior > 0.85:
        return "SPAM"                                   # ordinary spam: higher bar
    if spam + prior > 0.6:
        return "INBOX_WITH_WARNING"
    return "INBOX"
Quick check4 questions · +5 XP each

The front door and the filter

  1. Q1/4 An MX server answers 250 OK, then crashes with the message only in memory. What was the mistake?

    Answer & explanation

    Answer: B

    250 OK transfers responsibility. Write to replicas in two zones first, then answer.

  2. Q2/4 The queue is close to full at 9 pm. What should the SMTP edge tell new senders?

    Answer & explanation

    Answer: C

    SMTP's retries make the senders' queues our overflow buffer. A delay is fine; losing mail is not.

  3. Q3/4 The fake refund passes SPF and DKIM. Why isn't that enough to deliver it to the inbox?

    Answer & explanation

    Answer: B

    Authentication gives you an identity to judge. Reputation, age and content do the judging.

  4. Q4/4 What did RETVec change in Gmail's spam classifier, according to Google?

    Answer & explanation

    Answer: B

    A character-level vectorizer that resists homoglyphs and invisible characters, and is cheaper to run.

Deep Dive 3: Who Sent This? SPF, DKIM, DMARC and the 2024 Bulk-Sender Rules (Week 9 Concepts: Security Architecture; Week 1: Rate Limiting)#

You

"The From: line in an email is just text; anyone can type anything. Three standards bolted on over twenty years let a receiver check it: SPF says which servers may send for a domain, DKIM signs the message with the domain's key, and DMARC ties both to the From: domain a human actually sees and says what to do when they fail. In 2024 Gmail made them mandatory for anyone sending a lot."

This builds on Week 9, Day 5 · Security Architecture and Week 1, Day 3 · Rate Limiting at Scale.

Your turn · checkpoint 5not answered

Meera's reminder, sent through an email service

Meera's firm sends its reminder through an email service provider (ESP). The message says From: reminders@ her firm's domain. The ESP's servers use their own bounce domain in MAIL FROM, and the ESP signs with DKIM as d= the ESP's domain. Her firm's domain has no DMARC record. The reminder goes to 5,400 Gmail users today. Does it pass SPF? DKIM? DMARC? Which of the 2024 requirements does it miss, and what will the firm see?

Think it through, or jot it down. Then compare.

One strong answer

  • SPF and DKIM pass, but for the ESP's domains: SPF checks the MAIL FROM domain, DKIM the d= domain.
  • DMARC fails: neither passing domain is aligned with the From: domain, and there's no DMARC record at all.
  • As a bulk sender it needs SPF and DKIM, a DMARC record (p=none is enough), an aligned From: and one-click unsubscribe. It misses alignment and DMARC.
  • What it sees: temporary "rate limited" rejections such as 4.7.31 (no DMARC) and 4.7.32 (not aligned), or spam-foldering, and no mitigation from Gmail. The fix: the ESP signs with d= the firm's domain, the firm publishes DMARC, and the reminders carry List-Unsubscribe and List-Unsubscribe-Post headers.

What is published: Gmail's sender guidelines require, from 1 February 2024, that all senders to personal Gmail accounts use SPF or DKIM, valid forward and reverse DNS (PTR), TLS, and keep the spam rate in Postmaster Tools below 0.3%. Senders of more than 5,000 messages a day must also use SPF and DKIM, publish DMARC ("Your DMARC enforcement policy can be set to none"), align From: with the SPF or DKIM domain, and support one-click unsubscribe; DKIM keys must be at least 1,024 bits. Google's announcement (October 2023) added that unsubscribes must be processed "within two days", and that after Gmail began requiring some authentication the year before, unauthenticated messages fell by 75%; it quoted Yahoo backing the same changes. The FAQ says to stay below 0.1% and never reach 0.3%: from June 2024, bulk senders above 0.3% get no mitigation until they've been below it for 7 consecutive days, and from November 2025 Gmail is "ramping up its enforcement", with "temporary and permanent rejections": the FAQ lists temporary "rate limited" codes such as 4.7.27 (SPF), 4.7.30 (DKIM), 4.7.31 (DMARC), 4.7.32 (alignment) and 4.7.29 (TLS), and permanent blocks such as 5.7.27 (SPF) and 5.7.30 (DKIM). Gmail tracks limits per domain and per IP; an IP's quota is shared by every domain sending from it.

DiagramChecking one message's sender, from envelope to policy
%% title: Checking one message's sender, from envelope to policy
flowchart TD
  MSG["Incoming message<br/>MAIL FROM, From:, DKIM-Signature"]:::service
  SPF{"SPF<br/>is this IP allowed for<br/>the MAIL FROM domain?"}
  DK{"DKIM<br/>does the signature verify<br/>with the d= domain's key?"}
  AL{"DMARC alignment<br/>does either domain match<br/>the From: domain?"}
  POL@{ icon: "sd:dns", label: "DMARC record<br/>p=none, quarantine or reject" }
  REPD@{ icon: "sd:database", label: "Reputation<br/>per authenticated domain" }
  PASS(["DMARC pass<br/>identity to judge"]):::ok
  FAIL(["DMARC fail<br/>apply the domain's policy"]):::bad
  MSG ==> SPF
  MSG ==> DK
  SPF --> AL
  DK --> AL
  AL ==>|"aligned pass"| PASS ==> REPD
  AL -->|"no aligned pass"| FAIL
  POL -.-> FAIL
  class REPD store
  • SPF (RFC 7208) checks the envelope sender's domain, which humans never see, against a DNS list of allowed IPs, in at most 10 DNS-querying lookups. It breaks when mail is forwarded.
  • DKIM (RFC 6376) signs headers and body with a private key whose public half is in DNS under the d= domain. It survives forwarding, but breaks if a mailing list edits the subject or adds a footer.
  • DMARC (RFC 7489) is what makes the visible From: trustworthy: pass means SPF or DKIM passed for a domain aligned with From:. The domain owner publishes a policy, none, quarantine or reject, and an address for aggregate reports, so it can see who is sending as its domain. A DNS lookup of _dmarc.gmail.com in October 2026 returned p=none; sp=quarantine with a reporting address; Gmail's guidelines say it "will begin using a DMARC quarantine enforcement policy" and warn senders not to impersonate Gmail From: headers.
  • Reputation attaches to the aligned domain. Once a message passes DMARC, per-domain counters (volume, spam reports, unsubscribes) belong to someone. That's why the 2024 rules matter beyond spam: every bulk sender gets an identity that can be held to account.
  • A bulk-sender counter per domain, approximate and merged across regions every few minutes, finds senders crossing 5,000 a day.
  • Rate limits per domain and per IP. A sender over what its reputation allows gets 4.7.28, and SMTP's retries spread its mail out; Gmail tells senders to pause at least 10 minutes, then add connections one at a time.
  • Our outbound side follows the same rules: DKIM-signed with the user's domain, over TLS, retried per RFC 5321, with bounces processed so our own IPs keep their reputation at other providers.
pythonorg_domain · 29 lines
# auth/dmarc.py

"""
DMARC for one message (illustrative, after RFC 7489): pass if SPF or DKIM passed
for a domain aligned with From:; relaxed alignment compares organisational domains.
"""


def org_domain(d: str, public_suffixes: set) -> str:
    labels = d.lower().rstrip(".").split(".")
    for i in range(len(labels)):
        if ".".join(labels[i + 1:]) in public_suffixes:
            return ".".join(labels[i:])                 # e.g. mail.example.co.in -> example.co.in
    return d.lower()


def aligned(a: str, b: str, mode: str, psl: set) -> bool:
    return a.lower() == b.lower() if mode == "s" else org_domain(a, psl) == org_domain(b, psl)


def dmarc(from_domain: str, spf: tuple, dkim: list, record: dict, psl: set) -> dict:
    """spf = (result, mail_from_domain); dkim = [(result, d_domain), ...]; record from DNS or {}."""
    spf_ok = spf[0] == "pass" and aligned(spf[1], from_domain, record.get("aspf", "r"), psl)
    dkim_ok = any(r == "pass" and aligned(d, from_domain, record.get("adkim", "r"), psl) for r, d in dkim)
    if spf_ok or dkim_ok:
        return {"result": "pass", "identity": org_domain(from_domain, psl)}
    if not record:
        return {"result": "none", "identity": None}     # no policy: judged on other signals
    return {"result": "fail", "policy": record.get("p", "none"), "identity": None}

Deep Dive 4: Storing Mail Cheaply but Durably (Week 1 Concepts: Replication Trade-offs; Week 4: Multi-Tier; Week 10: Deployment Strategies)#

You

"Cost says most mail is old and never read again, so keep it on the cheapest disks. Durability says a message from 2014 must still be there in 2034. Replication handles hardware failures; it does nothing for a bug that runs on every replica, as Gmail found in 2011."

This builds on Week 1, Day 2 · Replication Trade-offs, Week 4, Day 5 · Multi-Tier Caching and Week 10, Day 3 · Deployment Strategies. The 2011 incident is the war story A software bug deleted Gmail mail from several copies at once.

Your turn · checkpoint 6not answered

Three copies, one bug

Every message is stored in three zones. A new storage release has a bug that, rarely, deletes a mailbox's data, and it's rolling out everywhere. How many copies survive, and what would have to be different for one to? And why keep three full copies of Meera's 12-year-old mail, read maybe once a year?

Think it through, or jot it down. Then compare.

One strong answer

  • None, potentially. Replicas protect against independent failures. A bug runs on every replica, and a delete replicates faithfully; Google's 2011 post: "in some rare instances software bugs can affect several copies of the data."
  • What survives: a copy the buggy code can't reach or change: offline backups (Google used tape), soft deletes purged only after days, and rollouts one cell of mailboxes at a time, so a bug hits a small slice first.
  • Cold mail: erasure coding (say 6 data and 3 parity fragments across zones) survives losing any 3 fragments at 1.5× the bytes instead of 3×. Old mail is immutable and rarely read, exactly what erasure coding suits.

What is published: in February 2011 Ben Treynor, Google's VP of Engineering, wrote that 0.02% of Gmail users had found their accounts empty: Google kept "multiple copies of your data, in multiple data centers", but "a storage software update" had a bug that hit several copies. Google stopped the rollout and reverted, and restored the mail from tape: "Since the tapes are offline, they're protected from such software bugs", though restoring took "hours ... instead of milliseconds". Mail sent to affected users in a 20-hour window "was likely not delivered", and its senders were notified. Google Cloud wrote in 2021 that Colossus underpins Gmail, and that Colossus in general keeps "the hottest data" on flash and moves data to "larger capacity drives as it ages and becomes colder"; and in 2023 that Spanner supports Gmail. Gmail keeps deleted messages in Trash for 30 days. How Gmail lays out mailboxes on these systems isn't published.

DiagramOne message's bytes, from hot to cold, with a copy software can't touch
%% title: One message's bytes, from hot to cold, with a copy software can't touch
flowchart TD
  NEW["New message<br/>body and attachments"]:::service
  CA["Content address<br/>hash of the bytes, refcount"]:::core
  HOT@{ icon: "sd:object-storage", label: "Hot tier · flash and disk<br/>3 replicas in 3 zones" }
  COLD@{ icon: "sd:object-storage", label: "Cold tier · large drives<br/>erasure-coded 6 + 3" }
  META@{ icon: "sd:database", label: "Mailbox metadata<br/>labels, thread, blob refs" }
  DEL["Soft delete<br/>tombstone, purge after days"]:::service
  BAK@{ icon: "sd:file", label: "Offline backup<br/>different software, not online" }
  NEW ==> CA ==> HOT
  HOT ==>|"after ~30 days"| COLD
  CA -.-> META
  META --> DEL
  HOT -.->|"daily, incremental"| BAK
  COLD -.-> BAK
  class HOT,COLD,META,BAK store
  • Metadata in a strongly consistent store, partitioned by user. Labels, read state and blob references are small and change all day; one key range per user makes "archive this thread" one transaction on one shard.
  • Bodies and attachments as immutable blobs, addressed by hash. Ravi's scan is stored once; a newsletter's logo sent to 3 million users is stored once, with a reference count. A blob is never edited, only referenced or released, which keeps replication and erasure coding simple.
  • Tier by age. Replicas on fast media for about 30 days, then erasure-coded fragments on large drives: ~5.9 EB of raw disk instead of ~11 EB on the Phase 2 estimate.
  • Deletes are slow on purpose. Trash for 30 days, then the metadata row goes and the blob's count drops; a blob at zero is purged only after a further grace period, so a buggy mass delete can be undone from tombstones.
  • A copy software can't reach. Backups on media the online storage software never mounts, written by a separate, simpler program and verified by reading back. They're slow to restore, so they're the last line, not the first.
  • Roll out storage software one cell at a time: canary, 1%, 10%, each step gated on blob checksums, reference counts and "mailboxes that shrank by more than half".
pythonput · 39 lines
# storage/blobs.py

"""
Content-addressed blobs with reference counts and slow purges (illustrative): a blob
is purged only after a grace period with no references, so a mass delete is reversible.
"""

import hashlib
import time

GRACE_S = 14 * 86400


def put(store, data: bytes) -> str:
    h = hashlib.sha256(data).hexdigest()
    if not store.exists(h):
        store.write(h, data, replicas=3, zones=3, tier="hot")
    store.incr_ref(h)
    return h


def release(store, h: str) -> None:
    if store.decr_ref(h) == 0:
        store.mark_unreferenced(h, at=time.time())        # not deleted yet


def purge_sweep(store, max_per_run: int, deleting_rate_alarm) -> int:
    if deleting_rate_alarm():                              # far more unreferenced than usual?
        return 0                                           # stop: maybe a bug, not users
    n = 0
    for h, since in store.unreferenced(older_than=time.time() - GRACE_S, limit=max_per_run):
        if store.ref(h) == 0:
            store.delete(h)
            n += 1
    return n


def cool(store, h: str) -> None:
    store.reencode(h, scheme=(6, 3), tier="cold")         # 1.5x instead of 3x, once rarely read
Quick check4 questions · +5 XP each

Senders and storage

  1. Q1/4 Meera's ESP signs with DKIM as its own domain and uses its own bounce domain. Her From: is her firm's domain. Does DMARC pass?

    Answer & explanation

    Answer: B

    DMARC needs SPF or DKIM to pass for a domain aligned with From:. Signing with d= her firm's domain fixes it.

  2. Q2/4 A sender to Gmail crosses 5,000 messages a day. What does it need beyond SPF or DKIM?

    Answer & explanation

    Answer: C

    Gmail's guidelines for bulk senders, since February 2024; spam rate under 0.3% applies to everyone.

  3. Q3/4 Why didn't three replicas in three data centres protect Gmail's mail in 2011?

    Answer & explanation

    Answer: B

    Replicas protect against independent failures. Bugs aren't independent; an offline copy is.

  4. Q4/4 Why erasure-code old mail instead of keeping three replicas?

    Answer & explanation

    Answer: B

    Reads of cold data are rare, so the slower reconstruction path is cheap; the saved disk is not.

You

"Gmail launched as 'search-based webmail'. What makes search tractable at this size is that nobody searches anyone else's mail. So there's no global index: there are a billion and a half small ones."

This builds on Week 7, Day 1 · Search Fundamentals and Architecture, Week 7, Day 2 · Indexing Pipeline and Week 7, Day 5 · Search Operations and Scale.

Your turn · checkpoint 7not answered

from:ravi has:attachment form 16

Meera has ~60,000 messages over twelve years. She types from:ravi has:attachment form 16 older_than:1y. Which parts of that query are exact filters on fields, and which are text matching? What's the cheapest order to evaluate them? Ravi's newest message arrived 15 minutes ago: does it need to be searchable already? And when she adds the label "ITR 2026" to 40 threads, what must be re-indexed?

Think it through, or jot it down. Then compare.

One strong answer

  • Fields: from:ravi, has:attachment, older_than:1y. Text: form and 16, in subject, body or attachment text.
  • Order: rarest first. A few hundred messages from Ravi, intersected with the attachment flag and the date, then with the postings for form and 16; each step shrinks the set.
  • Fresh mail: yes. New messages land in a small in-memory segment within seconds; queries read it with the on-disk segments.
  • Labels: nothing is re-indexed. Labels live in metadata as per-label bitmaps, applied as a filter at query time.

What is published: Google's 2004 announcement called Gmail "a free search-based webmail service", "built on Google search technology", to "quickly search every email they've ever sent or received". Gmail's help lists the operators: from:, to:, subject:, after:/before:, older_than:, has:attachment, label:, in:anywhere (Spam and Trash too), larger:/smaller:, AROUND for words near each other, rfc822msgid:, and OR, - and { }. Operators match messages, then whole conversations are shown. How Gmail's index is built isn't published.

DiagramOne mailbox's index, built from the delivery stream
%% title: One mailbox's index, built from the delivery stream
flowchart TD
  DLV@{ icon: "sd:stream", label: "Delivery events<br/>new, deleted, moved" }
  TOK["Tokenise<br/>fields, words with positions"]:::service
  EXT["Attachment text<br/>PDF, docs, async"]:::service
  MEM["Fresh segment<br/>in memory, seconds old"]:::hot
  SEG@{ icon: "sd:index", label: "On-disk segments<br/>merged as they grow" }
  BM@{ icon: "sd:database", label: "Label bitmaps<br/>in mailbox metadata" }
  Q["Query<br/>parse operators, plan by rarity"]:::core
  RES(["Message ids<br/>grouped into threads, newest first"]):::ok
  DLV ==> TOK ==> MEM
  DLV -.-> EXT -.-> MEM
  MEM -->|"flush and merge"| SEG
  SEG ==> Q
  MEM ==> Q
  BM -.->|"label: filter"| Q
  Q ==> RES
  class DLV queue
  class SEG cache
  class BM store
  • Partition the index like the mailbox. It lives on the user's shard: one shard per query, no fan-out, and the access check is "is this your mailbox". At ~66 MB (a twelve-year mailbox, a few hundred), hot parts fit in memory while the user is active.
  • Fields as columns, words as postings. Sender, date, size and flags are per-message columns (larger:10M scans one); words map to message ids with positions, which AROUND and phrases need.
  • Log-structured. New mail goes into a small segment; segments are merged in the background; deletes are a bitmap applied at query time until the next merge.
  • Index from the delivery stream, not the SMTP path, so a slow indexer delays search by seconds but never delays delivery.
  • Sort by date by default: mail search is mostly "find the one I know exists". Spam and Trash are one more bitmap, included only for in:anywhere.
pythonparse · 54 lines
# search/query.py

"""
A Gmail-style query over one mailbox's index (illustrative): each term becomes a set
of message ids, intersected smallest first; labels come from metadata bitmaps.
"""

import re
import time

TOKEN = re.compile(r'(-?)(\w+):(\S+)|(-?)"([^"]+)"|(-?)(\S+)')


def parse(q: str) -> list:
    terms = []
    for m in TOKEN.finditer(q):
        if m.group(2):
            terms.append((m.group(1) == "-", m.group(2).lower(), m.group(3).lower()))
        elif m.group(5):
            terms.append((m.group(4) == "-", "phrase", m.group(5).lower()))
        else:
            terms.append((m.group(6) == "-", "word", m.group(7).lower()))
    return terms


def ids_for(idx, meta, kind: str, value: str) -> set:
    if kind == "from":
        return idx.field("from", value)
    if kind == "has" and value == "attachment":
        return idx.flag("attachment")
    if kind == "older_than":
        n, unit = int(value[:-1]), value[-1]
        return idx.date_before(time.time() - n * {"d": 86400, "m": 30 * 86400, "y": 365 * 86400}[unit])
    if kind == "label":
        return meta.label_bitmap(value)
    if kind == "phrase":
        return idx.phrase(value.split())                  # positions must be consecutive
    return idx.postings(value)                            # a plain word


def search(idx, meta, q: str, limit: int = 50) -> list:
    pos, neg = [], []
    for negate, kind, value in parse(q):
        (neg if negate else pos).append(ids_for(idx, meta, kind, value))
    if not pos:
        return []
    pos.sort(key=len)                                     # rarest first keeps every step small
    hits = set(pos[0])
    for s in pos[1:]:
        hits &= s
    for s in neg:
        hits -= s
    hits -= meta.in_spam_or_trash()                       # unless in:anywhere was asked for
    return sorted(hits, key=idx.date, reverse=True)[:limit]

Deep Dive 6: Quotas and Rate Limits (Week 1 Concepts: Rate Limiting at Scale; Week 9: Noisy Neighbours)#

You

"Four limits, protecting four things. Storage quota protects cost. Sending limits protect the internet from our compromised accounts. Receiving limits protect a mailbox and its shard from a flood. Sender limits per IP and domain protect our users from other people's spam. All must be cheap enough to check on every message."

This builds on Week 1, Day 3 · Rate Limiting at Scale and Week 9, Day 2 · Noisy Neighbor Prevention.

Your turn · checkpoint 8not answered

14.6 GB of 15

Ravi's 26 MB arrives with Meera at 14.6 GB of 15. Accept it? And at 14.99 GB, which SMTP answer gives the message the best chance once she frees space? Separately, a client's misconfigured monitoring system sends her 200 alerts a minute. Which limit catches it?

Think it through, or jot it down. Then compare.

One strong answer

  • At 14.6 GB, accept: 26 MB fits. Quota is checked from a cached counter at RCPT TO and again before the write.
  • At 14.99 GB, answer 452 4.2.2 "mailbox full", a temporary failure: the sender retries for days, so if Meera frees space meanwhile, the message gets in. A permanent 5xx would bounce it at once.
  • The alert flood is over a receiving limit like Workspace's 60 a minute; our limiter defers the excess with a 4xx. Gmail's published behaviour is stricter: a Workspace account at its receiving limit can't get new mail for about 24 hours, and incoming messages bounce.
  • Neighbours are safe: the flood counts against one mailbox, so the rest of the shard keeps receiving.

What is published: each Google Account has up to 15 GB shared across Gmail, Drive and Photos; over quota, "your ability to send and receive email in Gmail can be impacted", and after 2 years over quota content "may be removed", with at least 3 months' notice; after a big deletion, usage "normally takes up to 48–72 hours" to update (Google One Help). Personal accounts unused for 2 years may be deleted, from December 2023 at the earliest (Google, May 2023). Personal Gmail's sending limit is around 500 recipients in one email or 500 emails a day (Gmail Help). A Workspace account may send 2,000 messages and 10,000 recipients a day "over a rolling 24-hour period", and receive 60 messages a minute, 3,600 an hour and 86,400 a day, each up to 50 MB (70 MB on Enterprise Plus) after encoding, which "adds about a 37% increase". At the receiving limit, new messages bounce for "about 24 hours" and "can't be recovered" (Google Workspace Admin Help).

DiagramFour kinds of limit, checked at different moments
%% title: Four kinds of limit, checked at different moments
flowchart TD
  IN@{ icon: "sd:mail", label: "Inbound message<br/>from a sending IP and domain" }
  SL["Sender limits<br/>per IP, per domain, by reputation"]:::service
  RL["Receiving limit<br/>per mailbox: minute, hour, day"]:::hot
  SQ["Storage quota<br/>cached usage plus reservation"]:::core
  OUTM@{ icon: "sd:user", label: "User sending<br/>web, app, SMTP, API" }
  OL["Sending limits<br/>per account, rolling 24 h"]:::service
  OK(["Accepted or sent"]):::ok
  T(["4xx · try later<br/>421, 451, 452"]):::bad
  IN ==> SL ==> RL ==> SQ ==> OK
  SL -->|"over"| T
  RL -->|"over"| T
  SQ -->|"full"| T
  OUTM ==> OL ==> OK
  OL -->|"over"| T
  class IN external
  class OUTM client
  • Three windows, one rate. 60 a minute, 3,600 an hour and 86,400 a day are all one message a second: checking all three allows a burst of 60 but no sustained flood. Sliding-window counters on the mailbox's own shard make each check a few increments.
  • Storage quota: a fast estimate and a slow truth. Exact usage is recomputed asynchronously (hence Google's 48 to 72 hours); the delivery path reads a cached counter and reserves the message's size. Being off by a few hundred megabytes is fine; reading every blob per delivery is not.
  • Temporary when the user can fix it. "Mailbox full" and "receiving too fast" are 4xx, so the sender's queue holds the message; 5xx is for things that won't change, like a mailbox that doesn't exist.
  • Sending limits stop compromised accounts, whose first use is usually spam: a rolling 24-hour cap, stricter for new accounts, protects our outbound IPs' reputation.
  • Guard the automation. In December 2020 an old quota system reported Google's account service as using nothing, automation cut its quota, and sign-ins failed for 47 minutes (war story: Google's sign-in went down because a quota system thought it was using nothing). Treat "usage dropped to zero" as a likely bug, and cap how fast a limit can tighten.
pythonsliding_count · 38 lines
# limits/receiving.py

"""
A per-mailbox receiving limit (illustrative) at Workspace's published windows: 60 a
minute, 3,600 an hour, 86,400 a day, each a sliding-window counter.
"""

import time

WINDOWS = ((60, 60), (3600, 3600), (86400, 86400))      # (seconds, max messages)


def sliding_count(kv, key: str, window_s: int, now: float) -> float:
    cur_start = int(now // window_s) * window_s
    cur = kv.get(f"{key}:{window_s}:{cur_start}") or 0
    prev = kv.get(f"{key}:{window_s}:{cur_start - window_s}") or 0
    elapsed = (now - cur_start) / window_s
    return cur + prev * (1 - elapsed)


def admit(kv, mailbox: str, now: float = None) -> str:
    now = now or time.time()
    key = f"rcv:{mailbox}"
    for window_s, limit in WINDOWS:
        if sliding_count(kv, key, window_s, now) + 1 > limit:
            return "450 4.2.1 Mailbox receiving too fast, try later"
    for window_s, _ in WINDOWS:
        b = int(now // window_s) * window_s
        kv.incr(f"{key}:{window_s}:{b}", ttl_s=2 * window_s)
    return "OK"


def reserve_storage(quota, mailbox: str, size: int) -> str:
    used, limit = quota.cached(mailbox)                   # fast estimate, reconciled later
    if used + quota.reserved(mailbox) + size > limit:
        return "452 4.2.2 Mailbox full"                   # temporary: the sender keeps retrying
    quota.reserve(mailbox, size, ttl_s=600)               # released or committed at write time
    return "OK"
Quick check4 questions · +5 XP each

Search and quotas

  1. Q1/4 Why is there no global search index across all mailboxes?

    Answer & explanation

    Answer: B

    A billion small indexes, each next to its own mailbox, beat one enormous index that every query would have to filter by user.

  2. Q2/4 Meera adds a label to 40 threads. What gets re-indexed?

    Answer & explanation

    Answer: C

    Labels change constantly; keeping them out of the text index avoids rewriting postings for a flag.

  3. Q3/4 60 a minute, 3,600 an hour, 86,400 a day: why three windows?

    Answer & explanation

    Answer: B

    A burst of 60 in a minute is fine; one a second for a whole day is the cap.

  4. Q4/4 Meera's mailbox is full when Ravi's message arrives. Which answer gives the message the best chance?

    Answer & explanation

    Answer: B

    If she frees space before the sender gives up, the retry succeeds.

Phase 5 · Stage 5 of 7 · 5 min budget · 2 min read

Phase 5: Scaling and Edge Cases (5 minutes)#

Interviewer

"Walk me through Meera's evening from the service's side, then the cases that bite."

Meera's Evening#

Deadline evening in Pune (the limits and the bulk-sender rules are Gmail's published ones; the rest is illustrative)
  • 10:00 AM · The firm's reminder
    • 5,400 messages to Gmail; bulk-sender rules apply; unaligned, some deferred with 4.7.32
  • 9:40 PM · Ravi's scan
    • Quorum write in 3 zones, 250 OK in under 2 s, inbox, indexed seconds later
  • 9:41 PM · The fake refund
    • Accepted, scored as phishing, filed in Spam with a warning
  • 9:55 PM · Search
    • One shard, one index, under a second
  • 11:58 PM · Full
    • A 30 MB message gets 452 "mailbox full"; she deletes old promotions; the sender's retry 30 minutes later is accepted

Edge Cases#

Edge cases
Edge case 1: A mailing list breaks DKIM

Problem: A residents' association list adds "[RWA]" to subjects and a footer; the original DKIM signature breaks, SPF fails, and DMARC fails for a sender with p=reject.

Solution: The list rewrites From: to its own domain and signs as itself, or adds ARC headers (RFC 8617) recording the results it saw before changing the message, which a receiver that trusts the list can use. Never exempt lists from DMARC: phishers would use the gap.

Edge case 2: A catch-all address under a dictionary attack

Problem: Every unknown address goes to one mailbox; spammers guess thousands of names an hour.

Solution: The mailbox's receiving limit caps the flood on its own queue partition; recommend against catch-alls, as Google's help does.

Edge case 3: A trusted sender is hijacked

Problem: A real company's ESP account is compromised and sends phishing that passes DMARC, with years of good reputation behind it.

Solution: Content models still run for authenticated senders; a sudden change in a domain's mail (new links, templates, volume) lowers its trust; the first users' reports move its score within minutes.

Edge case 4: Storage is ahead of the quota counter

Problem: A user deletes 5 GB, but usage still shows full for two days, so mail is deferred with 452.

Solution: Credit deletes to the fast counter at once while the exact recount runs; err toward accepting mail, since a little over quota costs less than a deferred message.

Quick check3 questions · +5 XP each

Scaling and edge cases

  1. Q1/3 A mailing list rewrites the subject, breaking the original DKIM signature. What lets the receiver still trust the message?

    Answer & explanation

    Answer: B

    ARC (RFC 8617) carries authentication results across an intermediary that changes the message.

  2. Q2/3 A long-trusted company's ESP account is compromised and sends phishing that passes DMARC. What catches it?

    Answer & explanation

    Answer: C

    Authentication says who; content and behaviour say whether to trust them today.

  3. Q3/3 A catch-all mailbox receives thousands of guessed addresses an hour. What keeps it from hurting other mailboxes?

    Answer & explanation

    Answer: A

    One mailbox's flood hits only its own limit and partition; neighbours keep flowing.

Phase 6 · Stage 6 of 7 · 2 min

Phase 6: Monitoring and Operations#

You

"The dashboard has to answer five questions: are we accepting mail, are we losing any, is the filter making the right mistakes, is storage safe, and are senders behaving?"

Monitoring Dashboard#

Mail platform dashboard — 21:45 IST, a busy weekday (illustrative)
Accepting mail
Metric Now
Inbound messages 1.21M/s
SMTP accept p99 (last byte to 250) 1.4 s
Temporary deferrals (4xx), all causes 6.8%
Queue write quorum p99 38 ms
Accepted to inbox p99 6.2 s
Not losing any
  • Accepted messages not yet delivered or bounced after 1 h: 214
  • Dead-letter partition: 31 messages; duplicates suppressed by message key: 2.1%
Filtering
  • Spam in inbox (reports ÷ inbox deliveries, 24 h): 0.07%; "Not spam" clicks ÷ spam-folder deliveries: 0.04%
  • Content model p99: 19 ms; messages settled before the model: 58%
Storage and senders
  • Blobs purged in the last hour vs 7-day median: 1.02×; checksum failures: 0
  • Bulk domains above 0.3% spam rate: 1,840

SLOs for the Platform#

SLOs for receiving, durability, filtering and search
SLO 1: Mail is accepted
  • Target: 99.95% of SMTP sessions from senders in good standing get a 250 or a 5xx (never an edge error); accept p99 < 2 s
SLO 2: Accepted mail is never lost
  • Target: zero accepted messages unaccounted for: every one delivered, or bounced to its sender, within 5 days
SLO 3: The filter's mistakes stay small
  • Target: spam in the inbox < 0.1% and wanted mail in spam < 0.05%, measured daily from user feedback
SLO 4: New mail is visible and findable
  • Target: accepted to inbox p99 < 10 s; searchable within 60 s; search p99 < 1 s
Quick check3 questions · +5 XP each

Reading the dashboard

  1. Q1/3 Blobs purged in the last hour jump to 40× the 7-day median. What do you do first?

    Answer & explanation

    Answer: B

    Purges are delayed and reversible by design; a spike is the moment to stop them, then investigate.

  2. Q2/3 Temporary deferrals rise from 7% to 30% while inbound volume is normal. Where do you look?

    Answer & explanation

    Answer: B

    Normal volume plus many 4xx means the edge thinks it can't write durably. Senders will retry, but the cause needs fixing.

  3. Q3/3 "Not spam" clicks on spam-folder mail double after a model release. What does that mean?

    Answer & explanation

    Answer: A

    False positives are the costlier mistake. A model release should be gated on both error rates.

Final challenge · Stage 7 of 7 · 2 min

Final Challenge: Deadline Night, With a Bad Release#

The interviewer leans back. "One more."

Interviewer

"It's 31 July, 10 pm. Inbound mail is 1.6 times normal because everyone is sending documents to their accountants. A phishing wave impersonating the tax department started at 8 pm. And at 9:50, the storage team began rolling out a new version of the blob store. At 10:05, the canary cell reports that 0.4% of its mailboxes show messages whose bodies can't be read. Tell me what you do, in order."

Deadline night's brief
  • Inbound: 1.6× normal, ~1.1 million messages a second (Phase 2 assumptions)
  • Phishing wave: ~40 million messages since 8 pm from hundreds of new domains (assumption)
  • Storage release: in the canary cell (1% of mailboxes), due to reach 10% at 10:30
  • Canary: 0.4% of its mailboxes have unreadable bodies; blob purges at 25× normal
Your turn · checkpoint 9not answered

What do you change for deadline night?

Decide what happens to the release, the purges, the affected mailboxes and their incoming mail, the phishing wave and the overall load, and what you measure.

Think it through, or jot it down. Then compare.

One strong answer

  • Stop the release and roll the canary back. The 10% step never happens; the bug has reached part of 1% of mailboxes, which is why storage software goes out cell by cell.
  • Freeze purges everywhere. The "deleted" bodies are unreferenced, not gone, so restoring references from metadata tombstones brings most back in minutes. Offline backups are the last resort.
  • Affected mailboxes: defer, don't bounce. Answer their incoming mail with a temporary 4xx so senders retry for days. A bounce (in 2011, senders to affected users were told their mail wasn't delivered) would push a returns-night document back to a client who may not notice in time. Tell affected users "some messages are being restored".
  • The phishing wave: cluster the new domains by shared infrastructure and templates, let user reports move their reputation within minutes, and widen the warning band for tax-refund mail from domains younger than a week.
  • Load: 1.6× is inside the 2× design peak; if queue writes slow, defer low-reputation senders first.
  • Measure: accepted-but-unaccounted messages (zero), unreadable bodies falling, purges back to normal, deferrals, and wanted mail in spam.
Quick check3 questions · +5 XP each

Final challenge · Quest check

  1. Q1/3 The canary cell shows unreadable bodies at 10:05. What is the first action?

    Answer & explanation

    Answer: B

    Staged rollouts exist so a bug is stopped at 1%. Then repair.

  2. Q2/3 Why freeze blob purges during the incident?

    Answer & explanation

    Answer: B

    Slow, reversible deletes turn a data-loss bug into a repair job.

  3. Q3/3 Mail keeps arriving for mailboxes under repair. What should the edge answer?

    Answer & explanation

    Answer: C

    A deferral costs minutes; a bounce on returns night can cost a deadline.

Wrap-up · 7 min

Interview Conclusion#

Transcript
Interviewer

"Why not answer 250 as soon as the message arrives and write it to disk in the background? It'd be faster."

You

"Because 250 is when the sender deletes its copy; crash before the write and the message exists nowhere. A quorum write to two zones costs tens of milliseconds, next to a megabyte upload."

Interviewer

"Three replicas in three zones is a lot of durability. Why do you still want offline backups?"

You

"Because replicas fail independently and bugs don't. In 2011 a storage bug deleted several copies of mail for 0.02% of Gmail users, and tape, which software couldn't reach, brought it back. Slow purges, staged rollouts and an offline copy protect you from your own code."

Interviewer

"The single most important decision?"

You

"Partitioning everything by mailbox: metadata, index, quotas and limits on the user's own shard. Email is a federated firehose on the outside, but on the inside no request ever needs two users' data. That's what turns 1.5 billion mailboxes into a billion and a half small, independent systems."

Summary: Concepts Applied from 10-Week Course#

Concepts from the 10-week course in an email service
  • Week 1: Data at scale
    • Replication trade-offs — quorum writes before 250 OK; replicas don't stop correlated bugs
    • Rate limiting — receiving limits at three windows; sender limits per IP and domain; sending limits per account
  • Week 2: Failure-first design
    • Idempotency — message keys so a retried SMTP transaction is queued once
  • Week 3: Messaging and async processing
    • Queues and backpressure — a durable delivery queue partitioned by mailbox; 4xx deferrals as protocol-level backpressure; dead letters and bounces
  • Week 4: Caching
    • Multi-tier storage — hot replicas, then erasure-coded cold tiers; image proxying
  • Week 7: Search systems
    • A per-mailbox inverted index with fields, positions and log-structured segments, fed by the delivery stream
  • Week 9: Multi-tenancy and security
    • SPF, DKIM, DMARC and reputation; a filter funnel; noisy-neighbour limits per mailbox
  • Week 10: Production readiness
    • Cell-by-cell rollouts of storage software; SLOs for loss, filtering and freshness

Why Gmail Matters#

Why this is a different kind of hard problem
You can't change the protocol
  • SMTP is from the 1980s and spoken by millions of servers you don't run. Durability, backpressure and retries have to be built from what it already offers: 250, 4xx and 5xx
A large share of the input is an adversary
  • Every message is a claim about who sent it and what it is. The filter makes two kinds of mistake, and the one that hides wanted mail is held to the tighter bar
Forever is a storage requirement
  • Users keep mail for decades. That means cheap tiers for old bytes, and protection against the failure replicas can't stop: your own software

"Say 250 only when it's safe, judge senders by who they provably are, and keep one copy your own code can't reach."

Self-Assessment Checklist#

After studying this case study, you should be able to:

Receiving:

Receiving0/3 ready
  • Explain why "250 OK" transfers responsibility and what must be durable before it
  • Use 4xx deferrals as backpressure and deduplicate retried messages
  • Partition a delivery queue so one flooded mailbox can't block others

Filtering and senders:

Filtering and senders0/3 ready
  • Design a filter funnel that orders signals by cost and treats the two kinds of error differently
  • Walk through SPF, DKIM and DMARC alignment for a message sent through an ESP
  • State Gmail's 2024 requirements for all senders and for bulk senders

Storage, search and limits:

Storage, search and limits0/3 ready
  • Separate metadata from immutable blobs, tier by age, and explain why replicas don't stop software bugs
  • Build a per-mailbox search index with field filters, positions and label bitmaps
  • Enforce storage quotas and receiving limits with cheap, temporary-failure checks

Sources#

Google and Gmail:

Official help pages:

Standards:

  • Docs

    RFC 5321 (SMTP), RFC 7208 (SPF), RFC 6376 (DKIM), RFC 7489 (DMARC), RFC 8058 (one-click unsubscribe), RFC 8617 (ARC), RFC 3463 (status codes, X.2.2 mailbox full): https://www.rfc-editor.org/

Reporting and observation:

Design note: the architecture, code, thresholds, dashboards and every number labelled as an assumption or estimate are an illustrative design for the problem, not a description of Gmail's internal systems beyond what Google has published. Messages per mailbox, message sizes, storage per mailbox, daily users, read and search rates, the deduplication saving, the erasure-coding scheme and the final challenge's figures are assumptions. Meera, Ravi and their messages are fictional.

Further Reading#

Related systems to study:

  • Read

    WhatsApp (Bonus Problem 2 in this series): messages that are deleted once delivered, the opposite storage choice to email's "keep forever"

  • Book

    Google Search (Bonus Problem 5): one global index sharded by document, where this quest keeps a billion small ones

  • Read

    AWS S3 (Bonus Problem 8): erasure coding, durability and the cost of nines

  • Docs

    Google Docs (Bonus Problem 20): another Google product where every request belongs to one document or user

Foundational concepts (this course):

  • Read

    Week 1, Day 2 — Replication Trade-offs

  • Read

    Week 1, Day 3 — Rate Limiting at Scale

  • Read

    Week 2, Day 2 — Idempotency in Practice

  • Read

    Week 3, Day 1 — Queue vs Stream

  • Read

    Week 3, Day 3 — Backpressure and Flow Control

  • Read

    Week 4, Day 5 — Multi-Tier Caching

  • Read

    Week 7, Day 1 — Search Fundamentals and Architecture

  • Read

    Week 7, Day 2 — Indexing Pipeline

  • Read

    Week 7, Day 5 — Search Operations and Scale

  • Read

    Week 8, Day 1 — Event Ingestion at Scale

  • Read

    Week 9, Day 2 — Noisy Neighbor Prevention

  • Read

    Week 9, Day 5 — Security Architecture

  • Read

    Week 10, Day 3 — Deployment Strategies

"Say 250 only when it's safe, judge senders by who they provably are, and keep one copy your own code can't reach."

this lesson in shorts · 4

play them all →

One idea from this lesson in about a minute each, made for your phone.

End of Side quest 28 · Gmail
Clear the questFinish Gmail.Stages clear as you read them. Marking complete unlocks after ~8 more min of reading.
  • 0/7stages cleared
design sheet · 7 diagrams · pdfRevise the whole design in ten minutesThe numbers, architecture, sequence, class and state diagrams, decisions and failure modes of this quest on a few printable pages.open the sheet →

New lessons by email

New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.

Discussion · …

Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…