Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

System Design (High-Level Design) Module 2 – Back-of-the-envelope estimation

Estimation drills: five systems end to end

Five worked estimation drills, from a link shortener to real-time payments: traffic, storage, bandwidth, cache and servers, and the decision each number drives.

  • Intermediate
  • 35 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot

What you will learn

  • Complete an estimate sheet for a system in about ten minutes, from assumptions to server counts
  • Justify each assumption and say how changing it would change the design
  • Recognise when an estimate changes the architecture, such as when writes outgrow one database primary

Before you start

On this page

The previous lessons each taught one estimate. A design round, or a real design review, needs all of them at once, quickly, and in an order that ends in a decision. This lesson works through five systems the same way, from assumptions to server counts, so that the method becomes a habit. Read each drill’s requirements, try the arithmetic yourself before you look at the output, and then compare your numbers and, more importantly, your conclusions.

An estimate sheet in seven steps: assumptions, traffic, storage, bandwidth, memory, servers and partitions, and the decision.Requirements and assumptionsstated out loudTrafficaverage and peak, reads and writesStorageper day, retention, copiesBandwidthingress and egress at peakMemorythe hot set at a hit-ratio targetServers and partitionstarget utilisation, sparesDecisionone sentence the numbers support

The order in which a drill fills its estimate sheet

Text description of the diagram

The diagram is a chain of seven boxes from top to bottom.

  1. Requirements and assumptions, stated out loud.
  2. Traffic: the average and the peak, split into reads and writes.
  3. Storage: per day, over the retention period, with the copies kept.
  4. Bandwidth: ingress and egress at the peak.
  5. Memory: the hot set a cache must hold for a hit-ratio target.
  6. Servers and partitions, at a target utilisation and with spares.
  7. The decision: one sentence that the numbers support.

The goal is never the precision of the numbers. An estimate that is off by 30% but leads to the right decision (this needs partitioning; this fits on one primary; egress will dominate the cost) has done its job. An estimate that is exact to five digits and leads nowhere has not.

How the drills are built

Each drill is a short script that states its assumptions as named constants at the top, so you can see, question and change every one of them. They share a few helpers for units and for rounding to two significant figures:

Helpers shared by the five drills Python · drills/sheet.py
# Helpers shared by the five drills: units, rounding to two significant figures, and the sheet's layout.
import math
import textwrap

DAY = 86_400
YEAR = 365 * DAY


def sig(x, digits=2):
    """x rounded to `digits` significant figures (estimates carry no more)."""
    if x == 0:
        return 0
    value = round(x, digits - 1 - math.floor(math.log10(abs(x))))
    return int(value) if abs(value) >= 10 ** (digits - 1) else value


def rate(per_second):
    return f"{sig(per_second):,}/s"


def size(n_bytes):
    for unit, factor in (("PB", 10**15), ("TB", 10**12), ("GB", 10**9), ("MB", 10**6), ("KB", 10**3)):
        if n_bytes >= factor:
            return f"{sig(n_bytes / factor):,} {unit}"
    return f"{sig(n_bytes):,} B"


def bits(bytes_per_second):
    bps = bytes_per_second * 8
    for unit, factor in (("Tbit/s", 10**12), ("Gbit/s", 10**9), ("Mbit/s", 10**6)):
        if bps >= factor:
            return f"{sig(bps / factor):,} {unit}"
    return f"{sig(bps / 10**3):,} kbit/s"


def servers(peak_rps, per_server_rps, target=0.6, spares=2):
    """Servers for the peak at the target utilisation, plus spares for failures and deploys."""
    return math.ceil(peak_rps / (per_server_rps * target)) + spares


def line(label, text):
    """One row of the sheet, wrapped at 78 characters."""
    print(textwrap.fill(text, width=78, initial_indent=f"  {label:<10}", subsequent_indent=" " * 12))

The drills also need a few capacities that only a load test of real software can give: how many durable writes a second one database primary sustains, how many open connections one server holds, how many requests one app server serves. The scripts use round placeholders (5,000 writes a second for a primary, for example) and say so. In a real design, measure them, and when you cannot, say out loud that they are assumptions.

