Chapter 14 of 18

Data and failure

Databases, transactions, migrations, concurrency and retries: the failures that only show up in production.

Everything so far in this course can be checked on one laptop by one person. This chapter is about the failures you can't see that way. They need two people pressing a button in the same second, a network that drops the reply and not the request, a server that dies halfway through, or a table with ten million rows in it. Unit tests run one thing at a time on clean data, and a demo has one honest user. Neither can show you any of this, so an agent that has passed every check has told you nothing about it.

An agent won't bring these up by itself, because the happy path works and nobody asked. In our own experiment (one small model, one-function tasks, no real repo), a task about payment webhooks passed the hidden scenarios 27% of the time on a vague brief and 33% with a full line of checks behind it. With a brief that said events arrive more than once and out of order, it passed 100% of the time either way. The mechanical checks in that harness couldn't catch what nobody had named. A neutral reviewer reading the one-shot code rejected all 15 vague runs and none of the worded ones, so someone thinking about webhooks can. But it was the right words that moved the pass rate.

So this chapter gives you the words, and each one comes with a machine you can break. Most of them are real: a real B-tree with real comparison counts, a real transaction log that really survives a power cut, every real ordering of two operations. Where the network or the numbers are simulated, the bench says so.

It ends on the other danger. An agent holding production credentials can do in seconds what used to take a careless person an afternoon, and it has been reported often enough now that you should plan for it. By the end you should be able to say "use a transaction with a unique constraint and an idempotency key" and know exactly what each of those words is holding up.

Start with why the data lives in a separate program at all.

Database

A program whose job is to keep data safe and find it again quickly, while many other programs read and change it at once. Your app asks it questions over a connection. If your app crashes, or the database itself does, what it said it saved is still there.

Agents reach for the nearest thing that works: a JSON file, an array, a local SQLite file. All of those pass a demo with one user. The difference only appears with two copies of your server, two requests at once, or a crash. So choosing the store is your decision, not a detail to leave to the agent.

Say it to your agent

“Don't store orders in a JSON file or in memory. Use Postgres with the connection string from DATABASE_URL. Then prove it survives: create an order, kill the server with kill -9, restart it, and show me the row.”

Table and row

A table is a named collection of records that all have the same fields, like a strict spreadsheet. A row is one record, such as one order. A column is one field, such as its total, and every value in it is the same kind of thing.

Agents copy the shape of the objects in their code straight into tables, often one big table with a text column full of JSON. It runs, and then it can't be searched, checked or joined. Describing the tables yourself is one of the cheapest ways to steer the whole design.

Say it to your agent

“Use three tables: customers, orders and order_items, with one row per thing and no JSON columns for data we filter on. Every list query needs an ORDER BY, ending in the primary key so that the order is stable.”

A table can do more than hold data. It can refuse it.

Schema

Also called Constraints

The declared shape of your data: which tables exist, which columns they have, what type each holds, and the rules each must obey, such as not empty, or never below zero. The database enforces those rules on every write, whoever is writing.

A rule in your code protects the one path that has it. An agent adding a new route, a script or a backfill won't know the rule is there. A constraint in the schema holds against all of them, including the agent itself. It is backpressure that lives in the data.

Say it to your agent

“Add these to the schema, not just the code: total_cents integer NOT NULL CHECK (total_cents >= 0), status text NOT NULL CHECK (status IN ('pending', 'paid', 'refunded')). Then write a test that inserts a bad row with raw SQL and expects an error.”

Primary key

The column that identifies one row and only one, for ever. It must be present and it must be unique. Usually it's a number or a random id with no meaning, because anything with meaning, like an email address, can change.

An agent asked to model users may reach for the email as the key, which works until someone changes their email and every link to them breaks. It may also pick counting numbers for public URLs, which tells anyone how many orders you have and lets them guess the next one. Say what you want.

Say it to your agent

“Give every table a primary key column called id, generated by the database, with no business meaning. Don't use email as a key. Use a random public id in URLs, so nobody can count our orders or guess the next one.”

Foreign key

A column in one table that must match the primary key of a row in another. An order's customer_id has to point at a real customer. The database refuses anything that would leave it pointing at nothing, and you choose what happens when the row it points at is deleted.

Without one, nothing stops an order pointing at a customer who no longer exists, and an agent writing a clean-up script will happily create thousands of them. With one, choose the delete rule yourself. An agent adding CASCADE to fix an error has decided that deleting one customer may silently delete their whole history.

