Build with it · Intermediate

Shipping It

Draft, open for academic review

Get what you built in front of real people, and keep it alive.

Your progress here is recorded once you pass the Agents and Automation exam.

8 modules. 74 lessons.

How the internet reaches your process

9 lessons · 1 h 17

You will be able toTrace one request from a typed URL to your running process and back, and name which layer is at fault when it fails.

  1. 1.1What a server actually isStart7 min
  2. 1.2The first ten minutes on a new machine9 min
  3. 1.3What keeps your process running when you close the laptop9 min
  4. 1.4Why your site is not loading8 min
  5. 1.5What a status code is telling you7 min
  6. 1.6The padlock, and what it does not prove9 min
  7. 1.7What sits in front of your app9 min
  8. 1.8One request, with a stopwatch10 min
  9. 1.9The handshake you keep paying for9 min
An illustrative case, built from documented patternsThe mock test that resolved two waysA coaching centre in Kota moves its test-series site to a bigger machine the night before four thousand students sit a Sunday mock paper.

Sharma Classes runs a weekly test series for about four thousand students. The papers are taken on phones, in a two-hour window on Sunday morning, on a site the centre’s one part-time developer built and keeps running on a rented virtual machine.

The old machine was small and had fallen over twice during a paper. So on Saturday evening the developer built a bigger one, copied the application across, restored the database, checked the site by typing the new IP address into his browser, and at half past ten at night changed the A record for tests.sharmaclasses.in from the old address to the new one.

At six on Sunday morning the phone started ringing.

Some students were seeing the new site, with Sunday’s paper on it. Others were seeing the old site, which was still running, still showed last week’s paper, and had a database nothing was writing to any more. A few were getting a full-page browser security warning. Nobody could work out what separated the three groups, because the same student got a different answer on the hostel wifi and on mobile data.

Three facts were established in ten minutes, with one command each:

  • dig +short tests.sharmaclasses.in @ns1.provider.net: the authoritative nameserver returned the new address. The change had saved.
  • dig +short tests.sharmaclasses.in @1.1.1.1: a public resolver returned the old address. It had asked before the change and was still holding the answer.
  • dig tests.sharmaclasses.in: the record’s TTL was 86400. One day. Nobody had touched it, because it was the default the provider set.

Nothing propagates. Records are pulled and then cached, and every cache holds its copy until the TTL runs out. A resolver that asked at nine on Saturday evening would keep handing out the old address until nine on Sunday evening: the whole of the test window and most of the evening after it. Lowering the TTL at six in the morning changes nothing for a cache that is already holding a copy, because the copy it holds carries the old number with it.

The security warning turned out to be a different fault wearing a DNS costume. The certificate on the new machine had been issued for tests.sharmaclasses.in and nothing else. Students who typed www.tests.sharmaclasses.in, which the old machine had also answered for two years, were meeting a name the new certificate did not cover.

So at ten past six, with the paper opening at nine, there were two options and a real cost on each.

Turn the old machine off. Then every student still resolving to the old address gets nothing at all: a connection refused, for some of them until that evening. It is clean, it is honest, and it would have stopped a large share of four thousand students from sitting a paper they had paid for.

Leave the old machine running and make it forward. Put a reverse proxy on the old box that passes every request through to the new server, so both addresses serve the same site and the same database. The costs are not zero: an extra network hop for everyone still on the old address, a second machine to pay for and to remember to turn off, and a certificate on the old box that has to be valid for every name it answers to, including the www name that was already broken.

The centre’s owner asked a third question, which was whether to postpone the paper by a week. Four thousand students had planned a specific Sunday around it, several had travelled, and a postponement is not free either.

How it played out

The developer kept the old machine alive and put Caddy in front of it, forwarding to the new server, with a certificate covering both the plain name and the www name. That took about twenty minutes and was in place before seven. The paper ran at nine with no further complaints, and nobody was told anything had happened.

The old machine stayed up for six days. It was retired only after dig +short ... @1.1.1.1 had agreed with the authoritative answer for a full day and its access log had been silent for twenty-four hours.

The centre changed three things afterwards. The TTL on every record is lowered to sixty seconds two days before any planned change and raised again a day after it. Certificates are checked with openssl s_client for every name the site answers to, www included, and a scheduled job alerts at twenty-one days remaining, because Let’s Encrypt no longer sends the warning email. And no infrastructure change is made in the twelve hours before a test: a rule written on the office wall rather than in a document nobody opens.

Worth arguing about

  1. Why would lowering the TTL at six on Sunday morning not have helped anybody who was already seeing the old site?

    One answer

    A TTL travels with the answer. A resolver that fetched the record on Saturday evening received it stamped with 86400 seconds and will not ask again until that runs out, whatever the authoritative server now says. Lowering the TTL only affects answers handed out after the change, which is exactly why it has to be done days in advance rather than at the moment you need it.

  2. The old machine served last week’s paper from a database nothing was writing to. What is the more dangerous version of that situation, and how would you avoid it?

    One answer

    The dangerous version is an old machine that is still writing: pointed at its own copy of the database, taking answers from the students who still resolve to it. You then have two live databases, both correct, both incomplete, and no clean way to merge them. Avoid it by making the old address stop being a destination and start being a forwarder: proxy every request to the new server so there is exactly one database, or take the old machine down entirely and accept the outage.

  3. Students on wifi and on mobile data saw different sites. What does that tell you before you run a single command?

    One answer

    That the two devices are asking different resolvers and getting different answers, so the fault is in name resolution or its caching rather than in the server, the application or the certificate. One machine cannot serve two different sites to two people at the same instant; two addresses can. That single observation narrows the search to DNS before any diagnosis begins.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    You changed an A record an hour ago. You see the new server; a colleague still sees the old one. What settles where the problem is, fastest?

  2. 2

    A signed-in user requests a page belonging to another team. Your handler answers 401. What does that cause?

  3. 3

    Your renewal cron ran at day sixty and reported success, but visitors still get the expired certificate. What has most likely happened?

  4. 4

    You turn on your framework’s trust-proxy setting and rate-limit on the leftmost entry of X-Forwarded-For. What have you built?

  5. 5

    Curl reports a 661 ms request from Mumbai to a server in Virginia, of which your handler accounts for 236 ms. Which change removes the most time?

  6. 6

    Behind a load balancer you get a small, steady drizzle of 502s that nothing in your application logs explains. Which mismatch produces exactly that?

Making a deploy boring

10 lessons · 1 h 27

