We Counted Every Behaviour Production Stripe Billing Has to Survive
A census of the failure surface, taken from a capability that has been built rather than imagined.
Contents
Most writing about subscription billing is a list of things people forget. Five gotchas, seven edge cases, the ten mistakes everyone makes. The lists are usually correct, and they are always short, because nobody has counted.
We have now counted ours.
Production Stripe Billing is a capability package: a specification, an implementation and an acceptance suite for the layer that sits between Stripe and one particular application. Every behaviour in it is tracked as a numbered requirement in a traceability ledger, because the release gate refuses to pass a requirement that claims to be done without naming the test that proves it.
That ledger has never been published as a count. This article is that count, with the method attached.
Every figure below is the output of scripts/blog-census.mjs, which reads the ledger and walks the test directories. It prints the commit it ran against, so any number quoted from it can be traced to a state of the package. These are its results at commit 565e3d9, taken on 23 September 2026.
What a requirement is here
This matters more than the headline, because the headline is meaningless without it.
A requirement is one behaviour that must be true. Not a task, not a user story, not a ticket. Each carries a row naming the file that implements it, the document that specifies it, the test that proves it, the tier that test runs in, and how it was verified.
A requirement is not a test case, and the two numbers are not interchangeable. There are 219 requirements and 447 test cases, and no fixed ratio between them: a requirement about two people cancelling at once needs a dozen assertions, one about an environment variable needs one. Wherever you see a large number attached to billing complexity, it is worth asking which of those two things is being counted, and whether the person counting has told you.
The shape of it
219 requirements 41 categories
25 specification documents 12,120 words
447 test cases across 5 tiers
218 of the requirements are P0 — the set the release gate blocks on. One is P1.
Forty-one categories is the first surprise. Before counting, most people would guess a dozen: checkout, subscriptions, upgrades, cancellation, webhooks, invoices, perhaps a few more. There are forty-one because the obvious ones turn out to be a minority of the work.
First, a subtraction
Twenty-eight of the 219 are not billing behaviour at all.
They cover selling and shipping the package itself: the purchase, authenticated and short-lived delivery of the download, the versioning entitlement that says founding buyers receive every v1.x release, the artefact's own manifest and support policy. Real requirements, gated like the rest, and completely irrelevant to what billing has to survive inside somebody else's product.
Counting them would inflate the headline by roughly a seventh. So they are separated, and this census is about the 191 that remain. The script enforces the split rather than trusting the prose: if a requirement prefix is not placed in a group, it throws instead of quietly reporting a smaller total.
Only three in ten is the billing you are picturing
| What the requirement is for | Requirements | Share |
|---|---|---|
| Commercial operations | 57 | 30% |
| Keeping Stripe and the database in agreement | 41 | 21% |
| Adapting to one particular codebase | 39 | 20% |
| Security, tenancy, agent safety and rollback | 33 | 17% |
| Failure, risk and lifecycle | 14 | 7% |
| Stripe API version discipline | 7 | 4% |
The categories are the package's own, assigned while building rather than while writing this, so they were not chosen to make the table interesting. The six buckets are editorial — ours — and they live in the script rather than in this paragraph so you can regroup them and see what you get.
The first row is what people mean by "add billing": checkout, plans, upgrades, downgrades, proration, cancellation, reactivation, trials, authentication-required payments, entitlements and the customer portal. Fifty-seven requirements. Three in ten.
The other seven are what the tutorial leaves out.
Agreement costs almost as much as operations
Forty-one requirements — a fifth of the package — exist for one purpose: making sure Stripe's view of a subscription and your database's view of it do not drift apart.
They break down as outbound idempotency (13), webhooks (10), durable sync (9), the state machine (4), network behaviour (3) and reconciliation (2).
Notice what is absent from that list. None of those requirements make a customer able to do anything. No feature ships because of them. A product manager would struggle to justify a single one, and a demo works perfectly without all forty-one.
They exist because two systems holding the same fact will eventually disagree about it, and because the ways they disagree are not intuitive. Stripe does not guarantee the order events arrive in. A retry can deliver a three-day-old event after a fresh one. A subscription updated twice in the same second can arrive in either order.
The comparison worth sitting with is this: agreement (41) is within sixteen requirements of every commercial operation combined (57). Checkout, plans, upgrades, downgrades, proration, cancellation, reactivation, trials, entitlements and the portal — the entire visible product — is not much more expensive than the invisible machinery that keeps two ledgers honest about it.
A fifth of it is not about billing either
Another thirty-nine requirements exist because the capability has to land in an application that already exists and was not designed around it.
Host contracts alone account for twelve. They define what the capability needs from its host — how to identify a tenant, how to check an administrator, how to run a background job — and they have their own conformance tier so a host can prove it satisfies them before anything is installed. The rest are preflight checks, install behaviour, database expectations, runtime and serverless constraints, and billing UI states.
This part has no equivalent in a tutorial, because a tutorial starts from an empty project where nothing needs adapting. It is also the part that is actually being bought. The knowledge that webhooks need idempotency is freely available. The mapping of that knowledge onto a codebase that calls its tenants workspaces, authenticates through a library you did not choose, and runs on a platform that kills long-lived processes is not.
What "verified" is allowed to mean
The ledger records how each requirement was checked, and this is the number most likely to be quietly inflated anywhere else.
automated 166
inspection 41
manual gate 12
Automated means a named test proves it. One hundred and sixty-six requirements name a test, and 122 distinct tests cover them, because tests are shared across related behaviours — one concurrency test can hold several requirements at once.
Fifty-three requirements name no test. Forty-one are verified by inspection, meaning a person read the code or the document and signed it off. Twelve are verified by a manual gate: a release checklist item such as a legal review or a live commercial verification.
We could have called all 219 "tested" and nobody would have checked. The reason the ledger separates them is that a requirement marked automated and done with no test named is a release-gate failure, which forces the distinction to be real for the build to pass.
If you take one thing from this census as a standard rather than as information, take that. The interesting question about any billing implementation is not how many behaviours it handles. It is how many of them something other than a person's memory is checking, and whether anyone has written down which is which.
The tiers, and why their shape matters
The 447 test cases are not one suite. They run at five tiers, and the tier is part of what a requirement's verification means.
| Tier | Cases | What it proves |
|---|---|---|
| offline | 394 | Behaviour with no network, against a real Postgres |
| sandbox | 23 | Behaviour against a live Stripe sandbox |
| reference installs | 16 | The capability adapting into real applications |
| host conformance | 11 | A host satisfying the contracts before install |
| test clock | 3 | Time-dependent behaviour: renewal, grace, expiry |
Eighty-eight per cent of the assertions run offline. That distribution is deliberate and it is the opposite of how most billing integrations are tested, which is by clicking through a sandbox and watching for a green tick.
The reason is boring and decisive. A suite that needs the network to tell you whether cancellation is correct is a suite that gets run before releases instead of on every change, and a test you run monthly catches bugs monthly. The offline tier runs against a real Postgres rather than a mock, so assertions about locking and constraints test the database that will actually be there, not a fake that agrees with the code by construction.
The small tiers are small on purpose rather than by neglect. Three test-clock cases sounds thin until you notice that simulated time is expensive and only a few behaviours genuinely need it: renewal, the grace window, and expiry.
The largest categories, and what they say
Six categories carry ten or more requirements each: agent safety (13), outbound idempotency (13), host contracts (12), cancellation and reactivation (10), security (10) and webhooks (10).
Only one of those — cancellation and reactivation — is a feature. A customer can perceive it. The other five are invisible: how the installing agent is constrained, how the same operation avoids being performed twice against Stripe, what the host must provide, what is enforced on the server, and how inbound events are handled without trusting their order.
Cancellation earns its place for a reason worth stating. It looks like the simplest thing in billing and it is not. Cancel now, or at period end. Access until the paid period expires, or immediately. Reactivation before the period ends, which is a different operation from subscribing again. Terminal cancellation, after which reactivation must not silently succeed. Ten requirements is what it took to make those distinctions explicit, and every one is a product decision rather than a technical constraint.
Explanation does not track requirement count
The specification runs to 12,120 words across 25 documents — an average of 55 words per requirement. The average is useless, because the variance is the finding.
| Document | Words |
|---|---|
| scope.md | 951 |
| outbound-idempotency.md | 945 |
| proration.md | 758 |
| webhooks.md | 659 |
| architecture.md | 657 |
| host-contracts.md | 640 |
Proration is the interesting row. It carries two requirements and the third-longest document in the package. Seven hundred and fifty-eight words to pin down two behaviours.
That happens when the rules are simple to state and almost impossible to guess. Proration is arithmetic the payment provider performs, on a schedule you did not choose, producing a number your customer will compare against their own mental model and occasionally dispute. Almost none of that becomes code in the capability. Nearly all of it has to be written down, because the failure mode is not a crash — it is an invoice somebody argues with.
The inverse appears too. Outbound idempotency has thirteen requirements and 945 words: dense in both, because every one of those behaviours is a rule the code must enforce and a person must understand before changing it.
Anyone sizing a billing project from a requirement count alone will therefore get proration wrong. Some behaviours are cheap to implement and expensive to decide.
Thirty files
The 166 requirements that name an implementation file point at just 30 distinct files.
That ratio is worth noticing, because it is the argument against treating this as a code problem. Two hundred and nineteen behaviours, painstakingly enumerated and individually verified, land in thirty files that a competent agent could generate in an afternoon given the specification. The scarce artefact is not the thirty files. It is knowing which 219 things they have to do.
The one that is still partial
Of 219 requirements, 218 are done and one is not. It is worth ending on, because it is the most honest row in the ledger and the hardest to automate away.
REQ-SEC-002 requires that every user-initiated billing mutation resolves identity and checks a canManageBilling predicate on the server, from the session, before the mutation. Upgrade, downgrade, cancel, reactivate, portal session, checkout — all of them, every path.
The package supplies the predicate. A host-conformance test proves the predicate itself works. What no test in the package can prove is whether your handlers actually call it on every route, because that is a property of code the package never sees.
The specification says so in as many words: a client-side guard, or a check on four of the six paths, passes every test here and is wrong.
That requirement is marked partial and verified by inspection rather than quietly marked done, and it will stay that way, because the honest status of "we cannot check this for you" is not done. It is the clearest illustration of where a reusable capability stops. Everything up to the boundary of the installed code can be specified and proven. The last step — did you wire it in correctly, everywhere — belongs to whoever owns the repository, and the most a package can do is tell them exactly where to look and admit that it is looking away.
Most security incidents in billing are not exotic. They are the fifth of six paths.
What this does not prove
The census covers one capability with a defined scope. It is not a claim about billing in general, and three limits are worth stating plainly.
It is scoped. The package's scope document draws a boundary. Usage-based billing, tax, multi-currency pricing and invoicing workflows sit outside it and therefore outside the count. A product needing those has a larger surface than 191, not a smaller one.
It is a snapshot. The ledger read 218 done and one partial on 23 September. An earlier progress report published 205 done with eleven still to do, a fortnight earlier. The number moves, which is exactly why the commit is printed beside it.
The grouping is editorial. The forty-one categories belong to the package; the six buckets are ours. Someone could reasonably argue that entitlements are agreement rather than a commercial operation, or that agent safety does not belong in a billing census at all. The per-category data is in the script's JSON output and regrouping it is a two-minute job.
Using it as a checklist
The practical value of a census is that it turns into questions about your own implementation. The ones worth asking first are where our count is highest and a tutorial's is zero.
If the same webhook arrives twice, does the business effect happen exactly once — and does it still happen exactly once when the first attempt recorded the event and then failed before finishing the work?
When your database and Stripe disagree about a subscription, which one wins, and what process notices without being asked?
When a customer cancels, what exactly happens to access, and when? Is that computable from stored state, or does it depend on an event arriving on time?
If a payment fails, how long does access survive, and is that deadline derivable from a snapshot rather than from history you did not keep?
Who is allowed to change billing for an organisation, and is that checked on the server for every path — including the ones your interface does not offer?
Five questions, drawn from five of the largest categories in the count. None is exotic. All are ordinary subscription behaviour, and each is a place where a plausible implementation tends to stop being correct.
The categories, for checking against your own
Every category with five or more requirements. The full set of forty-one, including the long tail of two- and three-requirement groups, comes out of the script as JSON.
| Category | Reqs | With a test | How the rest are verified |
|---|---|---|---|
| Safety | 13 | 3 | 10 by inspection |
| Outbound idempotency (OUT) | 13 | 13 | — |
| Host contracts | 12 | 12 | — |
| Cancellation reactivation | 10 | 9 | 1 by inspection |
| Security | 10 | 7 | 3 by inspection |
| Webhooks | 10 | 8 | 2 by inspection |
| Durable sync | 9 | 9 | — |
| Downgrades and schedules | 7 | 6 | 1 by inspection |
| Upgrades | 7 | 7 | — |
| Commercial verification (COM) | 6 | 0 | 6 by manual gate |
| Entitlements | 6 | 6 | — |
| Portal configuration | 6 | 6 | — |
| Rollback | 6 | 0 | 6 by inspection |
| Trials and SCA (SCA) | 6 | 6 | — |
| Stripe source register | 5 | 4 | 1 by inspection |
| Install (BOOT) | 5 | 5 | — |
| Data model | 5 | 4 | 1 by inspection |
| Commercial verification (DEL) | 5 | 0 | 5 by inspection |
| Failed payments | 5 | 3 | 1 by manual gate, 1 by inspection |
| Subscriptions | 5 | 5 | — |
| Refunds and disputes | 5 | 4 | 1 by manual gate |
| Runtime and serverless | 5 | 2 | 3 by inspection |
Read it as a prompt rather than a scoreboard. If a row here carries eight requirements and the equivalent area of your system has one function and no test, that gap is not necessarily wrong — your scope may genuinely be smaller — but it is worth knowing you have made that choice rather than discovering it during an incident.
Reproduce it
The script lives in the repository that publishes this blog.
node scripts/blog-census.mjs # the tables above
node scripts/blog-census.mjs --json # full per-category data
It reads capability/production-stripe-billing/reference/traceability.csv, walks the test directories, and refuses to print a grouping that does not account for every requirement.
We would rather publish the method than the conclusion. A census whose method is "we looked" is an opinion with a number attached, and billing already has plenty of those.