Requirements: anyone can create a short link to a long URL; opening the short link redirects to the long one; links never expire. Assume 100 million new links a month, and that each link is opened 100 times over its life.

The link shortener's sheet Python · drills/shortener.py
# Drill 1: a link shortener. Create short links; redirect anyone who opens one; links never expire.
import math

from sheet import DAY, bits, line, rate, servers, size

NEW_LINKS_PER_MONTH = 100_000_000
CLICKS_PER_LINK = 100  # redirects per link created (reads : writes = 100 : 1)
PEAK = 3  # busiest hour against the average
RECORD = 500  # bytes per link: code, long URL, owner, times, index entries
COPIES = 3
YEARS = 5
REDIRECT = 400  # bytes of a redirect response
HOT_SHARE = 0.2  # share of a year's links that receive most clicks
ENTRY = 600  # bytes per cached link, with the cache's overhead
APP_RPS = 2_000  # redirects one server sustains (from a load test)

writes = NEW_LINKS_PER_MONTH / (30 * DAY)
reads = writes * CLICKS_PER_LINK
links = NEW_LINKS_PER_MONTH * 12 * YEARS

print("Link shortener")
line("Traffic", f"new links {rate(writes)} on average, {rate(writes * PEAK)} at peak; redirects {rate(reads * PEAK)} at peak")
line("Storage", f"{links / 10**9:.0f} billion links in {YEARS} years x {RECORD} B x {COPIES} copies = {size(links * RECORD * COPIES)}")
line("Bandwidth", f"{bits(reads * PEAK * REDIRECT)} of redirects out at peak")
line("Cache", f"the hottest {HOT_SHARE:.0%} of a year's links: {size(NEW_LINKS_PER_MONTH * 12 * HOT_SHARE * ENTRY)}")
line("Servers", f"{servers(reads * PEAK, APP_RPS)} app servers at 60% of {APP_RPS:,} redirects/s each, plus 2 spares")
line("Decision", f"reads are {CLICKS_PER_LINK} times writes, so a cache in front of the database serves most redirects, while {rate(writes * PEAK)} of writes fit one primary. Codes of 7 characters from 62 letters and digits give {62**7 / links:,.0f} times the codes needed; {math.ceil(math.log(links, 62))} would be the bare minimum.")

Output

Link shortener
  Traffic   new links 39/s on average, 120/s at peak; redirects 12,000/s at
            peak
  Storage   6 billion links in 5 years x 500 B x 3 copies = 9.0 TB
  Bandwidth 37 Mbit/s of redirects out at peak
  Cache     the hottest 20% of a year's links: 140 GB
  Servers   12 app servers at 60% of 2,000 redirects/s each, plus 2 spares
  Decision  reads are 100 times writes, so a cache in front of the database
            serves most redirects, while 120/s of writes fit one primary.
            Codes of 7 characters from 62 letters and digits give 587 times
            the codes needed; 6 would be the bare minimum.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 shortener.py

What the numbers decide: this is a read-heavy service with tiny responses. A hundred redirects per link means a cache in front of the database can serve most of the traffic, and a modest number of app servers handles the peak. Writes are trivial for one primary, and nine terabytes over five years, with copies, fits a small database cluster. The code length is settled by arithmetic, not taste: seven characters from 62 symbols leave hundreds of times the codes needed.

Drill 2: chat

Requirements: one-to-one and small-group messages, delivered at once to recipients who are online, with a year of history kept on the servers. Assume 50 million daily users who each send 40 messages and receive or load 80 a day.

The chat service's sheet Python · drills/chat.py
# Drill 2: one-to-one and small-group chat. Send messages, deliver them to online recipients at once, keep history.
import math

from sheet import DAY, bits, line, rate, servers, size