You will be able toShip a code change and a schema change to a live site without dropping a request, and undo either one in under two minutes.

  1. 2.1The key you must not commit8 min
  2. 2.2The same build, everywhere9 min
  3. 2.3Which commit is live right now8 min
  4. 2.4The checks that run without you9 min
  5. 2.5The console clicks nobody wrote down9 min
  6. 2.6Deploying for nearly nothing8 min
  7. 2.7Ending a process without dropping a request8 min
  8. 2.8The copy you are allowed to break8 min
  9. 2.9Changing the schema under a running site11 min
  10. 2.10Undo, in under two minutes9 min
An illustrative case, built from documented patternsThree deploys, or one, before the clinic opensA two-doctor clinic in Nashik whose appointment system is maintained by the receptionist’s nephew, on a Tuesday evening with a promised change and a two-hour window.

The clinic books about ninety appointments a day. Reception starts taking them at half past seven in the morning; the doctors see patients from eight. The system is a small web application on a container platform, running two instances, maintained by the receptionist’s nephew Rohit in the evenings.

Dr Kulkarni had asked for something reasonable. The patients table has a column called name, which is printed on the prescription and shown at the top of the booking screen. She wanted the screen to show what the patient is actually called (Baby, Amma, a short form) while the prescription keeps the legal name. Rohit’s plan was to rename name to legal_name, add display_name, and update the code.

He had Tuesday from eight until ten. The change was one migration and about forty lines.

What stopped him was reading the platform’s deploy documentation properly for the first time. It performs a rolling deploy: it starts a new instance, waits for its health check, sends traffic to it, then stops an old one. For somewhere between thirty and ninety seconds, both versions of the code are running and both are serving real requests.

So shipping the rename and the code together does this. The migration runs. Every request that lands on an instance still running the old code asks for a column called name, which no longer exists, and fails. Reception is not open at nine at night, but two of the doctors’ own phones poll the day sheet, and the pharmacy next door uses the same login.

He found a second problem while looking. The container’s start command was npm run migrate && npm start. With two instances that is two migrations running at the same moment against the same database. It had never caused trouble because every previous migration had been an added table. A rename applied twice does not fail politely.

And a third. The clinic runs a monthly report query that reads the whole patients table and takes about four minutes. An ALTER TABLE wants an ACCESS EXCLUSIVE lock, which conflicts with a plain SELECT. If the report is running when the migration asks, the migration waits, and every query on patients that arrives afterwards queues behind the waiting lock request. A change that takes one millisecond takes the clinic’s booking system offline for as long as the report runs.

That left a decision with a cost on each side.

One deploy tonight. It is done, the doctor gets the feature she asked for two weeks ago, and it takes ninety minutes. The price is a window of failures during the rollout, no safe rollback, because rolling the code back leaves a schema the old code cannot read, and a migration that could stall behind the report.

Expand, migrate, contract. Add display_name as a nullable column tonight and write to both. Backfill in batches. Switch reads on Thursday. Stop writing the old column the following week. Drop it a fortnight later. Nothing ever breaks and every step is individually reversible. The price is three evenings instead of one, a feature the doctor has already waited for, and a conversation Rohit did not want to have, because the previous change had also been late and she had said so.

How it played out

Rohit took the long route and compressed it as far as it would go. Tuesday evening: add display_name as a nullable column, with SET lock_timeout = '3s' on the line before the ALTER, and ship code that writes both columns and reads the old one. The migration ran in four milliseconds at half past nine. Thursday: switch reads to the new column, still writing both. The following Tuesday: stop writing the old one. Two weeks later, name was dropped.

Dr Kulkarni got a working screen on Thursday, two days later than promised rather than a week, because the feature was usable as soon as reads switched.

Three other things changed that evening and outlived the feature. Migrations came out of the container start command into a separate release step that runs once before the new instances roll out. A /version route was added that returns the commit SHA and the instance name, so “is the fix live” stopped being an argument: during the Thursday deploy Rohit refreshed it and watched the answer flip between two SHAs, which is the clearest possible demonstration of why the rename would have broken. And on the following Sunday afternoon, with nothing wrong, he deployed something harmless and rolled it back, timing it: one hundred seconds, and the command is now written at the top of the runbook.

Worth arguing about

  1. The migration and the code that needs it were going to ship in the same deploy. Why does that still break?

    One answer

    Because a rolling deploy runs both versions at once. The migration is applied the moment the release step runs, but old instances keep serving requests for another thirty to ninety seconds and they still ask for the column that has just been renamed. The two versions are not sequential, so the schema has to be compatible with both of them at every instant, which is the whole reason expand, migrate and contract takes three deploys.

  2. Rohit’s fallback plan was to roll the code back if the deploy went wrong. Why would that not have rescued him?

    One answer

    Code rolls back; schema does not. Once the rename has been applied, the previous build asks for a column that no longer exists, so rolling back replaces one broken version with another. Expand, migrate and contract exists precisely to keep the code rollback safe: at every step the old code still works against the current schema, so the reversible thing stays reversible.

  3. What does SET lock_timeout = '3s' actually buy on a table that a four-minute report is reading?

    One answer

    Without it, the ALTER TABLE queues for the exclusive lock and every query arriving afterwards queues behind that request, so the whole table stalls for as long as the report runs and the symptom looks nothing like the cause. With it, the migration gives up after three seconds and fails cleanly, having disturbed nothing. A blocked migration becomes a five-second annoyance you retry in a minute instead of an outage.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    An API key has been pushed to a public repository. Which action actually ends the exposure?

  2. 2

    Why is npm ci the right install command in a pipeline, where npm install is not?

  3. 3

    You curl /version repeatedly while a deploy is running and the commit SHA alternates between two values. What is that telling you?

  4. 4

    A Terraform plan marks your production database as replace. Why is that the line to stop on?

  5. 5

    You run time docker stop on your container and it takes about ten seconds every time. What does that measure?

  6. 6

    Your migration tool wraps every migration in a transaction. You add a CREATE INDEX CONCURRENTLY. What happens?

Knowing what it is doing

9 lessons · 1 h 18

You will be able toAnswer “is it broken, for whom, and since when” in under five minutes using data you are already collecting.

  1. 3.1Reading logs at 2am7 min
  2. 3.2Where the logs go, and what they cost9 min
  3. 3.3Four numbers that describe any service9 min
  4. 3.4One screen that answers the question8 min
  5. 3.5Errors that come to you8 min
  6. 3.6The health check that lies9 min
  7. 3.7Alerts you would get out of bed for8 min
  8. 3.8Deciding how reliable it has to be10 min
  9. 3.9Finding the slow bit10 min
