Building an MRR Waterfall and Reconciling It to Stripe

Net MRR grew by $40 one month. I nearly closed the report and went to lunch. Then I looked underneath the number, and the base had lost three customers to churn while two upgrades papered over the hole. Flat on top, hemorrhaging underneath.

An MRR waterfall bridges beginning MRR to ending MRR by decomposing the net change into four gross buckets — new, expansion, contraction, and churn (with reactivation as an optional fifth) — so a flat net figure can't hide a leaking base. Build it from subscription-event and invoice history, not Stripe's current subscription object, or it won't tie out.

That's the whole thing. The rest of this is how to build one that survives a CFO with a highlighter.

What an MRR waterfall actually is (and why net MRR lies)

A waterfall takes your starting MRR, adds and subtracts every source of movement across the period, and lands on ending MRR. Nothing is netted before you see it. New money goes up, expansion goes up, contraction comes down, churn comes down. Do the arithmetic right and the four buckets plus your starting balance equal your ending balance exactly.

Stripe computes this natively. Under Billing → Analytics, the MRR Growth report already splits net change into the same categories. Per Uscreen's write-up of Stripe Billing analytics (December 2023), the report is "the sum of revenue from new paid subscriptions, expansion from upgrades, contraction from downgrades, and churn." So the concept isn't exotic. The argument is always about how the buckets get filled.

Net MRR lies because it collapses signal. A $40 net gain can be $2,000 of new business against $1,960 of churn, or it can be an actually healthy month. Same headline, wildly different meaning. Pacer AI's guide to ARR waterfalls (May 2026) makes the rule blunt: record gross movements, not net deltas — a customer expanding $50K and contracting $30K in the same period should show both, not a $20K net. If you're only reporting the net, you've already lost the plot.

The four buckets, defined so they don't overlap

New is MRR from customers who weren't paying you at the start of the period. Expansion is additional MRR from existing paid customers: upgrades, added seats, plan bumps. Contraction is reduced MRR from existing paid customers who are still paying you something. Churn is MRR lost when a customer stops paying entirely.

The dangerous boundary is contraction versus churn, and Stripe draws it precisely. Per Stripe Support's MRR/ARR explainer (updated 2026), churn "occurs when an existing paid subscription is either cancelled, scheduled to be cancelled, downgraded to a free subscription, paused or becomes delinquent." Contraction, by contrast, is an existing paid subscription modified down to a non-zero MRR. Pacer AI says the same thing from the ARR side: a customer who cuts 90% but keeps a $10K floor is contraction, and a customer who drops to zero is churn.

Reactivation — a previously churned customer coming back — is the optional fifth bucket. Break it out if win-back is a real motion for you. If it's three accounts a year, fold it into new and move on.

Napkin math: a five-customer waterfall

Let me do this with round numbers so the mechanics are obvious. Five customers, one month.

Customer Start MRR End MRR Movement Bucket
A $100 $100 $0
B $200 $500 +$300 Expansion
C $500 $200 −$300 Contraction
D $100 $0 −$100 Churn
E $0 $100 +$100 New

Start MRR is $900. Now the buckets: new is +$100 (E), expansion is +$300 (B), contraction is −$300 (C), churn is −$100 (D). Add it all to the start: 900 + 100 + 300 − 300 − 100 = $900. Ending MRR is identical to starting MRR. Net change: zero.

Look at what a single net figure would have told you. Nothing. Flat month, no story. But B tripled while C got cut by 60%, one customer walked entirely, and one showed up. That's four events worth talking about, hidden behind a goose egg.

Now the failure that taught me this. My first waterfall — early, overconfident, built in a spreadsheet at 11pm — netted customer C's downgrade against customer B's upgrade inside the same "expansion" cell, because I keyed on customer, not direction. The board deck showed clean expansion and zero contraction. It also quietly swallowed a churn in the same tangle. I got asked one question I couldn't answer, and learned to classify by direction of movement, per customer, every time.

Why Stripe's dashboard number and your board deck disagree

Here's where reconciliation goes sideways. MRR is not cash. Per Stripe Support's own definition, "MRR cannot be calculated by simply adding up invoice totals is because MRR is a forward-looking metric that is distinct from the immediate cash flow you are about to receive." An annual plan billed at $1,200 shows up as $100 of MRR, not $1,200, because Stripe normalizes it to a monthly figure. Your finance team, staring at a $1,200 invoice, will want to know where the other $1,100 went. It didn't go anywhere. It's next month's MRR, and the month after, and so on.

The dashboard also bakes in choices you may not know you made. Per Stripe's docs on viewing subscription metrics (2026), you can configure whether to subtract recurring or one-time discounts, and "whether to consider canceled subscriptions as churn immediately or at the end of the billing period. Changes take effect 24-48 hours after you save the configuration." So two teams on the same Stripe account can report different churn simply because one counts cancellations at request and the other at period end. If nobody wrote down the setting, the disagreement looks like a bug when it's a policy.

The classification edge-case table

These are the events that break decks. Each one has a defined Stripe behavior, a correct bucket, and a gotcha that catches people who don't read the fine print.