DAU = 50_000_000
SENT = 40  # messages each user sends a day
DELIVERED = 80  # messages each user receives or loads a day
PEAK = 3
MESSAGE = 200  # bytes stored per message: text, ids, times, index entries
COPIES = 3
KEEP_DAYS = 365  # history kept on the servers
ONLINE_AT_PEAK = 0.25  # share of daily users connected at the busiest moment
PER_CONNECTION_SERVER = 100_000  # open connections one server holds (from a load test)
PRIMARY_WRITES = 5_000  # durable writes one database primary sustains (an assumption to measure)

writes = DAU * SENT / DAY
reads = DAU * DELIVERED / DAY
connections = DAU * ONLINE_AT_PEAK
partitions = math.ceil(writes * PEAK / PRIMARY_WRITES)

print("Chat")
line("Traffic", f"messages sent {rate(writes)} on average, {rate(writes * PEAK)} at peak; deliveries {rate(reads * PEAK)} at peak")
line("Storage", f"{size(DAU * SENT * MESSAGE * COPIES)} a day with copies, {size(DAU * SENT * MESSAGE * COPIES * KEEP_DAYS)} for a year of history")
line("Bandwidth", f"{bits(reads * PEAK * MESSAGE)} of messages out at peak")
line("Servers", f"{connections / 10**6:.1f} million open connections need {servers(connections, PER_CONNECTION_SERVER)} connection servers (60% of {PER_CONNECTION_SERVER:,} each, plus 2 spares); message storage needs {partitions} write partitions")
line("Decision", f"{rate(writes * PEAK)} of message writes at peak is {writes * PEAK / PRIMARY_WRITES:.0f} times what one primary takes, so partition the messages by conversation; the open connections, not the bytes, set the size of the delivery fleet.")

Output

Chat
  Traffic   messages sent 23,000/s on average, 69,000/s at peak; deliveries
            140,000/s at peak
  Storage   1.2 TB a day with copies, 440 TB for a year of history
  Bandwidth 220 Mbit/s of messages out at peak
  Servers   12.5 million open connections need 211 connection servers (60% of
            100,000 each, plus 2 spares); message storage needs 14 write
            partitions
  Decision  69,000/s of message writes at peak is 14 times what one primary
            takes, so partition the messages by conversation; the open
            connections, not the bytes, set the size of the delivery fleet.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 chat.py

What the numbers decide: the message bytes are small (a few hundred megabits a second at the peak), but the write rate is not. Tens of thousands of message writes a second is many times what one primary takes, so messages are partitioned, and partitioning by conversation keeps the messages of one chat together. The size of the delivery fleet is set by something else again: millions of open connections, one per online user, counted with the share of users online at the busiest moment.

Drill 3: a video platform

Requirements: people upload videos, which are transcoded into several sizes and streamed to viewers. Assume 50 million daily viewers who watch an hour each at an average of 3 Mbit/s, and 100,000 uploads a day of 500 MB.

The video platform's sheet Python · drills/video.py
# Drill 3: a video platform. Upload videos, transcode them into several sizes, stream them to viewers.
from sheet import DAY, bits, line, size

DAU = 50_000_000
WATCH_SECONDS = 60 * 60  # an hour of viewing per user a day
BITRATE = 3 * 10**6 / 8  # bytes per second of an average stream (3 Mbit/s)
PEAK = 2
UPLOADS_PER_DAY = 100_000
ORIGINAL = 500 * 10**6  # bytes of an average uploaded video
RENDITIONS = 1.5  # transcoded copies at several sizes, as a multiple of the original
COPIES = 3
CDN_HIT = 0.95  # share of bytes served from caches near viewers

egress = DAU * WATCH_SECONDS * BITRATE / DAY
ingress = UPLOADS_PER_DAY * ORIGINAL / DAY
stored_per_day = UPLOADS_PER_DAY * ORIGINAL * (1 + RENDITIONS) * COPIES