An illustrative case, built from documented patternsFour days of greenA district scholarship portal in Madhya Pradesh whose uptime monitor reported one hundred per cent availability for four days while no application could be submitted.

The portal takes scholarship applications from students at about two hundred government colleges. It runs on four instances behind a load balancer, in front of a managed Postgres. The deadline was the coming Saturday, which is when almost all the traffic arrives.

On Friday morning a clerk in Sagar telephoned to say that applications were not saving. The team’s first reaction was that it was one college’s internet, because the monitoring said the site had been up continuously for eleven weeks.

The monitoring said that because of one route:

JavaScript
app.get("/health", (req, res) => res.json({ ok: true }));

An external uptime service had been polling it every five minutes since launch. It returns two hundred as long as the process is alive and the HTTP layer works. It had been returning two hundred all week from four instances, none of which could reach the database.

The cause was found in about twenty minutes and was old and dull. A staff member had opened a psql session on Monday evening to check a count, run BEGIN, and gone home. The transaction stayed open. Autovacuum could not clean the applications table, which grew and slowed, and a long-running report query took connections and did not give them back. By Tuesday afternoon the pool was exhausted. Every write timed out. The homepage, which is static and served from a CDN, stayed up throughout, which is what everybody looked at.

The log volume was another part of the trap. Submissions were failing with a connection timeout, which was logged, but the same message had been appearing a few times a day for months, from a retry that always succeeded on the second attempt. Nobody had demoted it, so an error that fired forty thousand times on Wednesday looked exactly like an error that had always been there.

The count, once somebody wrote the query, was 11,400 failed submissions over three and a half days, from about 6,000 distinct students, none of whom had complained to anybody at the department. They had assumed the form was hard.

That set up the decision the team argued about on Friday afternoon, once the immediate fault was fixed. The obvious fix is to make the health check tell the truth by querying the database. But the platform is configured so that a failing health check restarts the instance. Four instances polling a struggling database every five seconds, then all restarting together and reconnecting at once, is a way of finishing off a database that was merely unwell, and every one of them would then fail the next check and restart again.

Against that: a check that returns two hundred from a process that cannot serve a single request had just cost six thousand students an application window.

There was a second decision, and it belonged to the department rather than the team: whether to extend the deadline. Extending is not free. The scrutiny committee’s dates are fixed, the disbursal is tied to the term, and every previous extension had produced a fresh set of last-minute applications and a complaint from the colleges that had made their students meet the original date.

How it played out

They separated the two questions the check was being asked to answer. Liveness stayed trivial: it says only whether the process should be restarted, and it depends on nothing outside the process. A new readiness route runs select 1 with an eight hundred millisecond timeout, caches its result for five seconds so ten instances cannot become ten queries a second, and returns 503 rather than a two hundred with a false field, because the load balancer reads the status code and not the body. Readiness routes traffic; liveness restarts. Getting those the wrong way round is what would have produced the restart storm.

They added a synthetic check on a free tier of an external service, running from outside their own cloud, which logs in as a test applicant every five minutes and saves a draft. It exercises the database, so it fails when the database is unreachable.

They set statement_timeout and idle_in_transaction_session_timeout on the application role, which addressed the actual cause rather than its symptom.

And they built the screen they had never had: submissions per hour, drawn against the same hour of the previous week. On the Monday evening the two lines separate and never rejoin. Technical health had been perfect and the product had been dead, and the business count is the only graph that shows it. That chart, printed, is what persuaded the department to extend the deadline by four days.

Worth arguing about

  1. The homepage was reachable throughout. Why is that not reassurance?

    One answer

    The homepage is static and served from a CDN, so it stays up while the application behind it is dead: it never touches the database. A check is only worth what it exercises. What is needed is a synthetic check that does what a user does: sign in, load a page that queries the database, and fail when that fails. One of those is worth twenty pings at the front door.

  2. Why is putting the database check into the liveness probe the dangerous version of this fix?

    One answer

    Liveness answers “should I be restarted”. If it depends on the database, a database blip restarts every instance at once, which drops every connection and makes the database worse, which fails the next probe, which restarts everything again: a fleet-wide restart loop caused by a monitoring decision. Only readiness may depend on anything outside the process, because readiness merely stops traffic being routed to an instance that cannot serve.

  3. Which single graph would have caught this within an hour, and why is it the one small teams skip?

    One answer

    Submissions per hour, compared with the same hour last week. It is skipped because it looks like a product metric rather than an engineering one, and because every technical graph on the dashboard was healthy, which is exactly the point. A form that fails for everybody produces a beautiful chart of fast responses. The business count is the only screen that can see an outage your infrastructure is blind to.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    A user reports an error and quotes the reference shown on your error page. What does that reference buy you that a timestamp does not?

  2. 2

    Four servers each report a p99 latency per minute. Why can you not average the four to get the fleet’s p99?

  3. 3

    You add request_id as an indexed label on your log lines and your log store slows to a crawl. Why?

  4. 4

    Your readiness check queries the database. Ten instances are probed every second. Why cache its result for a few seconds?

  5. 5

    Which of these belongs on a pager at three in the morning?

  6. 6

    You have a 99.9% objective. Why alert on burn rate rather than on a raw error-rate threshold?

Load, cost and the traffic that is not customers

10 lessons · 1 h 33

You will be able toFind your first real ceiling with a load test and raise it using caching, pooling or a queue, without raising the bill.

  1. 4.1The bill, and the traffic that runs it up9 min
  2. 4.2The request you never serve9 min
  3. 4.3Serving from near the user, and what bandwidth costs9 min
  4. 4.4Finding your ceiling before your users do10 min
  5. 4.5The day you run two copies9 min
  6. 4.6It is almost always the database10 min
  7. 4.7Work that does not belong in a request10 min
  8. 4.8The traffic that is not customers9 min
  9. 4.9Files people send you9 min
  10. 4.10Why your email went to spam9 min
An illustrative case, built from documented patternsThe bill from an endpoint nobody was told aboutA two-person handicrafts export business in Jaipur, nine days into the festival season, reading a cloud invoice that is forty-five times last month’s.

Meera sells brass and blue pottery to buyers abroad. The site is a catalogue with an enquiry form, and one feature she is proud of: a box where a visitor pastes a few specifications and gets a product paragraph written for them, in English, by a model API. It was meant for her own use, writing listings. She left it open on the site because it was interesting, and because visitors who used it were about three times as likely to send an enquiry.

The route required no account. It took a text field and called the paid API.