Say it to your agent

“Make orders.customer_id a foreign key to customers.id with ON DELETE RESTRICT. Don't add CASCADE to make an error go away. If a customer must be deletable, ask me what should happen to their orders first.”

One small rule in the schema does more for you under load than almost anything in your code. It is the first part of the sentence this chapter is building towards.

Unique constraint

A rule that no two rows may have the same value in a column, or in a set of columns. The database checks it as each row goes in, so it holds even when two people try in the same millisecond. The loser gets an error.

Asked to stop duplicate signups, an agent writes the obvious thing: look for the email, and insert if it isn't there. That passes every test, because tests run one at a time. In production, two requests can both look before either inserts. The only check that can't be raced is the one inside the database.

Say it to your agent

“A SELECT before the INSERT won't stop duplicates under concurrency. Add a unique constraint on users.email, catch the duplicate key error and return a 409. Add a test that fires 20 signups with the same email at once with Promise.all, and asserts one row.”

The way you talk to all of this is a language older than most programmers, and it is worth being able to read.

SQL

The language for asking a relational database for things. You describe what you want, such as which rows, in what order and how many, and the database works out how to get it. SELECT reads. INSERT, UPDATE and DELETE change. It looks like English, and small mistakes in it can be enormous.

You don't need to write much SQL when an agent writes it for you. You do need to read what it wrote, especially the part after WHERE, because an UPDATE or a DELETE with the wrong condition, or with none, runs without complaint. Ask to see the statement before it runs against anything you care about.

Say it to your agent

“Show me the exact SQL before you run it. For any UPDATE or DELETE, first run a SELECT count(*) with the same WHERE, and tell me how many rows it would change. Wrap it in BEGIN, and don't COMMIT until I say so.”

Query plan

Also called EXPLAIN

The steps the database chose to answer your query: which table to read first, whether to read all of it or use a shortcut, and how to join. You write what you want, and a part of the database called the planner decides how. EXPLAIN shows you its decision.

An agent asked why a page is slow will guess, usually by rewriting code that isn't the problem. The plan isn't a guess. And it can change by itself as a table grows, so a query that was fine on your laptop's 100 rows can be a different query on production's 10 million.

Say it to your agent

“Don't guess why this is slow. Run EXPLAIN (ANALYZE, BUFFERS) on the query against a copy with production-sized data, and paste the plan. Tell me which node takes the time, and how many rows it read compared with how many it returned.”

The commonest thing a plan shows you is a whole table being read to find one row.

Index

A separate sorted structure that lets the database find rows without reading the whole table, like the index at the back of a book. The usual kind is a B-tree, a wide shallow tree you can walk from top to bottom in a few steps, however many rows there are.

On your laptop with 100 rows, a missing index is invisible, so no test or demo will show it. Agents rarely add one unless asked. But they also over-add when asked to make things fast, and every index is more work on every write. Ask for the index a real query needs, and for the plan that proves it gets used.

Say it to your agent

“The orders page filters on customer_id and sorts by created_at. Add an index on (customer_id, created_at) with CREATE INDEX CONCURRENTLY. Show me EXPLAIN ANALYZE before and after on a table with a million rows, and don't add any other indexes.”

N+1 query

One query to fetch a list, then one more for each item in it. A page of 50 orders that looks up each order's customer separately makes 51 trips to the database. Each is fast, and the page is slow. One query can usually fetch them all.

This is the signature performance bug of agent-written code, because the code reads perfectly: loop over the orders, show each customer's name. It passes with 3 rows in the test database. Only a real list shows it, and only if something is counting queries.

Say it to your agent

“This list page has an N+1. Fetch the customers in one query with WHERE id IN (...) or a join. Add a test that loads the page with 50 orders, counts the SQL statements, and fails if there are more than 3.”

ORM

Also called Object-relational mapper

A library that lets you work with database rows as objects in your language, and writes the SQL for you. Prisma, Drizzle, Django and Active Record have one. It's convenient, and it hides how many queries a single innocent line of code will run.

An agent writes ORM code fluently, and the ORM makes the expensive thing look exactly like the cheap thing: order.customer.name might be a field you already have, or a trip to the database. You can't tell from the diff. So turn on query logging and read the SQL it really sends.

Say it to your agent

“Turn on SQL query logging for this test. Load the orders page and paste every statement it runs. Then use the ORM's eager loading so the customer arrives with the order, and show me the log again.”

