Part 01 · The Backend Mindset

When Software Serves Many Users

ScaleUsers

What changes the moment a program has to serve thousands of people at once instead of one.

⏱ 1 hours · Station 1 of 153

Your app works. Then a thousand people open it.

You build a small notes app. It runs on your laptop, it saves to a file, and it works perfectly. Then a friend shares the link, and that evening a thousand people open it.

You haven't changed a single line of code. So what could possibly go wrong?

Almost everything, it turns out. Two people edit the same note. Five hundred people click "save" in the same second. One person pastes a 2 GB file. Someone types another person's name and reads their notes. The program crashes once, and now it's down for everyone.

This lesson is about that moment: what changes when software stops serving one person and starts serving many. Nearly every topic in this course exists because of it.

The problem: a program built for one person

A program on your own laptop quietly assumes many things that stop being true the moment other people use it.

One userMany users
Who is asking?Always youStrangers, and you have to tell them apart
Where does the data live?In a file next to the programIn one shared place everyone reads and writes
When do things happen?One thing at a time, when you clickThousands of things at once, whenever people click
How much work is there?As much as one person doesThe sum of everyone, with sudden spikes
What if it crashes?You restart itEveryone is locked out until it comes back
Can the user be trusted?It's youSome users are careless, and a few are hostile

None of these is a bug in your code. They are new conditions your code now runs under. A backend is, more or less, the set of answers to the right-hand column.

Think of a tea stall

Making tea for yourself at home is easy. One kettle, one cup, and nobody waiting.

Now picture a tea stall at a busy bus stand at 8 in the morning. Each cup takes the seller one minute. Customers don't arrive neatly, one per minute. They come in clumps: three at once when a bus stops, then nobody for a while.

  • At 30 customers an hour, the seller is busy half the time, and most people get their tea almost at once.
  • At 54 an hour, the seller is busy 90% of the time. There is nearly always a small crowd, and some people wait ten minutes.
  • At 70 an hour, more people arrive than one person can serve. The crowd never shrinks, it only grows.

The seller also has to remember whose cup is whose, share one sugar jar between everyone, and make sure one customer can't stand at the counter for half an hour blocking the rest.

one usermany usersyour appon your laptopnotes.txtonly yourssavenobody else touches ituser Auser Buser Cserverone, for everyonedatabaseone shared copyreqreqreqwritethree at the same moment
With one user, the app and its file sit together and nobody else touches them. With many users, everyone shares one server and one copy of the data, and sometimes they all arrive in the same moment.

Where the analogy stops working. Customers can see the line and decide to leave. A request can't: it just waits until something gives up, which is usually a timeout. And a computer can add a second "kettle" in seconds, but only if the work can actually be split. Some work can't, and that's a whole module of its own.

The building blocks

When software goes from one user to many, five things change. Each one becomes a part of this course.

  1. Shared data. Everyone must see the same truth, so the data moves out of a private file into one shared place, usually a database. (Lesson 7, and Module 6.)
  2. Simultaneous requests. Many requests arrive at the same instant, so the program has to work on more than one at a time. That's concurrency. (Module 4.)
  3. Finite capacity. Every request costs some time. Add enough of them and the server runs out of time, and requests start waiting in line. (This lesson.)
  4. Identity and trust. The program has to know who is asking and what they're allowed to do. (Module 9.)
  5. Blast radius. A crash, or one slow part, now hurts everyone at once. (Module 7.)

Capacity is the one you can put numbers on today, with just two of them:

  • Arrival rate: how many requests come in each second.
  • Service time: how long one request keeps a worker busy.

Multiply them and you get how busy the server is:

busy = arrivals per second × service time ÷ number of workers

10 requests a second × 0.1 s each ÷ 1 worker = 1.0, which means 100% busy, with no spare moment at all.

A second rule, called Little's law, tells you how many requests are inside your system at any moment:

requests in flight = arrivals per second × average time each one spends inside

At 200 requests a second, each taking 0.25 s, there are about 50 requests in flight at every instant. Your server needs room for 50 at once: 50 connections, 50 database calls, 50 pieces of memory. That's a number you can plan for.

Predict first

Twenty users click at the same moment. The server handles one request at a time, and each one takes 100 ms. How long does the last user wait?

Behind the scenes: three users, one server

Here is the same moment as the predict question, slowed down to three users.

Step through it

Three requests arrive together

User AUser BUser CServerDatabase

Step 1/6 Three people click "save" at once

Three requests leave three phones and arrive at the server within the same millisecond. Nothing about them says who should go first.

arrived
A, B, C
running
none

The picture to keep in your head

Every server, however big, is the same shape: requests arrive → wait in a line → a worker handles them → they leave.

Two things decide how long the line gets: how busy the workers are, and how unevenly people arrive. Because arrivals come in clumps, the line doesn't grow steadily. It stays short for a long time, then explodes.