For five weeks it was used thirty or forty times a day and cost around nine hundred rupees a month. On the ninth of October the provider’s dashboard projected forty-one thousand rupees for the month. The budget alert she had set at five thousand had arrived fourteen hours earlier, at two in the morning, in an inbox she reads once a day.

The logs told a plain story. From the fourth of October, a steady eleven requests a second, from a rotating set of addresses, each sending a prompt that had nothing to do with pottery. An unauthenticated route that calls a paid API is a free paid API for the entire internet, and it will be found by scanners within days of going live. Hers had been live for five weeks.

While she was in the invoice she found a second line she had never read. Egress: two point one terabytes. The catalogue’s hero image was a four megabyte photograph of a haveli courtyard, served from the origin at full size to every visitor on every page, and October’s traffic was a festival campaign.

So two costs, arriving together, in the fortnight that carries most of the year’s revenue. They are worth separating, because they have nothing in common except the invoice. The egress is a consequence of a file being larger than it needs to be, served from further away than it needs to be, and it will keep costing money in proportion to her success. The model spend is somebody else using a paid endpoint she left open, and it is not proportional to anything: it is whatever the script decides to do next.

She had also, without meaning to, given the abuser a discount. There was no maximum output length on the request, so every call generated as much text as the model would produce, and output tokens are priced at three to five times input tokens on the plan she was on. A cap of two hundred tokens would not have stopped the abuse and would have cut its cost by most of it.

The decision she had to make that morning was about the description box, and it was not obvious.

Turn it off now. The meter stops within a minute. It also removes the feature during the highest-converting fortnight of her year, on a site whose whole business is enquiries from strangers.

Leave it up and add a limit per IP address today. It is an hour of work. It is also weak in both directions: whoever is doing this is rotating addresses for a fraction of a paisa each, and a college or an office in a country she sells to shares one address between hundreds of people, so a tight per-address limit refuses a crowd of real buyers to inconvenience one script.

Leave it up and build the real control. An account, email verification, a daily quota per person, a hard cap on output length, a monthly ceiling at the provider. Two evenings of work, during which the meter keeps running at roughly four and a half thousand rupees a day.

How it played out

She turned it off within the hour, and the rule she wrote in her notes afterwards was: prefer a cap that breaks a feature over an uncapped bill you have to pay in full. The forty-one thousand rupees was payable; the provider refunded nothing, and was not obliged to.

Over the next two evenings she rebuilt it. The box now needs a free account with a verified email. There is a quota of fifteen generations per account per day, counted in a table, claimed before the call is made and refunded if the provider fails, so a provider error does not silently spend somebody’s allowance. Every request carries a hard maximum output length, because without one a single pathological input generates the model’s maximum. There is a monthly spend ceiling at the provider itself, with an alert at half of it: the alert tells her the money is going, the ceiling is what stops it. And the limiter is a token bucket per account rather than a fixed window, so a burst is allowed and the long-run rate is not.

The feature reopened four days later. Enquiries recovered within a week.

The egress line took twenty minutes. The hero image became a WebP at one hundred and eighty kilobytes, and Cloudflare’s free plan went in front of the site. Her cache hit ratio on the first day was thirty-eight per cent, which she traced to a framework default sending Set-Cookie on every response, including the catalogue pages that are the same for everybody. Most content delivery networks refuse to cache a response carrying a Set-Cookie header, which had silently disabled caching for the whole site. With that removed the ratio settled at ninety-six per cent and the egress line effectively disappeared.

Worth arguing about

  1. She had a budget alert and it fired. Why did it not prevent the bill?

    One answer

    An alert is a notification, usually delayed, sent to a person who may be asleep: hers arrived at two in the morning and was read fourteen hours later, by which time most of the money had gone. Alerts tell you the money is leaving; caps stop it. The controls that act without you are a maximum concurrency, a hard monthly ceiling on the key at the provider, a per-account quota enforced in code, and a bounded queue in front of expensive work.

  2. Why was a per-IP limit the wrong primary control for this particular endpoint?

    One answer

    Because it fails in both directions. Addresses are cheap to rotate, so the abuser pays almost nothing to get around it, while carrier-grade NAT and institutional networks put hundreds of legitimate people behind one address, so a limit tight enough to matter refuses real buyers in blocks. Anything that spends money per call belongs behind an identity: a verified account with a hard daily cap, with a loose per-address limit only as a blunt first layer.

  3. A cache hit ratio of thirty-eight per cent on a catalogue of static product pages. What is the most likely cause, and why does it hide so well?

    One answer

    A Set-Cookie header on responses that should be anonymous, usually from a framework session default. Most CDNs refuse to cache any response carrying one, because a cached response with somebody’s session cookie in it hands that session to the next visitor. It hides well because nothing is broken: the pages render, the site is fast for you, and the only visible symptom is a number on a dashboard nobody reads and an origin bill nobody attributes to it.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    A page that greets the signed-in user by name is served with Cache-Control: public, max-age=300. What is the failure?

  2. 2

    A popular cache key expires and ten thousand requests recompute the same value at once, repeatedly. Which one-line change prevents the recurrence?

  3. 3

    You want to serve 200 requests per second at 100 ms each. What does Little’s Law tell you to size?

  4. 4

    Traffic is past the knee of your capacity curve. Why is shedding load better than queueing it?

  5. 5

    Your serverless functions produce FATAL: sorry, too many clients already under load. What is the fix?

  6. 6

    A user uploads a file called photo.jpg whose bytes are an HTML page. You serve it back from your own domain with the type the browser claimed. What have you built?

Staying alive: the bad day and the day after

10 lessons · 1 h 28

You will be able toRun the first thirty minutes of a real outage from a written runbook, and leave behind a postmortem and a handover that change what happens next time.

  1. 5.1The restore you have never tested8 min
  2. 5.2The short list that actually matters10 min
  3. 5.3Data you must keep, and data you must delete9 min
  4. 5.4Everything expires, and nothing tells you8 min
  5. 5.5Timeouts, retries, and the failure you made worse9 min
  6. 5.6The outage that is not yours8 min
  7. 5.7The first thirty minutes of an outage10 min
  8. 5.8Writing down what happened9 min
  9. 5.9Breaking it on purpose, on a Tuesday8 min
  10. 5.10Writing it down for whoever is next9 min
An illustrative case, built from documented patternsRoll back, and lose twenty-five minutes of bookingsA teleconsultation booking service used by four rural clinics in Vidarbha, at five past nine on a Monday morning, twenty-five minutes after a deploy.