So far every query has run alone and nothing has gone wrong. Now let things go wrong. The first thing to fail is the machine, in the gap between two statements. This is the second part of the sentence.

Transaction

A group of database changes that succeed or fail as one. You BEGIN, make the changes, then COMMIT. If anything fails before the COMMIT, or the power goes, none of them happened. Moving money is the classic case: the debit and the credit must both happen, or neither.

Agents write each statement correctly and often don't wrap them, because every test passes without it. Tests don't crash between two lines. Production does, on a deploy, an out-of-memory kill or a lost connection. Look at any function with two writes in it and ask what happens if it dies between them.

Say it to your agent

“Creating the order and reducing the stock must happen together, so wrap both in one database transaction. Add a test that throws an error between the two writes and asserts neither is in the table afterwards. Don't call any external API inside the transaction.”

ACID

Four promises a transaction makes. Atomic: all of it or none. Consistent: your rules still hold afterwards. Isolated: others don't see it half-done. Durable: once it's committed it stays, through a crash. It's a word you'll hear in interviews, and each letter is something you can break.

Agents will pick a data store for you, and some of the popular ones give up one of these letters by default or by configuration. Knowing the four lets you ask a real question: what happens to this write if the machine dies a millisecond after you told the user it worked?

Say it to your agent

“Before you pick a store for payments, tell me for each ACID letter what it guarantees and under what settings. In particular, is a write on disk before the client hears yes? Quote the docs for the version we would use.”

The I in ACID works more like a dial than a yes or a no, and it probably isn't set where you think it is.

Isolation level

How much one transaction can see of others running at the same moment. Stronger levels hide more, and may refuse a transaction and make it retry. Weaker levels let strange things through. Dirty read, non-repeatable read and lost update are the names of those strange things.

Every isolation bug needs two transactions at once, so your tests will never hit one, and the agent can't reason its way to one unless you ask. And the default may not be what you assume. Say which level a piece of code relies on, and if you go stronger, say what retries.

Say it to your agent

“This reads a balance and then writes it, so at Read Committed it can lose an update. Run it at REPEATABLE READ, catch SQLSTATE 40001 and retry the whole transaction up to 3 times. Add a test with two concurrent connections that shows the retry happening.”

Race condition

A bug where the result depends on who gets there first. Two people buy the last ticket: both read one left, both subtract one, both write zero. Two sales, one ticket. Each step was right, and the bug was the order they ran in.

An agent's code is usually tested by running one thing at a time, which is the one order that never goes wrong. So a race can sit in green code for months, and show up only when traffic grows. You have to ask for it by name, and for a test that really runs things at once.

Say it to your agent

“This reads the stock and then writes it, which is a race. Write a test that runs 50 purchases at once against a real Postgres, not a mock, with stock set to 10. It must end with exactly 10 sold and stock at 0. Show it failing on the current code first.”

Locks and atomic updates

Also called SELECT FOR UPDATE, Optimistic locking

Three ways out of a race. An atomic update does the maths inside one statement: SET stock = stock - 1. A lock makes others wait: SELECT ... FOR UPDATE. A version check lets everyone try and makes the loser start again. Try them in that order.

Asked to fix a race, an agent often adds a lock inside the program, which works perfectly until you run two copies of the server. The fix has to live where the shared data lives. And the simplest fix is often just moving the arithmetic into the UPDATE.

Say it to your agent

“Replace the read and write with one statement: UPDATE products SET stock = stock - 1 WHERE id = $1 AND stock > 0, and check the row count. Zero rows means sold out. Don't add an in-process mutex, because we run more than one server.”

Deadlock

Two transactions each hold something the other wants, so neither can move. One locked row A and wants B, and the other locked B and wants A. They would wait for ever, so the database notices the cycle, kills one of them, and that one has to try again.

Locks fix races and make deadlocks possible, and an agent fixing the first won't think about the second. They appear only under real concurrency, as an occasional error nobody can reproduce. The fix is boring: take locks in the same order everywhere, and make sure anything that can be killed can retry.

Say it to your agent

“We're getting deadlock detected errors on transfers. Lock the two accounts in id order, lowest first, whichever way the money is going. Catch SQLSTATE 40P01 and retry the whole transaction up to 3 times with jitter. Add a test that runs opposite transfers concurrently.”

