Communication
Design Gmail
01
What the mail service must do
What the mail service must do
- Accept mail from any server on the internet for our domains over SMTP, and deliver our users' outgoing mail to any domain.
- Keep each user's mailbox: read state, stars, labels instead of folders, archive, trash and spam.
- Group messages into conversations using reply headers, with a fallback when clients drop them.
- Search everything a user has, including subjects, bodies, sender names and text extracted from attachments, with operators like
from:,has:attachmentandbefore:. - Run user filters on arrival, sort mail into tabs, and block spam, phishing and malware.
- Sync quickly to web, mobile and IMAP clients, with push notifications for new mail.
- Once our server says 250 OK to a sender, that message is never lost.
- Inbound mail appears in the inbox within a few seconds of acceptance for the vast majority of messages.
- Inbox listing and opening a message: p95 under 200 ms. Search: p95 under 500 ms even for mailboxes with 15 years of mail.
- Receiving stays up at 99.99%; sending can be a little less, because SMTP senders retry for days.
- Strict isolation: one user's data never appears in another user's results, caches or logs.
Email covers a lot of ground. Agree to focus on receiving, storing, searching and syncing, with spam defence and outbound delivery as supporting pieces. Calendar invites, chat, and the compose experience can be mentioned and left out. Ask whether you are designing a consumer service, a business service, or both, because business domains bring retention rules and admin policies that change the storage design.
02
Sizing the mail system
Sizing the mail system
Assume 1.2 billion mailboxes with 500 million people active on a given day, 3 billion inbound SMTP attempts a day of which most are junk, and an average mailbox of 9 GB including attachments.
| Quantity | Working | Result |
|---|---|---|
| Inbound attempts | 3B / 86,400 s | about 35,000 / s, peaks near 90,000 / s |
| Rejected before storage | bad reputation, failed auth, content blocks: assume 60% | about 1.8B a day never stored |
| Messages delivered to mailboxes | remaining 1.2B, including spam folder | about 14,000 / s average |
| Outbound sends | assumption | about 400M a day, 4,600 / s |
| Client read calls | 500M daily users x 30 list, open and sync calls | 15B a day, about 174,000 / s |
| New bytes a day | 1.2B x 60 KB average, attachments included | about 72 TB a day before dedup |
| Total stored mail | 1.2B x 9 GB | about 10.8 EB logical |
| Physical footprint | erasure coding at about 1.4x, minus attachment dedup | roughly 13 to 15 EB |
| Search index | about 3% of text-bearing bytes | a few hundred PB, mostly cold |
| Metadata rows | 1.2B mailboxes x about 40,000 messages each | about 48 trillion message records |
Most bytes are never read again. A typical user opens mail from the last few weeks and searches the rest occasionally. That justifies tiering: recent metadata and bodies on fast storage, years of history on cheaper, erasure-coded disks, and a per-user index that can sit cold until the user searches.
Almost every operation touches one user's mailbox. Sharding by user id keeps reads, writes, search and quotas local to one partition, and the only cross-user operation, delivering one message to many recipients, is a fan-out on the write side.
03
Interfaces
Interfaces
There are three faces to this system: SMTP for other mail servers, a JSON API for our own apps, and IMAP for third-party clients. The first two get most of the design attention.
04
Architecture overview
Architecture overview
flowchart LR
EXT["Internet mail servers"] --> MX["SMTP edge fleet"]
MX --> REP["Reputation and auth checks"]
MX --> SPAM["Content classifier"]
MX --> IQ[["Durable inbound queue"]]
IQ --> DLV["Delivery workers"]
DLV --> MB[("Mailbox store, sharded by user")]
DLV --> BLOB[("Blob store for bodies and attachments")]
DLV -. "change event" .-> IDX["Per-user indexer"]
IDX --> SI[("Search index segments")]
APP["Web and mobile apps"] --> API["Mail API"]
API --> MB
API --> SI
API --> OQ[["Outbound queue"]]
OQ --> OUT["Outbound SMTP fleet"]Mail enters through a fleet of SMTP servers that face the open internet. They are designed to be suspicious and cheap: most of the work they do is saying no. Messages that survive the checks are written to a durable inbound queue replicated across zones before the server answers 250, so a crash after that point cannot lose the message.
Delivery workers drain the queue. For each recipient they run the user's filters, choose labels and a tab, find or create the conversation, store the body, and append a row to the user's mailbox. Each change also bumps the user's history id and emits an event that the indexer and the push notification service consume. The Mail API serves clients from the mailbox store and search index, and hands outgoing mail to an outbound queue that a separate SMTP fleet delivers with retries.
Terminates TLS, enforces connection limits per IP, looks up reputation, checks SPF, DKIM and DMARC, and runs a fast content model. It rejects during the SMTP conversation wherever possible, so bounce responsibility stays with the sender.
A wide-column store in the style of Bigtable, with rows keyed by user and then message id. It holds headers, labels, flags, thread ids and a pointer to the body. Each user's rows sit together, so listing a label is a range scan.
Bodies and attachments live here, addressed by content hash, so a 4 MB PDF mailed to 3,000 employees is stored once and referenced many times.
A per-user inverted index built from change events, plus a history log per mailbox that lets every client ask "what changed since id N" instead of re-downloading the inbox.
05
Deep dive: from 250 OK to searchable inbox
Deep dive: from 250 OK to searchable inbox
Saying no early
Most inbound connections are abusive, so the edge works in order of cost. First the IP: a reputation lookup against our own history and shared blocklists can refuse a connection or answer with a temporary 421, which legitimate servers retry and many spam tools do not. Next the envelope: RCPT TO for an unknown address is rejected immediately, which stops directory harvesting. Then the domain: SPF says whether this IP may send for the envelope domain, DKIM proves the domain signed the content, and DMARC tells us what the domain owner wants done when those fail.
Only after all that does the edge spend real CPU on content. A fast classifier looks at headers, URLs, text and sender history and returns a verdict in milliseconds: reject, quarantine, deliver to spam, or deliver. Rejecting during the SMTP session matters. If we accepted and later bounced, our bounce would go to a forged sender address and we would become a source of junk ourselves.
The durability line
SMTP has a clear contract: before 250, the sender keeps the message and will retry; after 250, it is ours. So the edge writes the full message to a queue that is synchronously replicated to a second zone before replying. Everything downstream, from filters to indexing, can then fail and retry without risk, because the source of truth is already safe. If the queue write fails, the edge answers 451 and the sending server simply tries again later.
flowchart TD Q["Message in durable queue"] --> H["Hash body and attachments"] H --> B["Write blobs if hash is new"] B --> F["Split by recipient"] F --> R1["Recipient A: filters, labels, tab"] F --> R2["Recipient B: filters, labels, tab"] R1 --> T1["Find or create thread"] R2 --> T2["Find or create thread"] T1 --> W1["Append mailbox row, bump history id"] T2 --> W2["Append mailbox row, bump history id"] W1 --> E["Change events to indexer and push"] W2 --> E
Labels are an index, not a location
Folders put each message in exactly one place. Labels let a message be in Inbox, Work and Travel at once, and "archive" simply removes the Inbox label. Store the message row once, with its label set as a field, and maintain a secondary row per label: (user, label, reverse_time, message_id). Listing Inbox newest-first is then a short range scan of the Inbox rows for that user. Changing a label rewrites a handful of small rows rather than moving any data, which is why archiving thousands of messages is cheap.
Unread counts per label are kept as counters next to the label rows and corrected periodically by a recount, because counters that drift by one or two are a common source of user complaints.
Threading without trusting clients
The standard way to link replies is the Message-ID of each message plus the In-Reply-To and References headers of replies. When those are present, the delivery worker looks up the referenced ids in a small per-user map and attaches the new message to the same thread. Plenty of clients and mailing list servers mangle those headers, so fall back to a normalised subject (strip "Re:", "Fwd:" and list tags) within a time window and among overlapping participants. Keep the fallback conservative. Merging two unrelated "Invoice" emails into one thread is more annoying than leaving a reply on its own.
Search per user
A single global index over trillions of messages would be enormous and would need a security filter on every query. Mail search is always scoped to one user, so build one small index per mailbox, stored as immutable segments in the same shard as the mailbox. New mail is added to a small in-memory segment and flushed periodically; background merges combine small segments into large ones, the way an LSM tree compacts. Structured operators such as from:, label: and after: become filters on indexed fields, and free text runs against the tokenised subject, body and attachment text.
Most users rarely search, so their segments live on cold storage and are loaded on first query, which costs a second or so once and is then cached. Heavy searchers keep theirs warm. Because the index is derived data, a corrupted or outdated one can always be rebuilt from the mailbox store.
Sync with a history log
Every mutation to a mailbox (new message, label change, deletion) appends an entry to that user's history log and increments its history id. A client keeps the last id it saw and asks for changes since then, receiving a short list instead of the whole inbox. Push notifications only need to say "something changed, fetch history", which keeps them tiny and safe to drop. The log is trimmed after a few weeks; a client that has been offline longer gets a 410 and does a full resync of recent mail.
06
Decisions and their costs
Decisions and their costs
A message can carry many labels, archive is a label removal, and nothing ever moves. It costs extra index rows per label and careful counter maintenance, and IMAP clients that expect folders need a translation layer.
Flexible and cheap to changeMaps directly to IMAP and is simple to store, since each message has one parent. Users who want a message in two places must copy it, and moving large folders becomes real data movement.
Simpler, less usefulPrivacy is structural because a query cannot touch another user's data, indexes stay small, and cold ones cost nothing until used. The cost is billions of small indexes to manage and slower first searches for inactive users.
Fits a per-user workloadEasier to operate as one cluster, but every query needs a user filter that must never be forgotten, and posting lists for common words span billions of users you will never return.
Wrong shape for mailThe sender's server is told immediately and owns the bounce, so we never send backscatter to forged addresses. It requires classifiers fast enough to run inside the session, which limits how heavy the content model can be.
Protects our sender reputationLets heavier models run with no time pressure. But every rejection becomes a bounce we must send, often to an innocent forged address, and storage absorbs all the junk first.
For the second-pass scan onlyThe same file sent to a whole company is stored once. Deletion becomes reference counting, which must be done carefully so that one user's delete never removes a blob another user still points to.
Large savings on business mailEach mailbox is fully self-contained, and deletion is trivial. Storage grows with recipient count, which is painful for newsletters and large internal announcements.
Simpler, costlier07
Failure modes
Failure modes
A job offer from a small company's misconfigured mail server fails DKIM and looks unfamiliar. The candidate never sees it.
Personalise verdicts with the user's history: people they have written to, contacts and past "not spam" clicks outweigh weak global signals. Feed spam and not-spam reports back into training, and show a clear "why is this in spam" note so users learn to check.A freshly registered domain hosts a fake login page that looks harmless at delivery time and turns malicious an hour later.
Check links again when they are clicked, not only on arrival, by routing them through a safety lookup. Warn on lookalike sender domains and on first contact from outside an organisation, and make account protection (second factor, suspicious sign-in alerts) the backstop.An attacker signs a victim up to thousands of newsletters to bury a fraud alert, or a misbehaving system mails one address 50,000 times an hour.
Rate limit per recipient at the edge with temporary failures, so legitimate senders retry later. Collapse repeated near-identical messages, and detect subscription bombs by the burst of confirmation emails.A batch of compromised accounts sends spam through us. Major receivers start rejecting mail from those IPs, and ordinary users' mail bounces too.
Score outgoing mail like incoming mail, cap new and suspicious accounts, and send from separate IP pools by trust level so bad traffic cannot damage good. Watch feedback loops from large receivers and act within minutes.A bug in attachment text extraction stalls the indexer on certain PDFs. New mail arrives in the inbox but does not show up in search.
Isolate slow extraction on separate workers with timeouts, index headers and body first and attachments after, and alert on per-shard index lag. Search can also scan the newest unindexed messages directly as a fallback.A power event takes out one zone holding replicas for millions of mailboxes.
Mailbox data is replicated across zones and blobs are erasure coded across them, so reads continue from survivors. Inbound queues already hold a second copy, so delivery resumes where it stopped and nothing accepted is lost.08
How to run the interview
How to run the interview
Draw the 250 OK line. Explaining exactly when responsibility passes from sender to you shows you understand SMTP rather than treating email like any other API.
Shard by user and say why. Reads, search, quotas and privacy all follow from it. The one cross-user operation is fan-out at delivery.
Layer spam defence by cost. IP, envelope, authentication, then content. Name SPF, DKIM and DMARC and what each one proves.
Model labels as index rows. It answers "how does archive work" and "how is the inbox listed quickly" in one sentence.
Argue for per-user search indexes. Interviewers often push toward one big cluster; explain why mail is different.
Explain sync with a change log. A history id per mailbox makes mobile clients cheap and push notifications trivial.
Remember the outbound side. Sending reputation is a shared resource, and protecting it from compromised accounts is part of the design.
09
How the design grows
How the design grows
Postfix for SMTP, Dovecot for IMAP, folders as directories on disk, and a basic spam filter. Fine for thousands of mailboxes.
A stateless SMTP fleet, mailboxes in a distributed store sharded by user, bodies in object storage, and a web app on top of a JSON API.
Per-user indexes make archive and search a workable habit. Labels replace folders and conversations replace flat lists.
SPF, DKIM and DMARC enforcement, sender reputation, and models trained on user reports, with rejection pushed into the SMTP session.
Incremental sync for mobile, automatic tabs for promotions and updates, and suggested replies and summaries layered on top of the same store.
Try next
Free to read · better with landed
Whiteboard this with landed
landed runs it as a live system design round on the whiteboard and grades your trade-offs.