print("Video platform")
line("Traffic", f"{DAU * WATCH_SECONDS / DAY * PEAK / 10**6:.1f} million streams playing at the busiest moment")
line("Storage", f"{size(stored_per_day)} a day with renditions and copies, {size(stored_per_day * 365)} a year")
line("Bandwidth", f"in {bits(ingress * PEAK)} at peak; out {bits(egress * PEAK)} at peak, of which the origin serves {bits(egress * PEAK * (1 - CDN_HIT))}")
line("Decision", f"egress is {egress / ingress:,.0f} times ingress and dwarfs everything else: stream from a CDN, and size the origin for its cache misses, not for the viewers.")

Output

Video platform
  Traffic   4.2 million streams playing at the busiest moment
  Storage   380 TB a day with renditions and copies, 140 PB a year
  Bandwidth in 9.3 Gbit/s at peak; out 12 Tbit/s at peak, of which the origin
            serves 630 Gbit/s
  Decision  egress is 1,350 times ingress and dwarfs everything else: stream
            from a CDN, and size the origin for its cache misses, not for the
            viewers.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 video.py

The concurrent streams come from Little’s law: viewing sessions start at some rate and last an hour each, and the rate times the duration is the number playing at any moment. What the numbers decide: egress is more than a thousand times ingress and runs to terabits a second, which no origin should serve by itself. Video is streamed from a content delivery network, and the origin is sized for the share the CDN does not serve from its caches, here 5%. Storage is the second large figure: about 140 PB a year with renditions and three copies. At that size, erasure coding, with half the raw space of three replicas, is worth its extra CPU and network for the videos that are rarely watched (HDFS erasure coding).

Drill 4: a news feed

Requirements: people post, and each person’s feed shows the newest posts of the accounts they follow. Assume 20 million daily users who load the feed 50 times a day, post once every two days, and have 200 followers on average.

The news feed's sheet Python · drills/feed.py
# Drill 4: a news feed. People post; each follower's feed shows the newest posts of the accounts they follow.
from sheet import DAY, bits, line, rate, size

DAU = 20_000_000
FEED_LOADS = 50  # feed reads per user a day
POSTS = 0.5  # posts per user a day
FOLLOWERS = 200  # average followers of an account that posts
PEAK = 2.5
FEED_PAGE = 20 * 1_000  # bytes of a feed page: 20 items of about 1 KB
TIMELINE_ENTRY = 16  # bytes to store "post id, time" in one follower's timeline
KEEP_PER_USER = 500  # timeline entries kept for each user
COPIES = 3

posts = DAU * POSTS / DAY
inserts = posts * FOLLOWERS  # fan-out on write: one insert per follower
reads = DAU * FEED_LOADS / DAY

print("News feed")
line("Traffic", f"posts {rate(posts * PEAK)} at peak; feed loads {rate(reads * PEAK)} at peak")
line("Fan-out", f"each post goes to {FOLLOWERS} timelines: {rate(inserts)} inserts on average, {rate(inserts * PEAK)} at peak")
line("Storage", f"timelines of {KEEP_PER_USER} entries for {DAU / 10**6:.0f} million users: {size(DAU * KEEP_PER_USER * TIMELINE_ENTRY * COPIES)} with copies")
line("Bandwidth", f"{bits(reads * PEAK * FEED_PAGE)} of feed pages out at peak")
line("Decision", f"writing each post into every follower's timeline multiplies {rate(posts * PEAK)} of posts into {rate(inserts * PEAK)} of inserts, but makes each feed load one cheap read; accounts with millions of followers are better merged in when the feed is read.")

Output

News feed
  Traffic   posts 290/s at peak; feed loads 29,000/s at peak
  Fan-out   each post goes to 200 timelines: 23,000/s inserts on average,
            58,000/s at peak
  Storage   timelines of 500 entries for 20 million users: 480 GB with copies
  Bandwidth 4.6 Gbit/s of feed pages out at peak
  Decision  writing each post into every follower's timeline multiplies 290/s
            of posts into 58,000/s of inserts, but makes each feed load one
            cheap read; accounts with millions of followers are better merged
            in when the feed is read.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 feed.py