The service books video consultations between village clinics and doctors in Nagpur. Monday between nine and eleven is the busiest window of the week by a distance, because that is when the clinics open and the queue outside is already formed.

The deploy went out at 08:40. It contained a new table, booking_slots, and code that writes a row to it for every booking as well as to the old bookings table, plus an SMS confirmation to the patient’s phone. It had been tested for a fortnight on staging.

At 09:05 the alert fired: more than a third of requests to the booking route returning 5xx, sustained for four minutes. The maintainer, who is one person with a laptop in Nagpur, opened the logs.

What he found first was not a cause. It was an arithmetic problem. Between 08:40 and 09:05, about sixty bookings had succeeded. Each of those had written a row into booking_slots (a table the previous build knows nothing about) and each patient had received a confirmation message telling them to come at a stated time. Rolling back would make those sixty bookings invisible to the application. Sixty people would arrive at four clinics, holding a message from a system with no record of them.

The error itself gave no hint. The stack trace ended in the HTTP client. Latency on the booking route had gone from two hundred milliseconds to thirty seconds. Requests to routes that had nothing to do with booking were also failing, which is the shape of a resource problem rather than a bug in one handler: something is holding workers, and everything queues behind it.

The scope told him a little more, and took a minute to establish. It was all four clinics, not one, so it was not somebody’s connection. It was every route rather than one, which pointed away from the code he had just changed and towards something shared. And it had begun before the alert: the error rate graph, read backwards, rises out of the noise at about 08:52, thirteen minutes before anything told him.

What he did not have was a dependency screen. The service calls three external things (the video provider, the SMS provider and the mail relay) and none of them had a rate or a latency graph. “Is it us or them” is the second question in most incidents, and at 09:06 he had no way to answer it in under ten minutes.

So at 09:07 there were two paths.

Roll back now. The fire stops. The evidence (logs, metrics, the bad artifact) all survives a rollback, so diagnosis loses nothing. The price is sixty invisible bookings, which is not an abstraction: it is somebody telephoning four receptionists and getting names written into a paper book before the patients arrive, and it is a repair job to write afterwards.

Spend ten more minutes understanding it. If it is something small and obvious, the fix is forward and nothing is lost. If it is not, thirty-eight per cent of the busiest two hours of the week continue to fail while he reads.

There was a third question, smaller and easy to skip: whether to say anything publicly before knowing what was wrong. The service has a status page. The instinct is to wait until there is something definite to say.

How it played out

He posted at 09:07, before doing anything else: one line saying that bookings were failing, that somebody was working on it, and that the next update would be at 09:30. The status page is a static page on a different host from the application, which is the only reason it was available. He then rolled back at 09:10. The error rate was falling by 09:12 and clear by 09:14.

The sixty rows were exported from booking_slots to a CSV at 09:20 and read down the phone to four receptionists, who wrote them into the paper book. Two people, forty minutes. Every patient was seen.

The cause was found at 10:20, from the artifact and the logs, with the site up the whole time. The new code called the SMS provider inline, in the request, with no timeout, and Node’s fetch has no default timeout. The provider was slow that morning, not down. Each booking held a worker for thirty seconds. Within a minute every worker in every instance was waiting on the same silent socket, which is why pages that never touch SMS were failing too.

The postmortem recorded the numbers rather than adjectives: forty-two minutes, about 3,100 failed requests, roughly two hundred bookings not made. It also recorded that the failure began at 08:52 and was detected at 09:05: thirteen minutes of ignorance against twenty-nine minutes of response. Three actions, each with a name and a date: an explicit connect and read timeout on every outbound call, smaller than the caller’s own budget; SMS moved into the job queue with a circuit breaker in front of the provider; and a rule that a deploy adding a table ships the day before the code that writes to it. A monthly rollback drill was added, and timed at one hundred seconds.

Worth arguing about

  1. Rolling back cost sixty bookings and forty minutes of telephone calls. Why was it still the right call at 09:07?

    One answer

    Because the cost of rolling back was known and bounded, and the cost of continuing was neither. Sixty rows can be exported and repaired; an outage of unknown length in the busiest window of the week cannot be estimated. Rolling back also destroys no evidence: the logs, the metrics and the bad artifact all survive, and the cause was in fact found an hour later with the site up. The common way a fifteen-minute incident becomes a two-hour one is somebody deciding to understand it first.

  2. What made this particular rollback expensive, and what would have made it cheap?

    One answer

    Shipping the new table and the code that writes to it in the same release. That put data in a shape the previous build cannot read, so undoing the code stranded twenty-five minutes of real bookings. The expand, migrate and contract sequence would have made it cheap: ship the table on its own, then the code that writes to both, then the code that reads the new one. At every point the previous build still works, so the code rollback stays free while only the schema moves forward.

  3. The postmortem records a detection gap of thirteen minutes. Why is that number more useful than the cause?

    One answer

    Because the cause was specific to one missing timeout and will not recur, while thirteen minutes of blindness applies to every future failure whatever its cause. Teams reliably fix the bug and leave the detection gap in place, which is why the next unrelated incident also runs for a quarter of an hour before anybody knows. Splitting the duration into detect, decide and fix tells you where the effort actually belongs.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    Somebody argues that the read replica makes nightly dumps unnecessary. What does the replica not protect against?

  2. 2

    Your only backup is a dump that runs at 02:00. Data is destroyed at 15:30. What have you chosen?

  3. 3

    Why register every vendor account to a role address such as ops@ rather than to a person’s mailbox?

  4. 4

    A circuit breaker has closed and open states. What does the half-open state add?

  5. 5

    Ten minutes into an outage the cause is still unclear. Why roll back before you understand it?

  6. 6

    An incident lasted forty-two minutes; you learned about it at minute thirty-one, from a customer. What should the postmortem’s main action address?

Shipping a model, not just a web app

9 lessons · 1 h 25

You will be able toPut a model behind an HTTP endpoint with a measured latency budget and a stated cost per request, ship a new version by shadow then canary rather than by hope, and detect the week its inputs stop resembling the data it was trained on.

  1. 6.1A prediction endpoint is a web service with a large secret9 min
  2. 6.2Where inference time actually goes10 min
  3. 6.3What a prediction costs, worked out10 min
  4. 6.4Which model, which prompt, which version9 min
  5. 6.5The feature computed two different ways10 min
  6. 6.6Shadow, then canary, then everybody9 min
  7. 6.7The model that quietly stopped fitting10 min
  8. 6.8The model that trains on its own decisions9 min
  9. 6.9What the page says when the model is not there9 min