Now the other thing production has that your laptop doesn't: it can't stop. Everything you change, you change while it runs.

Migration

A script that changes the structure of a live database, such as adding a column, and is kept in your repo so that every copy can run it in order. The hard part is that, for a while, old code and new code are running against it at once.

An agent treats a migration as one edit next to another, and on a laptop it is. In production the deploy isn't instant, so the old code meets the new schema, or the new code meets the old one. A migration that looks like one tidy line can also lock a busy table while it runs. Neither shows up in tests.

Say it to your agent

“Before you write this migration, tell me what the old code does when this runs and what the new code does before it runs. Set lock_timeout to 2s. Use CREATE INDEX CONCURRENTLY. Don't rename or drop anything in this change.”

Expand and contract

Also called Parallel change

Making a breaking change in safe steps. Add the new thing alongside the old, have the code write to both, copy the old data over, move reads across, stop using the old, and only then remove it. At each step the old and the new code both work.

Told to rename a column, an agent will rename the column. Told "use expand and contract, as separate pull requests", it will produce a sequence you can pause and reverse. It feels slow, and it's the difference between a dull week and an outage. Agents make the tedious version cheap, so there's little excuse left.

Say it to your agent

“Rename users.name to full_name using expand and contract, as separate pull requests. One: add the column. Two: write to both. Three: backfill in batches of 1000. Four: read from the new one. Five: stop writing the old one. Six, at least a week later: drop it. Each must be safe to roll back alone.”

Backup and restore

A backup is a copy of your data, kept somewhere a mistake can't reach it. A restore is getting it back. Until you have really done a restore, you don't know the backup works, and a replica doesn't count, because it copies your mistakes too.

An agent will tell you backups are configured, because a setting says so. That is not evidence that you can get the data back, how long it takes, or that the backup isn't kept in the same blast radius as the thing it protects. Ask it to restore one and count the rows.

Say it to your agent

“Restore last night's backup into a new database called restore_test. Tell me how long it took, compare row counts on the five biggest tables with production, then write that up as a scheduled job that alerts when it fails or when the newest backup is over 25 hours old.”

A transaction protects you inside one database. A real checkout also talks to a card network and a mail server, and they aren't in your transaction. From here on, the failures are between machines.

Partial failure

When some of the work happened and some didn't, and you may not be able to tell which. On one machine, a thing either ran or it crashed. Across a network, you sent a request and heard nothing: it might have failed, or it might have worked and the reply got lost.

Agents write the happy path as a straight line: charge the card, save the order, send the email. Each line is correct, and a crash between any two leaves a state nobody designed. Ask what happens at every gap. Wrapping it all in a database transaction doesn't help, because a rollback can't uncharge a card.

Say it to your agent

“For this checkout, list every point it could crash and what state each one leaves. Then rewrite it so the order and a row in an outbox table commit in one transaction, and a worker makes the charge with the order id as the idempotency key. Add a test that kills it at each point.”

If silence means you don't know, the only safe response is to try again, and make trying again harmless. This is the third part of the sentence, and the one that holds the other two together.

Idempotency

Also called Idempotency key

An operation is idempotent if doing it twice has the same effect as doing it once. A client makes that true of a payment by sending a unique key with the request. The server saves the first result against that key, and a repeat gets that result back and charges nothing.

Agents add retries readily, since retries look like robustness, and a retry on a charge is how a customer pays twice. It doesn't appear in tests, because the test network never loses a reply. Done properly, this is the whole sentence: in one transaction, insert the key under a unique constraint, and only the winner does the work.

Say it to your agent

“Use a transaction with a unique constraint and an idempotency key. Require an Idempotency-Key header on POST /charges. In one transaction, INSERT it into idempotency_keys with ON CONFLICT DO NOTHING, and if no row went in, return the saved response. Test it with 20 concurrent requests using one key: one charge, 20 identical replies.”

Retry and backoff

Also called Jitter, Retry storm

Trying a failed call again, and waiting longer each time. Backoff is the growing wait. Jitter is making each wait random, so that clients which failed together don't all come back together. Without those, retries can keep a struggling server down.

Ask an agent to make something robust and you get a retry loop, often with a fixed delay, at every level. It looks responsible and passes tests. Under a real failure, every client turns one request into several at exactly the moment the server can't cope. Ask for a limit, backoff and jitter, and only on what is safe to repeat.

Say it to your agent