What the numbers decide: the interesting figure is neither the posts nor the reads, but the fan-out. Writing every post into each follower’s timeline turns a few hundred posts a second into tens of thousands of inserts a second, and in exchange each feed load becomes one cheap read of a precomputed list. That trade pays while audiences are small. An account with millions of followers would turn one post into millions of inserts, so such accounts are better merged into the feed when it is read.

Drill 5: real-time payments

Requirements: people pay each other from bank account to bank account, and every payment must be recorded exactly once and durably. Assume 30 million daily users who make 2 payments and check their status or history 4 times a day, with a sharp peak factor of 4.

A drill modelled on UPI-style payments

This drill is modelled on account-to-account payments of the kind UPI provides in India, but its numbers are illustrative assumptions, not UPI’s. The traffic lesson compared the same estimate with the Reserve Bank of India’s Payment System Indicators: about 690 payments a second on average would be about 7.6% of a month in which the whole network handled 2,45,089.58 lakh payments.

The payments service's sheet Python · drills/payments.py
# Drill 5: real-time payments between bank accounts. Every payment must be recorded exactly once, durably.
import math

from sheet import DAY, line, rate, sig, size

DAU = 30_000_000
PAYMENTS = 2  # payments per user a day
STATUS_READS = 4  # status and history reads per user a day
PEAK = 4
WRITES_PER_PAYMENT = 4  # the payment, a debit and a credit entry, and an event for other systems
RECORD = 1_500  # bytes per payment across those rows, with index entries
COPIES = 3
KEEP_YEARS = 10
PRIMARY_WRITES = 5_000  # durable writes one database primary sustains (an assumption to measure)

payments = DAU * PAYMENTS / DAY
writes = payments * WRITES_PER_PAYMENT
reads = DAU * STATUS_READS / DAY

print("Real-time payments")
line("Traffic", f"payments {rate(payments)} on average, {rate(payments * PEAK)} at peak; status reads {rate(reads * PEAK)} at peak")
line("Writes", f"{WRITES_PER_PAYMENT} durable writes per payment: {rate(writes * PEAK)} at peak")
line("Storage", f"{size(DAU * PAYMENTS * RECORD * COPIES * 365)} a year with copies, {size(DAU * PAYMENTS * RECORD * COPIES * 365 * KEEP_YEARS)} over {KEEP_YEARS} years")
line("Decision", f"about {sig(writes * PEAK):,} durable writes a second at peak, so one primary database will not be enough: partition the ledger into at least {math.ceil(writes * PEAK / PRIMARY_WRITES)} partitions by payment id, so that each payment's {WRITES_PER_PAYMENT} writes stay inside one of them.")

Output

Real-time payments
  Traffic   payments 690/s on average, 2,800/s at peak; status reads 5,600/s
            at peak
  Writes    4 durable writes per payment: 11,000/s at peak
  Storage   99 TB a year with copies, 990 TB over 10 years
  Decision  about 11,000 durable writes a second at peak, so one primary
            database will not be enough: partition the ledger into at least 3
            partitions by payment id, so that each payment's 4 writes stay
            inside one of them.

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 payments.py

What the numbers decide: a payment is several durable writes, not one, and the peak factor is high. About 11,000 durable writes a second at the peak is more than one primary database will take, so the ledger is partitioned. The partition key decides how hard each payment is. Keyed by payment id, all four writes of a payment land in one partition, so each payment stays one local transaction; an account’s history then needs an index that spans the partitions. Keyed by account, the payer’s and the payee’s entries usually sit in different partitions, so most payments must be coordinated across two of them, either with a distributed transaction, which adds latency and new ways to fail, or as separate local transactions that leave the two accounts briefly inconsistent (Sharding pattern).

Durable writes are also where the latency lesson’s measurements matter: each commit waits for a flush to the drive, and databases raise their throughput with group commit, which makes several transactions durable with one flush (PostgreSQL commit_delay). The storage, under a petabyte for ten years with copies, is the easy part.

Say the result in a sentence