An illustrative case, built from documented patternsPoint eight nine offline, a coin toss in the officeA college in Coimbatore that scores students for dropout risk so three counsellors can spend their week on the right forty names.

The college has about four thousand students and three counsellors. Before the model there was a list, maintained by a clerk, of students who had missed a lot of classes or failed papers. The clerk was reassigned in June and the list stopped.

The IT department built a replacement. Five years of records, a gradient-boosted tree, an area under the curve of 0.89 on a held-out set that had been kept properly separate. It produces a weekly roster of forty names, ranked. It went live in July.

By the end of August the head of counselling had a complaint that was hard to argue with. The roster kept naming students who, on meeting them, were plainly fine: a topper, twice. And three students had left in that period, none of whom had appeared on any roster.

Nothing in the system was broken. There were no errors, latency was flat, the model file was the one that had been evaluated, and the tests passed. The head of IT spent a day on it and found that the numbers going into the model in production were not the numbers it had been trained on. Three of them, specifically.

days_since_last_login was computed in the training notebook from timestamps in UTC and in the web application from timestamps in IST. For any student whose last activity was in the evening, which is most of them, the two paths differed by one. A feature the model leaned on heavily was systematically shifted.

attendance_pct came, in training, from a warehouse table that a nightly job cleans and back-fills. At serving time it came from the live row, where first-year students have a null until that job has run. Serving replaced the null with zero. Training had replaced it with the column mean. Zero means “never attended”, which is the strongest signal the model has, and every first-year student was being handed it.

programme was normalised in the training pipeline (trimmed and uppercased) and not in the serving path. So “B.Sc “ and “b.sc” arrived as categories the encoder had never seen, and the encoder mapped them to index zero, which is the most common category. Not an error. A confident, quiet lie.

The decision that had to be made was not technical, and it belonged to the head of counselling. The fix would take two to three weeks.

Suspend the model. Then the counsellors need a list from somewhere, and the clerk who used to make one is in another department. Rebuilding it by hand costs about a fortnight of the counsellors’ own time, which is the exact resource the model was bought to protect, in a term that has already started.

Keep using the roster while it is fixed. It is cheap and it is already in everybody’s calendar. It is also close to random, and a name on that list receives a counsellor’s hour that a different student does not receive. Three weeks of that is three weeks of misallocated attention with a false claim of rigour attached to it.

How it played out

They did neither, quite. The ranked roster was switched off the same afternoon, and the same page was refilled with a rule-based list the counsellors already trusted and could explain: attendance below sixty per cent, two or more failed papers, or no login for twenty-one days. That took an afternoon, it is not a model, and it carried the term.

The rebuild took four weeks rather than three. One module, features.py, is now imported by both the training job and the web application, so each feature is computed by exactly one piece of code. A scheduled job takes a hundred real rows, scores them through both paths, and diffs the feature vectors (not the final predictions, the features) failing the build on any mismatch. It found two more divergences in its first fortnight, both introduced by somebody editing one path. The fitted encoder, the scaler and the imputation values are serialised into the same directory as the model and loaded together, with a checksum logged at start-up beside the model version.

The rebuilt model scored 0.81 on the same held-out set. Lower than 0.89, and the first number anybody believed, because it was computed on the features the model would actually be given. It ran in shadow for a day, then as a canary on a quarter of the roster with the counsellors blind to which names came from which list, and went to everybody in November.

Worth arguing about

  1. The test suite passed throughout. Why could tests not have caught this?

    One answer

    Because no piece of code was wrong. The training pipeline computed its features correctly and the serving path computed its features correctly; they simply disagreed. Skew lives in the gap between two correct implementations, so a test of either one in isolation passes. What catches it is a comparison across the two paths on the same real rows: score a sample through both, diff feature by feature, and treat any difference as a failure.

  2. Of the three divergences, which is the most dangerous, and why?

    One answer

    The unseen category mapped to index zero. The timezone shift and the null handling are wrong by a measurable amount and would show up in a distribution comparison; the encoder default silently asserts that an unknown value is the most common category, produces a confident prediction, reports nothing, and gets worse over time as new programmes, plans, devices or country codes appear. A wrong number is recoverable; a wrong number that claims to be an ordinary one is not visible at all.

  3. Why did a score of 0.81 count for more than the earlier 0.89?

    One answer

    Because 0.89 was measured on features the model would never actually receive, so it was a statement about a different program. 0.81 was measured on the features produced by the shared module that also runs in production. An offline number is only worth what its inputs share with the serving path, which is why the diff test and the shared feature code come before any claim about quality.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    A prediction endpoint calls joblib.load("model.pkl") inside the handler. Beyond the wasted time, what else must change when you move it to start-up?

  2. 2

    Why does quantising a language model from 8-bit to 4-bit roughly double the tokens per second during generation?

  3. 3

    A rented GPU costs $0.80 an hour and your product serves one request every ten seconds. What does the arithmetic say?

  4. 4

    A user complains about an answer from last Tuesday. Which record makes the complaint investigable?

  5. 5

    You are canarying a new model at five per cent. Why split by a stable hash of the user ID rather than per request?

  6. 6

    You train tomorrow’s ranking model on today’s clicks. Which logging habit keeps that training set honest?

Security after the short list

9 lessons · 1 h 20

You will be able toEnforce authorisation per object rather than per page, revoke a session or rotate a key without an outage, and run the first day of a breach or an abuse report with evidence preserved and users told the truth.

  1. 7.1Authentication is not authorisation9 min
  2. 7.2Sessions, and taking one back9 min
  3. 7.3The code you did not write9 min
  4. 7.4Changing a key while everything is using it8 min
  5. 7.5Bots, scrapers and the flood you cannot absorb9 min
  6. 7.6Taking money without holding card numbers9 min
  7. 7.7The tools that can do anything8 min
  8. 7.8The first day of a security incident10 min
  9. 7.9The email about one of your users9 min
An illustrative case, built from documented patternsReceipt number 4412A school in Pune during fee week, after a class-nine student changes one digit in a URL and finds another family’s receipt.

The school takes fees online from about nineteen hundred families. Fee week runs Monday to Friday, with a late fee after that, and the portal is the only online route: the alternative is a counter in the office that manages perhaps two hundred families a day.

On Tuesday afternoon a student in class nine looked at the address of his own receipt, /receipt/4411, and typed 4412. He got a receipt belonging to a family he did not know: the parent’s name, their mobile number, the amount paid, the date, and the last four digits of the card. He showed his class teacher, who took him to the office, who telephoned the vendor who built the portal.