latency vs how busy the server is50%70%80%90%95%going from 50% to 80% busy: wait is 2.5 times longergoing from 80% to 95%: 4 times longer again
The wait doesn't grow in a straight line as the server gets busier. Going from 50% to 80% busy makes the wait a few times longer; going from 80% to 95% makes it several times longer again.

That curve is why engineers never plan to run a server at 100%. The last few percent of capacity are the most expensive ones.

Try it: keep the line short

Move the sliders and watch what happens to the waiting time. Try these in order:

  1. 8 users a second, 100 ms, 1 worker. The server is 80% busy, and the average wait (400 ms) is already four times the work itself.
  2. Push it to 9 users a second. Only one more user per second, and the wait jumps to 900 ms.
  3. Now add a second worker. The wait drops to a few tens of milliseconds. Doubling capacity didn't halve the wait, it nearly removed it.
  4. Go back to 1 worker and set 12 users a second. More arrive than the server can finish, and the line grows forever.
Live lab

How busy is your server?

busy
80%
avg wait
400 ms
avg response
500 ms
Still working, but waiting now rivals the work itself. A few more users and it gets worse fast.

The numbers come from the standard queueing formula (M/M/c, with Erlang C), which assumes people arrive at random. Real traffic is often even clumpier, so real waits are usually a bit worse.

Code from first principles

Step 1: the same server, one at a time and side by side. A real HTTP server, built only from the standard library, where every request takes 100 ms. Then twenty users hit it at the same moment.

package main

import (
	"fmt"
	"net"
	"net/http"
	"slices"
	"sync"
	"time"
)

// Each request needs 100 ms, like waiting for a database to answer.
func work() { time.Sleep(100 * time.Millisecond) }

// serve starts a real HTTP server on a free port and returns its address.
func serve(oneAtATime bool) string {
	var mu sync.Mutex
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if oneAtATime {
			mu.Lock() // only one request may be inside at a time
			defer mu.Unlock()
		}
		work()
		fmt.Fprint(w, "ok")
	})
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		panic(err)
	}
	go http.Serve(ln, handler)
	return "http://" + ln.Addr().String()
}

// load sends `users` requests at the same moment and returns when each one finished.
func load(url string, users int) []time.Duration {
	start := time.Now()
	done := make([]time.Duration, users)
	var wg sync.WaitGroup
	for i := range users {
		wg.Add(1)
		go func() {
			defer wg.Done()
			resp, err := http.Get(url)
			if err != nil {
				panic(err)
			}
			resp.Body.Close()
			done[i] = time.Since(start)
		}()
	}
	wg.Wait()
	slices.Sort(done)
	return done
}

func secs(d time.Duration) string {
	return fmt.Sprintf("%.1f s", d.Truncate(100*time.Millisecond).Seconds()) // rounded down to 0.1 s
}

func main() {
	const users = 20
	fmt.Printf("%d users click at the same moment; each request needs 100 ms\n", users)
	for _, mode := range []struct {
		name string
		one  bool
	}{{"one at a time", true}, {"side by side ", false}} {
		t := load(serve(mode.one), users)
		fmt.Printf("%s: first done after %s, last after %s\n", mode.name, secs(t[0]), secs(t[users-1]))
	}
}

Output (the same in all five languages; times are rounded down to 0.1 s):

20 users click at the same moment; each request needs 100 ms
one at a time: first done after 0.1 s, last after 2.0 s
side by side : first done after 0.1 s, last after 0.1 s

Reading the output:

  • one at a time: the first user is done after 0.1 s, but the last one waits 2 seconds, because each request has to wait for everyone ahead of it.
  • side by side: everyone is done after 0.1 s. Each request spends its 100 ms waiting, and waiting can overlap.
  • Nothing about the work changed. Only how many requests the server lets in at once changed. That single decision is the difference between a 0.1 s app and a 2 s app.

(In Java, the program sends one warm-up request first. The JVM loads classes and opens its first connection on the very first call, and that start-up cost would otherwise land on the first user.)

Step 2: what happens as traffic grows. Step 1 was one burst. Real users arrive at random, a few every second. This program uses the queueing formula for one worker, wait = busy ÷ (1 − busy) × service time, to show the average wait at different traffic levels.

package main

import (
	"fmt"
	"math"
)

const serviceMs = 100.0 // one request keeps the server busy for 100 ms

func main() {
	fmt.Println("one server, one request at a time, 100 ms each (users arrive at random)")
	fmt.Println("users/s   busy   avg wait   avg total")
	for _, rate := range []float64{2, 5, 8, 9, 9.5, 10, 12} {
		busy := rate * serviceMs / 1000 // share of each second the server is working
		switch {
		case busy < 1:
			wait := busy / (1 - busy) * serviceMs // average time spent in line
			fmt.Printf("%7.1f  %4.0f%%  %6.0f ms  %7.0f ms\n", rate, busy*100, math.Round(wait), math.Round(wait+serviceMs))
		case busy == 1:
			fmt.Printf("%7.1f  %4.0f%%  the line never settles down\n", rate, busy*100)
		default:
			extra := rate - 1000/serviceMs // arrivals the server can't keep up with
			fmt.Printf("%7.1f  %4.0f%%  the line grows by %.0f people every second\n", rate, busy*100, extra)
		}
	}
}

