How to calculate churn from your Stripe data
23 September 2026 · by Nordva, who make Nordva Launch
How do I calculate churn from my Stripe data?
Count the customers who were paying at the start of the month, then how many of them had no paying subscription at the end. The second divided by the first is customer churn. Revenue churn is the MRR those customers took with them, divided by MRR at the start. A restricted, read-only Stripe key and one short Node script give you both.
Churn is two numbers, and people mix them up.
- Customer churn: of the customers paying at the start of a period, the share who stopped paying by its end.
- Revenue churn: of the monthly recurring revenue (MRR) at the start, the share that left with those customers.
They move apart when your customers pay different amounts. Lose one customer on your cheapest plan and customer churn looks bad while revenue churn barely moves. Lose your one large account and it is the other way round. Look at both.
The glossary entry on churn has a worked example with made-up numbers. This guide is about getting the real numbers out of Stripe.
What counts as churned in Stripe
Everything you need is on the Subscription object: its status, when it started, when it ended, and whether it is set to cancel. The statuses need a decision each.
canceled: gone.ended_atis when it ended. Do not usecanceled_atfor this: for a subscription canceled at period end, the API reference sayscanceled_atis the time of the cancel request, not the end of the period.cancel_at_period_end: trueon an active subscription: the customer asked to leave, but has paid until the period ends. It is not churn yet. Count these separately, because next month’s churn is already sitting in that list.past_due: a payment failed and Stripe may retry. Stripe still countsactiveandpast_duesubscriptions in MRR, so treat it as still paying.unpaid: retries ran out and your subscription settings chose to keep the subscription but stop charging. Stripe’s own analytics count a subscriber as churned when all their subscriptions are canceled orunpaid.incomplete_expired: the first payment never succeeded, so this person was never a paying customer.trialingandpaused: not paying yet. A subscription only becomespausedwhen a trial ends without a payment method, per the status table.
Churn is about customers, not subscriptions. A customer with two subscriptions who cancels one has not churned; the revenue they dropped is contraction, which is a different number.
How Stripe defines it
If you use Stripe Billing, the Billing overview in the Dashboard already shows churn, and the analytics docs give the definition: subscribers who churned in the last 30 days, divided by active subscribers 30 days ago plus new subscribers in those 30 days. Their example is 100 churned out of 1,000 at the start plus 100 new, which Stripe works out as 9.1%.
That is a fair definition. It is also not the one most people mean when they say “monthly churn”, which divides by the customers you started with. With the same data, Stripe’s number comes out lower whenever you are adding customers. Neither is wrong. Pick one, write it down, and do not compare numbers made with different definitions.
The rest of this guide computes the start-of-month version for any calendar month you choose, so you can see it per month and change the rules to match your business.
Step 1: make a read-only key
Use a restricted API key, not your secret key. It starts with rk_live_ or rk_test_ and can only do what you allow. In the Dashboard, create one with Read on subscriptions and nothing else. The script only sends GET requests, and Stripe’s docs map GET to read permission. If the key lacks a permission, the error message says which one to add.
Step 2: run the script
It lists every subscription created before the end of the month (status: "all", because the list endpoint leaves out canceled subscriptions by default), works out which customers were paying at the start and at the end, and prints both kinds of churn. The Stripe Node library pages through the results for you when you loop with for await.
npm install stripe
STRIPE_KEY=rk_live_... node churn.mjs 2026-08 usd
// churn.mjs: customer churn and revenue churn for one calendar month (UTC).
// Usage: STRIPE_KEY=rk_live_... node churn.mjs 2026-08 usd
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_KEY);
const [month = new Date().toISOString().slice(0, 7), currency = "usd"] = process.argv.slice(2);
const now = Math.floor(Date.now() / 1000);
const startDate = new Date(`${month}-01T00:00:00Z`);
const endDate = new Date(startDate);
endDate.setUTCMonth(endDate.getUTCMonth() + 1);
const start = startDate.getTime() / 1000;
const end = Math.min(endDate.getTime() / 1000, now); // the current month ends today
// What one subscription is worth per month, in the currency's smallest unit.
const PER_MONTH = { day: 365 / 12, week: 52 / 12, month: 1, year: 1 / 12 };
function monthlyValue(sub) {
let total = 0;
for (const item of sub.items.data) {
const r = item.price.recurring;
if (!r || r.usage_type === "metered" || item.price.unit_amount == null) continue; // usage and tiered prices are left out
total += (item.price.unit_amount * (item.quantity ?? 1) * PER_MONTH[r.interval]) / r.interval_count;
}
return total;
}
// Was this subscription paying at time t (Unix seconds)?
function payingAt(sub, t) {
if (["incomplete", "incomplete_expired", "paused"].includes(sub.status)) return false; // never paid
const from = Math.max(sub.start_date, sub.trial_end ?? 0); // a trial is not paying
let until = sub.ended_at ?? Infinity; // cancel_at_period_end: still paying until it ends
if (sub.status === "unpaid") until = Math.min(until, now); // the API does not say since when
return from <= t && t < until;
}
const mrrAtStart = new Map(); // customer id -> MRR at the start of the month
const payingAtEnd = new Set();
const cancelling = new Set();
for await (const sub of stripe.subscriptions.list({ status: "all", created: { lt: end }, limit: 100 })) {
if (sub.currency !== currency) continue; // never add one currency to another
const value = monthlyValue(sub);
if (value <= 0) continue; // free plans are not paying customers
const customer = typeof sub.customer === "string" ? sub.customer : sub.customer.id;
if (payingAt(sub, start)) mrrAtStart.set(customer, (mrrAtStart.get(customer) ?? 0) + value);
if (payingAt(sub, end)) payingAtEnd.add(customer);
if ((sub.cancel_at_period_end || sub.cancel_at) && payingAt(sub, now)) cancelling.add(customer); // as of today
}
const churned = [...mrrAtStart.keys()].filter((c) => !payingAtEnd.has(c));
const mrrStart = [...mrrAtStart.values()].reduce((a, b) => a + b, 0);
const mrrLost = churned.reduce((sum, c) => sum + mrrAtStart.get(c), 0);
const pct = (a, b) => (b ? `${((100 * a) / b).toFixed(1)}%` : "n/a");
const money = (minor) => (minor / 100).toFixed(2); // wrong for zero-decimal currencies such as JPY
console.log(`${month}, ${currency.toUpperCase()}, up to ${new Date(end * 1000).toISOString().slice(0, 10)}`);
console.log(`Paying customers at the start: ${mrrAtStart.size}`);
console.log(`Of those, not paying at the end: ${churned.length}`);
console.log(`Customer churn: ${pct(churned.length, mrrAtStart.size)}`);
console.log(`MRR at the start: ${money(mrrStart)}`);
console.log(`MRR of the customers who left: ${money(mrrLost)}`);
console.log(`Revenue churn (cancellations only): ${pct(mrrLost, mrrStart)}`);
console.log(`Paying today, set to cancel: ${cancelling.size}`);
Run it for the last six months, one after another, before you draw any conclusion from a single month.
What the script decides for you
Every churn number rests on choices. These are the script’s, so you can change the ones that do not fit.
- Calendar months in UTC. For the current month, the end is today.
- A trial is not paying. Paying starts at
trial_endwhen there is one. This matches Stripe, which leaves trials out of active subscribers. - Cancel at period end counts on the day it ends, using
ended_at. Customers who asked to leave but still have paid time show up in the last line instead. That line is always as of today, whichever month you ask for. past_dueis still paying, as in Stripe’s MRR.unpaidis churned from today. The subscription object does not record when it became unpaid, so for past months these customers still count as paying. If you need the exact date, your webhook handler can store it when thecustomer.subscription.updatedevent arrives.- One currency at a time. Stripe amounts are in the smallest unit, so the script divides by 100; for a zero-decimal currency such as JPY, remove that division.
- Free plans, metered usage and tiered prices are left out of MRR, as Stripe leaves out free plans and usage-based products.
- Discounts are ignored. Stripe lets you choose whether discounts reduce MRR; the script uses list prices.
- Current prices only. The script sees each subscription as it is now. A customer who downgraded in the middle of the month shows their new price at the start too. For MRR history by customer and month, the Billing overview offers downloadable reports, including MRR per subscriber per month.
- Revenue churn here is cancellations only. Stripe’s “churned revenue” also includes contraction (downgrades), which a current-state script cannot see.
Reading the number while you are small
With twenty customers, one cancellation moves customer churn by five points. A single month tells you almost nothing; three to six months start to show a direction.
The count matters less than the names. Take the churned list from the script and look at each customer: when did they start, which plan were they on, what did they ask you for before they left? The answer on why customers cancel walks through the usual reasons and how to tell which one is yours. If the people who leave early mostly came from one channel, that channel is costing you more than its signup numbers suggest; the guide on which channel brings paying customers shows how to record that.
Who should not use Launch for this
If churn, cohorts and retention curves are the numbers you watch every week, a dedicated revenue analytics tool, or Stripe’s own Billing overview, goes further than Launch does. If you bill through anything other than Stripe, Launch cannot read it. And if the script above answers your question, keep it: it is free and yours.
The faster way: Nordva Launch
Launch reads your Stripe account so you do not have to keep a script running. You connect Stripe in the dashboard with a restricted key; Launch reads it with GET requests only, and disconnecting deletes every synced row. Every plan can connect Stripe. The Free plan reads 30 days of history and syncs every six hours; Indie reads 90 days and Builder 730 days, both syncing hourly.
What Launch adds is the part a churn script cannot: where each paying customer came from. A customer is traced to their first visit through nordva_visitor_id in the customer’s or subscription’s metadata, through identify() with their user id or email, or through their email on your waitlist, first touch only. So when someone leaves, you see where they came from next to what they paid. The Revenue API page lists what it reports. Start free: no time limit, no card.
Questions
- Does Stripe show churn already?
- Yes, if you use Stripe Billing. The Billing overview in the Dashboard shows subscriber churn rate and churned revenue, and you can configure whether discounts count against MRR. Its churn rate uses the last 30 days and divides by active subscribers 30 days ago plus new subscribers in those 30 days, so it will not match a calendar-month calculation exactly.
- Is a past_due subscription churned?
- Not yet. Stripe keeps counting past_due subscriptions in MRR. After the last payment retry, your subscription settings decide whether it becomes canceled, unpaid or stays past_due. Stripe counts canceled and unpaid as churned.
- When does a cancel at period end count as churn?
- When the period ends and the subscription is canceled, not when the customer clicks cancel. Until then the subscription is active and the customer has paid for the time. Report those separately as customers set to cancel.
- Why is my churn number different from Stripe's?
- Usually the window and the denominator. Stripe uses a rolling 30 days and adds new subscribers to the denominator; the script here uses a calendar month and only the customers you had at its start. Discounts, trials and free plans are other places two definitions part ways.
Try it in your project
npx nordva@latest init
Free plan, no card, no invite.