At the end of an estimate, say what it means in one sentence that names the number and the decision: “about 11,000 writes a second at the peak, so one primary database will not be enough”, or “egress is over a thousand times ingress, so we stream from a CDN”. The sentence is what the people in the room remember, and it invites the right challenge: someone can now ask whether the peak factor of 4 is right, which is a better conversation than one about arithmetic.

When an estimate changes the architecture

Most estimates confirm a simple design. Look for the few that change it:

  • Writes outgrow one primary. Then partition the data, and choose the key so that the operations that must be atomic stay in one partition: one server’s disks, processors and network links all run out at some size (Sharding pattern).
  • Data outgrows one machine. Petabytes of objects go to storage built for them, and the copies decide the raw size.
  • Egress outgrows the origin. Then caches close to users carry the bytes, and the origin serves their misses.
  • The hot set outgrows one cache node. Then the cache itself is partitioned, or the hit-ratio target is lowered on purpose.
  • Connections outgrow one server. Then a fleet of connection servers sits in front of everything else, sized by the users online at the peak.
Data Storage Converter Check the storage figures of a sheet in decimal or binary units. Data Rate Converter (Mbps to MB/s) Check bandwidth figures between bytes a second and bits a second.

Key takeaways

  • Fill the sheet in order: assumptions, traffic, storage, bandwidth, memory, servers and partitions, decision.
  • State every assumption as a named number, including the capacities that only a load test can give.
  • Look for the figure that dominates: redirects for a shortener, connections and writes for chat, egress for video, fan-out for a feed, durable writes for payments.
  • Finish with one sentence that names the number and the decision it drives.

Exercise

Exercise · Medium · Python

Fill in the estimate sheet of a paste service

A paste service stores text that people paste and share by link. Write paste_sheet(...) in paste.py so that it returns the six figures of the service's estimate sheet as a dictionary. Its parameters are the assumptions, with the values of the first sample test in brackets:

  • new_per_day: pastes created a day (1,000,000)
  • reads_per_paste: times each paste is read over its life (10)
  • paste_bytes: the average size of a paste (10,000)
  • keep_days: days a paste is kept (365)
  • copies: copies kept of every paste (3)
  • peak: the peak factor against the daily average (3)
  • per_server_rps: requests a second one app server sustains (1,000)
  • target: the highest utilisation allowed per server (0.6)
  • spares: spare servers for failures and deploys (2)
  • hot_share: the share of the kept pastes a cache should hold (0.1)
  • entry_overhead: extra bytes per cached paste (100)

Return these keys:

  • "peak_writes" and "peak_reads": requests a second at the peak, where reads a day are the pastes created a day times the reads of each paste;
  • "stored_bytes": every paste kept for keep_days days, with its copies;
  • "peak_egress_gbit": the peak reads times the paste size, in gigabits a second;
  • "cache_bytes": the hot_share of the kept pastes, each with its overhead;
  • "app_servers": servers for the peak reads and writes together at the target utilisation, rounded up, plus the spares.

The sample tests accept each figure within 10%, as an estimate should, except the server count, which must be exact.

Starter code · paste.py

import math

DAY = 86_400


def paste_sheet(new_per_day, reads_per_paste, paste_bytes, keep_days, copies, peak,
                per_server_rps, target, spares, hot_share, entry_overhead):
    """The estimate sheet of a paste service, as a dictionary of six figures."""
    # Replace this line with your code.
    return {}
The sample tests · test_paste.py
import math

from paste import paste_sheet

FIRST = dict(new_per_day=1_000_000, reads_per_paste=10, paste_bytes=10_000, keep_days=365, copies=3, peak=3,
             per_server_rps=1_000, target=0.6, spares=2, hot_share=0.1, entry_overhead=100)
SECOND = dict(new_per_day=20_000_000, reads_per_paste=50, paste_bytes=4_000, keep_days=30, copies=2, peak=4,
              per_server_rps=2_000, target=0.5, spares=3, hot_share=0.2, entry_overhead=200)