“Retry only on 429, 503 and network errors, at most 3 times, with full jitter: wait a random time from 0 up to min(10s, 200ms x 2^attempt). Honour Retry-After. Don't retry 4xx errors or a POST with no idempotency key. Retry here only, not in the layers above or below.”

Delivery guarantees

Also called At-least-once, At-most-once, Exactly-once

What a message system promises. At most once: it may be lost, and it never repeats. At least once: it won't be lost, and it may arrive twice. Most systems you use give you at least once, so your code has to cope with seeing the same thing again.

Webhook handlers and queue workers are written by agents as if each message came once, in order, because that's how a test sends them. In production that assumption is simply false, and it's written in the docs of the service you're using, which the agent didn't read. This is the trap in our experiment.

Say it to your agent

“Stripe delivers webhooks at least once, so this handler must be idempotent. Record each event id under a unique constraint in the same transaction as the work, and skip ids already there. Add a test that delivers every event twice and asserts we ship once.”

Duplicates are half of that trap. The other half is the one a vague brief almost never gets right.

Out-of-order delivery

Messages can arrive in a different order from the one they were sent in, because a retry of an old one can land after a newer one. If your code treats the latest to arrive as the latest to happen, a late "paid" event can undo a refund.

This is the behaviour nobody asks for, so nobody gets it. In our experiment, an agent told to handle payment webhooks passed the hidden scenarios 27% of the time, and 33% with types, lint, tests and mutation testing all behind it. Told that events arrive at least once and out of order, it passed 100%. One small model and one-function tasks, so read the shape and not the size.

Say it to your agent

“Webhooks arrive at least once and out of order. Dedupe by event id. Don't trust arrival order: on each event, fetch the current state of the payment from the provider's API, and only act on that. Ship exactly once per order, never after a refund. Test all six orderings of paid, refunded and a repeat of paid.”

Timeouts and deadlines

Also called Deadline propagation

A timeout limits one call. A deadline limits the whole job: the moment by which the answer stops being any use, handed down through every call that serves it, so nobody keeps working for a user who has gone.

The network chapter said to set a timeout. An agent then sets one per call, each a round number, and retries inside each. The sum can be far longer than anyone will wait, and the work carries on long after the page has given up. Timeouts are a chain, so they have to be set as one.

Say it to your agent

“Set one deadline of 2 seconds at the edge, pass the remaining time down with each call, and stop work once it has passed. Each outbound call uses the smaller of its own limit and what remains. Retry at the top layer only. Log how much time each hop had left.”

Circuit breaker

A switch around a call to something that might fail. Count the failures, and past a limit stop calling it for a while and fail at once. After that, let one trial call through. If it works, close again. Named after the thing in your fuse box.

A timeout means that every request waits the full time before failing, and when the thing you depend on is down, that's all your capacity waiting. A breaker turns slow failure into fast failure. Agents won't add one unasked, and when they do it deserves a test, since a breaker that's never been open is one you're only hoping works.

Say it to your agent

“Wrap calls to the recommendations service in a circuit breaker: open after 5 failures in 10 seconds, fail at once with the cached list for 30 seconds, then allow one trial call. Log each state change, and add a test that forces it open and checks the page still loads.”

Queue

A list of jobs waiting to be done, usually run by a separate service. One program adds a message, and a worker takes it later and deletes it when it's finished. The caller doesn't wait, and a spike of work waits in the queue and not on your server.

Told to send emails on signup, an agent writes the call inline, and now a slow mail server makes signup slow. Told to use a queue, it may not think about a worker dying halfway, a job that always fails, or a queue growing faster than it drains. Those are the questions to ask.

Say it to your agent

“Move sending the receipt onto a queue. The worker must be idempotent, keyed on the order id. Set the visibility timeout to 5 times the slowest job, send messages to a dead-letter queue after 5 failures, and add an alert on queue depth and on anything in the dead-letter queue.”

Graceful degradation

Also called Load shedding

Doing less when part of the system is unwell, in place of falling over. Hide the recommendations, serve a slightly old list, turn off the search, and keep the page up. You decide in advance what the product can live without, and what it can't.

An agent's default is to wait for everything and then render, so one optional panel can take down your whole checkout. You have to name which parts are optional. And you have to name what must never be faked: a stale recommendation is fine, a stale price at the moment of paying is not.

Say it to your agent

“The product page must render if reviews or recommendations are down: give each a 200 ms budget and a fallback, and log and count each fallback used. The price is not optional. If it's unavailable, turn off buying and say why. Test each with the dependency switched off.”