The portal checks that somebody is logged in. A middleware does it, on every route, correctly. What it does not check is whether the logged-in parent has anything to do with the receipt being requested. The handler loads the row by ID and renders it. This is broken object-level authorisation, and it is consistently reported as the most common serious flaw in web APIs, largely because nothing about the request is invalid. No tool is required. Somebody changed a number.

The fix is small and everybody could see it: put the ownership into the query, so the row is fetched by ID and account together and a forbidden row and a missing row take the same code path. But the same pattern appears in four places: receipts, transport records, the term invoice, and a bulk endpoint that takes a list of IDs and, in the version written last year, checks the first one.

The vendor found the fourth of those by doing what the lesson recommends and nobody does: opening one object he owned, noting the identifier, and requesting the neighbouring identifier from a second account. Fifteen minutes with a browser and two logins found three of the four holes. The fourth needed a grep for every place a record is loaded by identifier, which produced eleven call sites, of which four went straight to rendering.

One more thing was true and made the week worse. Nineteen hundred receipts is nineteen hundred parents’ names, mobile numbers and payment amounts, walkable in a loop by anybody with an account, and any parent at the school can get an account, because that is what the portal is for. The exposure was not one receipt. It was the fee register of the whole school, available to every family in it.

That is where the decision sat, on Tuesday evening, with the late-fee deadline on Friday.

Take the portal offline until all four are fixed and tested. Nothing further leaks. Nineteen hundred families lose the online route for at least a day, the counter cannot clear the backlog before Friday, and the school would have to waive the late fee for everybody, which the trustees would have to approve and which sets a precedent they had refused twice before.

Patch it live, during fee week. The change is a few lines per handler. It is also a rushed change to the code path that takes money, in the week that takes most of the year’s money, and a mistake there is worse than the leak.

A second decision was harder and nobody wanted it. The school did not know whether anybody had done this before the student did. The access logs would say, and they roll after fourteen days.

How it played out

Before any code was changed, the vendor exported the access logs for the receipt routes to a separate account, because the answer to “did anybody else do this” exists only in evidence and the retention clock was running. That took twenty minutes and it turned out to matter: six weeks earlier, forty-one sequential requests from one address in under a minute, walking upwards through receipt numbers. A scraper, not a parent. No other pattern in the window.

They did not take the portal down. One change went live at half past nine that night: every receipt query became WHERE id = $1 AND account_id = $2, shipped behind the existing kill switch so it could be reverted in seconds, and verified before and after with two real parent accounts on two phones. The other three object types were patched over the following two days in the same shape, one per deploy. The bulk endpoint was changed to authorise every ID in the list rather than the first.

The school then wrote to all nineteen hundred families. What happened, what was visible, that they had found one earlier instance and what it appeared to be, what had been changed, and an address that a named person reads. They did not write “sophisticated attack”, because it was not one. Two families replied angrily and both were telephoned.

Afterwards, three things stayed. A permissions test file (a table of user, object and expected answer, run in CI) which started at twenty-two rows and found one more hole on the day it was written. New URLs use UUIDs, and the ownership check was kept, because an unguessable identifier is a secret that leaks through referrer headers, screenshots and shared links rather than an authorisation check. And the admin surface moved behind an explicit allowlist that answers 404 to everybody else.

Worth arguing about

  1. Would switching receipt IDs from sequential numbers to UUIDs have fixed this?

    One answer

    No. It would have stopped somebody enumerating the whole set with a loop, which is worth having, but an unguessable identifier is a secret and not a permission. It leaks through referrer headers, browser history, screenshots, forwarded links and support tickets, and once it has leaked the request is still authorised. The two are defence in depth together: use opaque identifiers and still scope every query by the owner.

  2. Why export the logs before patching anything?

    One answer

    Because the patch changes nothing about the past, but the retention window keeps expiring: these logs rolled at fourteen days. Scope has to be established from evidence rather than from reasoning about what should have been possible, and “did anybody else use this” is answerable only while the records exist. Preserving first also mirrors the security-incident sequence: keep the evidence somewhere the incident cannot reach, then contain.

  3. Why is a permissions test file unusually valuable for this class of bug?

    One answer

    Because the failure is completely silent. The request is well formed, the session is valid, the query succeeds, the page renders, the status is 200, and nothing appears in any error feed. There is no exception for a monitor to catch and no crash for a supervisor to restart. A table of user, object and expected answer, run in CI, is the only thing that asserts the rule at all, and it runs without a browser, so it is cheap enough to extend every time a new object type is added.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    GET /api/invoices/1042 returns somebody else’s invoice to a signed-in user. Which change removes the whole class of bug most reliably?

  2. 2

    A team chooses stateless tokens, then adds short-lived access tokens with a stored refresh token so they can revoke. What has that achieved?

  3. 3

    Why pin third-party CI actions by commit hash rather than by version tag?

  4. 4

    In a key rotation with an overlap window, which step do people skip, and what does skipping it cost?

  5. 5

    Your payment webhook verifies its signature, but every event fails verification in production and passes locally. What is the usual cause?

  6. 6

    Why should an admin path answer 404 rather than 403 to an unauthorised caller?

Running it for years

8 lessons · 1 h 10

You will be able toDecide what to run yourself and what to buy using a cost per user and a maintenance budget you have actually calculated, keep a system upgraded and legally presentable over several years, and move it to another provider without losing a URL.

  1. 8.1What to run yourself, and what to pay for9 min
  2. 8.2The upgrade you keep postponing9 min
  3. 8.3What one user costs you9 min
  4. 8.4Moving to another provider without losing a URL9 min
  5. 8.5The hour a week that keeps it alive8 min
  6. 8.6Removing a feature, and retiring a site8 min
  7. 8.7The pages every public site needs9 min
  8. 8.8Keeping it going without burning out9 min
An illustrative case, built from documented patternsThe volunteer left, and the free tier endedA children’s library trust in Kochi with a lending site nobody has deployed for eleven months, and thirty days of notice.

The trust runs a lending library for about nine hundred active borrowers, with three thousand one hundred borrower records and nine thousand titles. The site was built in 2023 by a volunteer, Anoop, who did it well and then took a job in Bengaluru in January. Nobody has deployed anything since.

In November two emails arrived within a week. The managed database provider was discontinuing the free tier the site runs on, in thirty days. And the language runtime the application uses reaches end of life in February, after which it stops receiving security patches: a date that had been published three years in advance.