Output (the same in all five languages):

one server, one request at a time, 100 ms each (users arrive at random)
users/s   busy   avg wait   avg total
    2.0    20%      25 ms      125 ms
    5.0    50%     100 ms      200 ms
    8.0    80%     400 ms      500 ms
    9.0    90%     900 ms     1000 ms
    9.5    95%    1900 ms     2000 ms
   10.0   100%  the line never settles down
   12.0   120%  the line grows by 2 people every second

Reading the output:

TrafficBusyWait compared with the 100 ms of work
5 users/s50%the same (100 ms)
9 users/s90%9 times as long
9.5 users/s95%19 times as long
12 users/s120%never ends

Compared with step 1: step 1 showed what one burst does. Step 2 shows that even a steady flow causes long waits once the server is nearly full. From 9 to 9.5 users a second is only 5% more traffic, but the wait more than doubles.

Real engineering

  • Keep headroom. Teams usually plan for servers to be around 60–70% busy at their peak, not 95%. The curve above is the reason: the last 30% is where the waiting explodes.
  • Measure the slow users, not the average. An average of 200 ms can hide 1% of users waiting 5 seconds. Engineers watch the 95th and 99th percentile (p95, p99) response times.
  • Waiting and computing are different problems. Running requests side by side (step 1) helps when the time goes to waiting on a database or network. If the time goes to the CPU itself, for example resizing images, you need more cores or more machines. Module 4 covers this in depth.
  • Every shared thing has its own line. Database connections, file handles and calls to other services are all limited. Little's law tells you how many of each you need.
  • Find the knee before your users do. Load-testing tools such as k6, wrk or hey fire realistic traffic at a server, so you can see where the wait starts to climb.

When it breaks

The same five changes, seen as failures. Each one is a common production incident.

Try each case

Pick a failure

normal: 50 requests/s
9:00 pm: 2,000 requests/s for ten minutes
breaks: A burst far above capacity. The line fills in seconds and requests start timing out. The fixes are capacity planned ahead of known events, caching the results page, and a queue or rate limit at the front door.

Think before you read on

Your server is 50% busy at 100 ms per request, and responses average 200 ms. Next month, traffic doubles. Will the average response time double to 400 ms?

Reveal

No, it gets far worse. Doubling the traffic takes the server from 50% to 100% busy, and at 100% the line never settles: responses don't double, they grow without limit. Waiting time doesn't grow in a straight line with traffic. To survive double the traffic, you need roughly double the capacity before the traffic arrives.

Check yourself

1. A server handles 40 requests a second, and each one spends 0.5 s inside it. About how many requests are in flight at any moment?

Answer

About 20, by Little's law: 40 per second × 0.5 s = 20. You'd want room for at least that many concurrent requests, database connections included.

2. Requests spend most of their 100 ms waiting for a database. Will handling them side by side help, even on a single CPU core?

Answer

Yes. While one request waits for the database, the CPU is free to start others. Side by side helps whenever the time is spent waiting, which is true of most web APIs.

3. A server is 90% busy. Traffic grows by just 5%. Why can that make users wait more than twice as long?

Answer

Because the wait grows like busy ÷ (1 − busy). At 90% that's 9 times the service time, and at 95% it's 19 times. Close to full, small increases in traffic cause huge increases in waiting.

4. Name two of the five things that change when software goes from one user to many.

Answer

Any two of: data becomes shared, requests arrive at the same time, capacity runs out, you must know who is asking and what they may do, and a failure now affects everyone.

Your challenge: a server with four workers

Change step 1 so the server handles at most 4 requests at once: not one, and not unlimited. In Go you might use a buffered channel as a semaphore, in Java a fixed thread pool of 4, in Python a ThreadPoolExecutor(4), and in Node.js or Rust a counter of free slots.

Before you run it, predict: when 20 users click at the same moment, when does the last one finish?

You've succeeded when your program prints that the last user finished after 0.5 s (20 requests ÷ 4 at a time × 100 ms), and you can explain why that's better than one at a time but slower than unlimited.

Bonus: why might a real team still choose a limit of 4 over "unlimited"? (Hint: what happens to the database?)

The mental model

Many users turn one program into a shared service. Data has to be shared, requests overlap, identity matters, and one failure reaches everyone. Every server is a line in front of some workers: busy = arrivals × service time ÷ workers, and the wait explodes as busy approaches 100%. So leave headroom, and let requests overlap whenever they're waiting.

Station stamp

Finish the chapter

Done reading? Press Complete to save your progress. XP is Pro-only — on Pro this chapter is worth +10 XP.

Go Pro to earn XP →