Not every failure takes two machines. Some are in the numbers themselves, right there in one line.

Money and floats

Computers store most decimals in binary, which can't hold 0.1 exactly, so sums drift. A float is fine for measuring distance, and wrong for money. Store money as a whole number of the smallest unit, such as pence, or use a decimal type, and decide where rounding happens.

An agent will write price * 1.2 in a float if nothing stops it, and the test passes, because the test checks one amount that happens to round well. The bug appears as a total that's a penny out on one invoice in a thousand. Say integers, say which unit, and say who gets the odd penny.

Say it to your agent

“Store money as integer minor units in a column called amount_minor, with the currency beside it. Never use a float for money, in code or in the database. Round once, at the invoice line, half-even, and test a 3-way split of 10.00 that adds up to exactly 10.00.”

Dates and time zones

Also called UTC, Daylight saving

An instant is one point in time, everywhere. What a clock on the wall shows depends on where it is and on that place's current rules. Store instants in UTC and convert when you display. A time in the future, like a meeting, is different: store the wall-clock time and the place.

Tests run in one time zone, on a normal day. So an agent's date code passes, and then a birthday shows up a day early in New York, or a daily job runs an hour off for half the year. Two days a year, some wall-clock times exist twice or not at all. Ask what happens on those days.

Say it to your agent

“Store created_at as timestamptz. For the daily 09:00 reminder, store 09:00 and the IANA zone name, not an offset, and work out each next run from the calendar. Don't add 24 hours. Test it across the Europe/London clock changes of 29 March and 25 October 2026.”

The last group of failures comes from having more than one copy of anything.

Replication

Also called Read replica, Replica lag

Keeping copies of a database on other machines. One copy, the leader, takes the writes and sends them to the followers, which can serve reads. Usually it sends them after telling the client yes, so a follower is a little behind. That gap is called replica lag.

An agent asked to scale reads will send them to a replica, and it works in every test, where there's no lag. In production someone saves their profile, the page reloads from a replica, and the old value comes back. And if the leader dies, writes that had been confirmed but not yet copied are gone.

Say it to your agent

“Send reports to the read replica, and keep anything a user just changed on the primary. After a write, read that user's data from the primary for 5 seconds. Add a metric and an alert on replica lag, and show me what happens to the page if lag reaches 30 seconds.”

Eventual consistency

A promise that all copies will agree if you stop changing things and wait. Until then, different readers can see different answers. It's a fair trade for speed and uptime, as long as the product was designed for it and it isn't a surprise.

An agent writes code that reads back what it just wrote, because that always works locally. Against an eventually consistent store, that code is wrong some of the time. And when two copies took different writes, something has to pick. The default is often last write wins, which means one person's change is silently thrown away.

Say it to your agent

“This store is eventually consistent, so don't read back after writing to get the new value: return it from the write. When two copies conflict, don't use last write wins for the cart. Merge the two sets of items, and add a test with two conflicting writes.”

CAP theorem

When a network splits your copies of the data apart, you have to choose. Either refuse some requests, so nobody gets a wrong answer, or answer them all, and accept that some answers are out of date. You can't have both while the split lasts.

It's a staple of interviews, and an agent can recite it confidently, including the slogan that's wrong. What matters is the question it forces on you: for this one piece of data, during an outage, would you rather show an error or show something stale? A bank balance and a like count have different answers.

Say it to your agent

“For each kind of data here, tell me what the user sees during a network partition: an error, or possibly stale data. Balances must refuse. The product catalogue may be stale. Don't just call the database CP or AP. Say what happens to a read and a write.”

Some data is dangerous because of what it is. And now there is another reader of it.

Personal data

Also called PII

Anything that can be tied to a real person: a name, an email address, a location, an account number. It carries legal duties in much of the world. It gets everywhere, into logs, backups, test databases and, now, an agent's context, which may leave your machine.

Agents log whole request bodies when debugging, seed test databases by copying production, and paste rows into a prompt to reason about them. All of it is helpful, and each one moves personal data somewhere you didn't decide on. A scrubber only catches what has a shape, so name the fields that may leave, and keep the rest out.

Say it to your agent

“Don't log request bodies. Log only these fields: level, event, order_id, status and ms. Add a test that sends a request with a marker email address and fails if the marker appears in the log output. Use generated fixtures, and never copy production rows into a test or into this conversation.”