The trust’s position when it looked at what it had:

  • The repository exists and is complete. The README has four steps. Three of them fail on a machine that is not Anoop’s laptop.
  • Nobody at the trust knows how to deploy it. There is a deploy script; it references an environment variable nobody can find the value of.
  • The domain renews on a card belonging to Anoop’s father. Auto-renew is on. Nobody at the trust has access to the registrar account.
  • There is a nightly database backup. Nobody has ever restored one.
  • The librarian uses the lending screen for every issue and return, about seventy a day.

The trustees had thirty days and three options, each with a cost they could name.

Pay for the managed database and buy Anoop’s time. Roughly two thousand rupees a month for the database, plus a weekend or two of a former volunteer’s paid time for the runtime upgrade and a handover. It is money the trust does not have spare, and it puts them back where they started: dependent on one person who lives elsewhere.

Move everything onto a small virtual machine themselves. About five hundred rupees a month, a quarter of the price. It also means somebody at the trust owns an operating system: patches, firewall, backups, restores, a database to upgrade, and a person to be woken. There is no such person. The librarian is a librarian.

Retire the lending half and keep the catalogue as a static archive. Crawl the catalogue into flat HTML, host it free, serve 301s from the old catalogue URLs, export the lending records to CSV, and go back to the paper register the library used before 2023. The running cost falls to almost nothing and the links keep working. It also removes the one screen the librarian uses seventy times a day.

Before choosing, one trustee asked the question that should have been asked in 2023 and had not been: if we take the paid database plan, how do we leave it later. The answer was reassuring and it was not luck: the database is ordinary Postgres, the dump command is standard, and there is no export queue or support ticket between the trust and its own data. Had the volunteer chosen a proprietary store with a vendor-specific query language, the same thirty days would have been a rewrite rather than a bill.

The other quiet fact in the room was that the lending screen is the only part anybody uses daily, and it is also the part that holds state. The catalogue is a read-only view of a table and could live anywhere for nothing. Almost the whole of the trust’s operating burden came from one feature, which is a useful thing to know before deciding what to keep.

How it played out

The trustees bought two weekends of Anoop’s time, which turned out to be the cheapest of the three once they compared money against hours of attention rather than money against money. Buy the stateful thing whose backup, restore and upgrade obligations would eat somebody’s weekends; run the stateless things that recover by restarting.

The first weekend was infrastructure. The runtime upgrade was taken in two steps rather than one, with a staging run each time. The database moved to a paid plan on the same provider, with a dump and restore rehearsed on a scratch instance first and timed at twenty-six minutes: a number now written in the runbook, because that number is the trust’s real recovery time. The domain moved into an account owned by a role address at the trust, with auto-renew on a trust card and a transfer lock enabled. A restore was performed into an empty database and a page opened against it.

The second weekend was documents. A README that a trustee followed on a borrowed laptop while Anoop watched and did not help: it took three attempts and eleven corrections, and every correction was a step he does without noticing. A RUNBOOK with the deploy command, the rollback command, the restore procedure and the date it was last actually tested. A RENEWALS file listing the domain, the card, the database plan, the certificate and both end-of-life dates. And a DECISIONS file with six entries explaining why things are as they are.

They also computed cost per user for the first time: about two thousand five hundred rupees a month across nine hundred active borrowers, roughly two rupees eighty each, almost all of it fixed. Ten times the borrowers came out at about forty paise each, which ended a two-year argument about whether the system would cope with growth.

The librarian now has an hour in the calendar on Friday morning. Eleven months later the site had had one incident: a certificate that failed to renew because a firewall change had closed port 80, caught by the expiry check at nineteen days remaining.

Worth arguing about

  1. The virtual machine was a quarter of the monthly price. Why was paying more the cheaper decision?

    One answer

    Because the comparison is money against hours of attention, and attention was the resource the trust did not have. The saving of fifteen hundred rupees a month buys an obligation to patch, back up, restore, monitor and upgrade a database, with nobody able to do any of it at three in the morning. Buy the stateful things whose failure would ruin a weekend; run the stateless things whose failure means restarting a process.

  2. What made the README test honest, and why can the author not perform it alone?

    One answer

    Somebody else followed it on a clean machine while the author watched and refused to help. The author cannot find the gaps because the missing steps are precisely the ones he performs without noticing: a global install from years ago, an environment variable exported in a shell profile, a directory that already exists. Every question the other person has to ask is a defect in the document, and the count of questions is the measure.

  3. What did the ten-times-users test settle, and why is that the right question to ask?

    One answer

    It settled whether the design needed to change, and the answer was no: at nine thousand borrowers the cost per user falls, because nearly the whole bill is fixed. The test is right because it separates a cost problem from an architecture problem. When the answer is alarming it is almost always one line item (an uncached endpoint, a missing index, an unresized image, an expensive model call) and fixing that line is far cheaper than the rewrite people reach for instead.

Test yourself6 questions on this modulePractice. Nothing is recorded and no score is kept.
  1. 1

    You are deciding what to self-host on a small team. Which heuristic does most of the work?

  2. 2

    Why does a database major-version upgrade need rehearsing in a way an application upgrade does not?

  3. 3

    Your cost per user is fine today. You multiply it by ten times the users and the answer is alarming. What is the usual correct response?

  4. 4

    You are moving to another provider next weekend. Which step has to happen days in advance rather than on the day?

  5. 5

    A feature is removed and its pages have no replacement. What should those URLs return?

  6. 6

    A maintenance task happens roughly once a year and takes half a day. What should you do with it?

What this course is

Most of what stops people shipping is not code. It is a domain that will not resolve, a key committed by accident, a bill that arrives three weeks later, and a backup nobody has ever restored. This course covers the boring machinery between “it works on my laptop” and “strangers are using it” (servers, DNS, HTTP, secrets, deploys, logs, cost, abuse and recovery) with real numbers and real failure modes. You need to have built something small already. You do not need to have deployed anything.

Who wrote it

Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.

More in Build with it

  1. Python, from Zero, for AI89 lessonsFrom your first line of code to your first API call.
  2. Building with AI80 lessonsFrom your first API call to a feature you can trust
  3. Agents and Automation72 lessonsWhat an agent actually is, and when you should build a script instead.
  4. Building Apps with AI87 lessonsBuild working software before you can write it, without fooling yourself.
  5. Data, SQL and Getting to the Answer66 lessonsMost AI problems turn out to be data problems. This is where you learn to ask a database a question and defend the answer.
  6. Knowing If It Works76 lessonsThe least glamorous skill in applied AI, and the one that decides whether your thing actually works.
  7. Context Engineering87 lessonsThe window is a budget. Learn to spend it.