Stripe doesn't calculate churn rate for you—you have to extract it from subscription cancellations and MRR decline. If you're selling a subscription product, understanding which customers are canceling and when is critical for retention strategy.
Capture Churn Events via Webhooks
The fastest way to track churn is to listen for Stripe's native cancellation events.
Set up a webhook listener for subscription cancellations
In your Stripe dashboard under Developers > Webhooks, create an endpoint that receives customer.subscription.deleted and customer.subscription.updated events. This captures both explicit cancellations and subscription lapses.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const express = require('express');
const app = express();
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(req.body, sig, process.env.STRIPE_WEBHOOK_SECRET);
} catch (err) {
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === 'customer.subscription.deleted') {
const subscription = event.data.object;
console.log(`Churn: ${subscription.customer} on ${new Date(subscription.ended_at * 1000)}`);
}
res.json({received: true});
});
app.listen(3000, () => console.log('Webhook running'));
Log churn events to your database
Store each cancellation event in a table with the customer ID, cancellation date, and MRR at time of churn. This becomes your churn log.
if (event.type === 'customer.subscription.deleted') {
const sub = event.data.object;
await db.insert('churn_events').values({
customer_id: sub.customer,
subscription_id: sub.id,
churn_date: new Date(sub.ended_at * 1000),
mrr_lost: sub.items.data.reduce((sum, item) => {
if (item.price.recurring?.interval === 'month') {
return sum + (item.price.unit_amount / 100);
}
return sum;
}, 0),
reason: event.data.previous_attributes.status || 'unknown'
});
}customer.subscription.updated fires on many changes. Check the status field change to distinguish actual cancellations from plan updates.Query Historical Subscriptions and Calculate Churn Rate
For a dashboard view, pull subscription history from Stripe's API and calculate churn retroactively.
List all subscriptions and filter by status
Use stripe.subscriptions.list() with pagination to fetch all active and canceled subscriptions. Include the customer object to correlate with your own customer records.
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
async function getChurnMetrics(startDate) {
let hasMore = true;
let startingAfter = undefined;
let canceledCount = 0;
let mrrLost = 0;
while (hasMore) {
const subscriptions = await stripe.subscriptions.list({
status: 'all',
limit: 100,
starting_after: startingAfter,
created: {
gte: Math.floor(startDate.getTime() / 1000)
}
});
subscriptions.data.forEach(sub => {
if (sub.status === 'canceled' && sub.ended_at > startDate.getTime() / 1000) {
canceledCount++;
const monthlyAmount = sub.items.data
.filter(item => item.price.recurring?.interval === 'month')
.reduce((sum, item) => sum + item.price.unit_amount, 0) / 100;
mrrLost += monthlyAmount;
}
});
hasMore = subscriptions.has_more;
if (hasMore) {
startingAfter = subscriptions.data[subscriptions.data.length - 1].id;
}
}
return { canceledCount, mrrLost };
}
Calculate churn rate and visualize
Divide churned subscriptions by your starting count for the period. Plot over time—weekly, monthly, or cohort-based—to spot retention trends.
async function monthlyChurnRate(year, month) {
const startDate = new Date(year, month - 1, 1);
const endDate = new Date(year, month, 0);
const startOfMonth = Math.floor(startDate.getTime() / 1000);
const endOfMonth = Math.floor(endDate.getTime() / 1000);
// Subscriptions active at start of month
const activeAtStart = await stripe.subscriptions.list({
status: 'active',
created: { lt: startOfMonth },
limit: 1
});
// Subscriptions canceled during this month
const churnedThisMonth = await stripe.subscriptions.list({
status: 'canceled',
created: { gte: startOfMonth, lt: endOfMonth }
});
const startCount = activeAtStart.total_count || 0;
const churnRate = startCount > 0 ? (churnedThisMonth.data.length / startCount) * 100 : 0;
return { month, year, churnRate: churnRate.toFixed(2), churned: churnedThisMonth.data.length, activeStart: startCount };
}
subscriptions.list() paginates at 100 items per call. For large customer bases, use the created filter and cursor pagination to avoid timeouts.Sync Churn Data to Your BI Tool
Once you're tracking churn, surface it where your team can act on it.
Set up a daily sync to your analytics warehouse
Use a scheduled job (cron or serverless function) to sync your churn_events table to a BI platform like Metabase, Superset, or Segment for cohort analysis.
const cron = require('node-cron');
// Run daily at 2 AM
cron.schedule('0 2 * * *', async () => {
const now = new Date();
const thisMonth = await monthlyChurnRate(now.getFullYear(), now.getMonth() + 1);
// Push to Segment
const analytics = require('analytics-node')(process.env.SEGMENT_WRITE_KEY);
analytics.track({
userId: 'system',
event: 'monthly_churn_metric',
properties: thisMonth
});
console.log(`Churn synced: ${JSON.stringify(thisMonth)}`);
});
Common Pitfalls
- Treating all
customer.subscription.updatedevents as churn. Only canceled subscriptions count as churn. - Forgetting that Stripe amounts are in cents. Always divide by 100 to get dollars.
- Miscalculating the denominator. Churn rate = canceled this month ÷ active at month start, not total ever created.
- Including test mode subscriptions. Always filter by
livemode: trueto exclude sandbox data.
Wrapping Up
You now have churn visibility in Stripe—either via webhooks for real-time tracking or via the subscriptions API for historical analysis. Build your dashboard, set alerts for spikes, and use cohort analysis to spot which customer segments are most at-risk. If you want to track this automatically across tools and correlate churn with product behavior, Product Analyst can help.