Which brings the chapter to its last danger. Everything above is a way for good code to fail. This is a very capable program holding the keys.

Agents and production data

An agent with production credentials can delete data as fast as it can type. So keep credentials it can read away from production, give it a read-only role when it needs to look, and put destructive steps behind something it can't talk its way past. A sentence in its instructions doesn't count.

An agent stuck on a problem looks around for a way through, and a token in a file is a way through. It doesn't feel the difference between staging and production, and it may only find out which one it was in by reading the error afterwards. Instructions are guides, and it can read them and do the other thing.

Say it to your agent

“You don't get production credentials. Remove any from .env and from every file you can read, and use the local database. If you need to look at production, I'll give you a read-only role. Propose anything destructive as a migration in a pull request. Never run terraform destroy, DROP or DELETE yourself.”

Do it for real

Reading gets you the words. This gets you the experience behind them, and something to show.

Find a race in your own project

Unit tests can't see this, so you need one test that can. The commit it produces shows you know what a test run in isolation is unable to tell you.

  1. Find code that reads a value and then writes based on it: a stock count, a balance, a usage limit, a check for an existing record.
  2. Have your agent write a test using a real database, not a mock, that runs 50 of that operation at once with Promise.all.
  3. Run it. If it fails, you have a real race. If it passes, make sure it can fail by removing the safeguard and running it again.
  4. Fix it with an atomic update or a unique constraint, then watch the test pass.

Done when: A commit with the concurrent test, and its message showing the failing count before the fix and the right count after.

Make a payment endpoint safe to retry

This is the sentence from the top of the chapter, built and proven. It's a strong answer to "tell me about a production failure you designed against".

  1. In a small project, add POST /charges, which calls a fake payment provider that records each charge in a table.
  2. Write a test client that retries on timeout, and a fake network that loses 20% of the replies. Run 100 charges and count the doubles.
  3. Require an Idempotency-Key. Insert it first, under a unique constraint, in one transaction with the work.
  4. Run it again, and add a test firing 20 concurrent requests with one key. Then kill the server partway through a run, restart it and count again.

Done when: A pull request showing doubled charges before and zero after, including under concurrency, and a note saying what a crash at each step leaves behind.

Take the agent's hands off production

The three reports in this chapter have a cause in common, which is reachable credentials. An hour on this is cheap insurance, and an audit you can point at.

  1. List every credential on your machine and in your repo that reaches something you'd hate to lose. Have the agent search with gitleaks and grep, and check .env files, shell history and CI settings.
  2. Delete or move each one out of reach, and rotate anything that has ever been in a file an agent could read.
  3. Create a read-only database role, and test that DELETE fails with it.
  4. Turn on deletion protection wherever your provider offers it, and try to destroy something to see it refuse.
  5. Restore a backup into a new database, and write down how long it took and how many rows came back.

Done when: A short written audit in your repo: what could reach production before, what can now, the permission denied output, and the restore time.

The build, stage 14 of 18

Two people settle at once

A real race you reproduced and closed, a payment webhook that can arrive twice without charging twice, a migration, and a backup you really restored.

About 120 to 210 minutes, on the same app as every other stage. We ran it first and show what went wrong.

Open stage 14 of the build

Gate 14: Data and failure

0 of 6 right

  1. 1Your signup checks whether an email exists and inserts it if not. Tests pass. In production you find a few duplicate accounts each week, always created within the same second. What is the fix?

  2. 2An agent adds a retry to POST /charges, so a timeout doesn't lose a sale. Support now reports occasional double charges. It proposes storing a key with each charge and checking for it first with a SELECT. Is that enough?

  3. 3You need to rename users.name to full_name. An agent writes one migration and updates all the code in one pull request, and every test passes. You deploy gradually, across several servers. What happens?

  4. 4Say a Stripe webhook handler sets the order to the type of the event it just received, and ships on paid. It has handled thousands of test events perfectly. Which one thing will really happen in production?

  5. 5An agent is stuck on a failing command in staging. It searches the repo, finds a production token in an old config file, and uses it. How do you make sure this can't recur?

  6. 6Someone says "we're safe, we have a replica and nightly backups". Which question tells you the most?

Get every answer right to install it, and to put a file in your repo. Wrong answers cost nothing.

Next chapterPerformance and cost

The workshop

Name a word, or describe what you mean in everyday language.