def near(actual, expected):
    """Within 10%, as an estimate should be."""
    return math.isclose(actual, expected, rel_tol=0.10)


def test_rates():
    """computes peak writes and reads a second"""
    sheet = paste_sheet(**FIRST)
    assert near(sheet["peak_writes"], 34.7)
    assert near(sheet["peak_reads"], 347)
    assert near(paste_sheet(**SECOND)["peak_reads"], 46_300)


def test_storage():
    """keeps every paste with its copies"""
    assert near(paste_sheet(**FIRST)["stored_bytes"], 1.095e13)
    assert near(paste_sheet(**SECOND)["stored_bytes"], 4.8e12)


def test_egress():
    """turns peak reads into gigabits a second"""
    assert near(paste_sheet(**FIRST)["peak_egress_gbit"], 0.0278)
    assert near(paste_sheet(**SECOND)["peak_egress_gbit"], 1.48)


def test_cache():
    """sizes the cache for the hot share of the kept pastes, overhead included"""
    assert near(paste_sheet(**FIRST)["cache_bytes"], 3.69e11)
    assert near(paste_sheet(**SECOND)["cache_bytes"], 5.04e11)
    tiny_pastes = dict(FIRST, paste_bytes=200, entry_overhead=300)
    assert near(paste_sheet(**tiny_pastes)["cache_bytes"], 1.825e10)


def test_servers():
    """rounds the servers up and adds the spares"""
    assert paste_sheet(**FIRST)["app_servers"] == 3
    assert paste_sheet(**SECOND)["app_servers"] == 51
A hint

Work in the order of the sheet: rates first (new_per_day / DAY * peak), then storage (new_per_day * keep_days * paste_bytes * copies), then bandwidth from the peak reads (bytes a second times 8, divided by 10**9), then the cache, then math.ceil(total_peak / (per_server_rps * target)) + spares.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 The payments drill estimates about 11,000 durable writes a second at the peak. With one database primary assumed to sustain 5,000, what does that number decide?

    Choose one answer.

    Show the answer to question 1

    Answer: One primary is not enough, so the ledger is partitioned, keyed so that each payment's writes stay in one partition

    Writes cannot be spread over read replicas, and the peak, not the average, must be carried. More than twice what one primary takes means partitioning. A key such as the payment id keeps all of a payment's writes in one partition, so each payment stays a single local transaction; keyed by account, most payments would span two partitions.

  2. Question 2 of 5 In the video drill, which figure shapes the design most?

    Choose one answer.

    Show the answer to question 2

    Answer: Egress, more than a thousand times ingress and terabits a second at the peak

    An hour of viewing per user at 3 Mbit/s adds up to terabits a second, so the design is about delivery: a CDN serves most bytes and the origin is sized for the misses.

  3. Question 3 of 5 A link shortener will create about 6 billion links in five years. Are codes of 7 characters, drawn from 62 letters and digits, enough?

    Choose one answer.

    Show the answer to question 3

    Answer: Yes, 62^7 is about 3.5 trillion, hundreds of times the links needed

    62^7 ≈ 3.5 × 10^12, about 587 times 6 × 10^9. Six characters (about 57 billion codes) would be the bare minimum; the seventh leaves room for growth and for random codes that rarely collide.

  4. Question 4 of 5 A service posts 100 times a second, and each post is written into the timelines of 300 followers on average. How many timeline inserts a second is that?

    Type a number.

    Show the answer to question 4

    Answer: 30000 inserts per second

    100 × 300 = 30,000 inserts a second. Fan-out on write multiplies the write rate by the average audience, which is why accounts with very large audiences are often merged in when a feed is read instead.

  5. Question 5 of 5 Which of these belong on every estimate sheet?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • The assumptions, stated so that someone can disagree with them
    • The copies kept, in the storage figure
    • The decision the numbers drive, in one sentence
    • The peak, not only the average

    An estimate exists to support a decision, so it carries its assumptions, its peak, its copies and the decision itself. Five significant digits claim a precision that no assumption behind them has; one or two are honest.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.