Event What Stripe does Bucket The gotcha
Mid-cycle plan switch Credits the old plan, charges the new one for remaining days — e.g., $20 credit against a $40 charge for 20 days left (per Stripe's proration docs, 2026) Expansion or contraction, based on the new recurring price The proration lines are one-time, not recurring. Never let them into MRR.
Refund Returns cash for a past charge Usually neither A refund is a cash event. It doesn't change forward-looking MRR unless the subscription itself changed.
Paused subscription Keeps status=active while collection stops; invoices can be voided (per Stripe's pause-payment docs) Churn, if you follow Stripe's default classification The status still reads active. A naive export counts paused accounts as live MRR.
Downgrade to nonzero Modifies the subscription down but keeps a paid floor Contraction Don't net it against an upgrade elsewhere.
Drop to zero / cancel / to free Ends paid MRR entirely Churn This is the boundary — nonzero floor is contraction, zero is churn.
Annual-to-monthly move Renormalizes the monthly figure Depends on the resulting monthly amount The invoice cadence changes but MRR compares monthly-to-monthly; don't read the annual invoice as a spike.

The pause row is the sneaky one. Because a paused sub stays status=active, if you filter your export on status you'll carry dead weight as live revenue. Decide your rule and apply it the same way every month.

The month-by-month worked waterfall from raw events

Now the version that actually matters: driving the buckets from raw subscription events, with the ambiguous ones classified explicitly. Three months, one small book of business.

Month Raw event Classification rule applied Movement Bucket
Jan Cust F starts at $200 New paid subscription +$200 New
Jan Cust G plan switch $300→$500 mid-cycle Proration lines dropped; recurring delta only +$200 Expansion
Feb Cust H downgrade $400→$150 Nonzero floor → contraction, not netted −$250 Contraction
Feb Cust F proration credit −$18 One-time line, excluded from MRR $0
Feb Cust I paused (status=active) Pause = churn per Stripe default −$120 Churn
Mar Cust I resumes payment Previously churned returning +$120 Reactivation
Mar Cust J cancels $250 Drop to zero → churn −$250 Churn

Two events needed a deliberate hand. G's plan switch generated a proration credit and charge, so I recorded only the recurring price change of +$200 and threw the one-time lines away. I's pause left the status reading active, so status-filtering would have kept the $120 on the books. I classified it as churn to match Stripe's default and reversed it in March as reactivation when collection resumed. Everything else is direction-of-movement arithmetic.

That's the spine. Raw events in, one explicit rule per ambiguous case, four (or five) clean buckets out. Build the classification logic once, version-control it, and you stop re-litigating the same edge case every quarter.

Build it from history, not the current subscription object

The temptation is to pull each subscription's current state from the API and reconstruct the past from it. Don't. As Steven Wang lays out in his write-up of calculating MRR from raw Stripe data (May 2025), "the Stripe API only returns the most recent state of each subscription; we don't know if a plan was stopped and restarted, if its price changed" — the history is gone from that object. A sub that was $500, dropped to $200, then climbed to $400 just reads as $400 today.

So derive the series from invoices and subscription events, which carry the timeline. Yes, it's more work than one API call. It's also the difference between a waterfall that reconstructs any prior month and one that only knows the present. If you're building this alongside your broader reporting, the unit economics dashboard build covers the same event-sourced philosophy for CAC and payback.

Reconciliation checklist: tying the waterfall to invoiced revenue

When your ending MRR doesn't match Stripe's dashboard, work this order before you assume anything's broken.

  1. Strip prorations and one-time lines. Per Stripe's proration docs, mid-cycle switches create non-recurring lines. They never belong in MRR.
  2. Normalize annual to monthly. A $1,200 annual plan is $100 of MRR, per Stripe Support's definition — not a $1,200 month.
  3. Handle refunds as cash, not MRR. A refund alone doesn't move forward-looking MRR unless the subscription changed.
  4. Decide paused treatment. Paused subs stay status=active per Stripe's docs; pick churn-or-not and apply it consistently.
  5. Confirm cancel-timing config. Check whether your account counts cancellations as churn immediately or at period end — Stripe's metrics docs note the 24–48h lag on config changes.
  6. Reconcile ending MRR to the dashboard within tolerance. Within a handful of dollars from rounding and timing, you're done. Off by a customer, and one of the five rules above is being applied differently than Stripe applies it.

For the vocabulary behind these buckets, the growth metrics glossary has the short definitions to hand a new analyst.

Catching contraction before it becomes churn

Contraction is a leading indicator. The customer who trims seats this quarter is often the one who cancels next. But billing events only tell you that the movement happened — the downgrade, the reduced plan — never why. The why lives in the product.

Joining billing movements with product-usage data is how you get ahead of it. Declining active users, funnel drop-off, a feature that stopped getting touched, all showing up weeks before the plan gets cut. Product analytics platforms like Kixo surface those usage signals so a contraction event comes with an explanation instead of a shrug. Once you pair the movement with the behavior, contraction becomes a save-motion trigger rather than a line in next month's churn report.

The one-line takeaway

Record gross movements, write down an explicit rule for every edge case, derive the series from history instead of the current object, and reconcile ending MRR to invoiced revenue before anyone sees the deck. Do that and the waterfall holds up. After a few weeks of tying MRR to Stripe, I promise you the attribution wars start to feel like a picnic.

FAQ

Why doesn't my MRR match my Stripe invoices? Because MRR is forward-looking and normalized, not cash. Per Stripe Support, you can't sum invoice totals to get MRR — annual plans divide to monthly, and mid-cycle prorations are one-time lines that don't belong in the number.

Is a paused subscription churn or not? By Stripe's default classification it counts as churn, even though the subscription stays status=active. Because the status still reads active, a status-filtered export will miscount paused accounts as live MRR unless you handle them on purpose.

What's the difference between contraction and churn? Contraction is an existing paid customer dropping to a lower, still-nonzero MRR. Churn is dropping to zero — cancel, downgrade to free, pause, or delinquent, per Stripe Support. Never net a contraction against an expansion.

Should I build MRR from the Stripe API's subscription object? No. As Steven Wang notes, the API returns only each subscription's most recent state, so plan changes and restarts are lost. Build the historical series from invoices and subscription events instead.