6 min read

How to Track Churn Rate in Stripe

Stripe doesn't calculate churn rate for you—you need to pull subscription data and do the math. If you're bleeding customers and don't know why, you can't fix it. Here's how to set up churn tracking in Stripe and get actual numbers.

Set Up Webhooks for Subscription Events

The cleanest way to track churn is to listen for when customers cancel. Stripe sends webhook events for every subscription state change.

Create a webhook endpoint in Stripe

Go to Developers > Webhooks in your Stripe Dashboard. Click Add an endpoint and point it to a publicly accessible URL on your server (e.g., https://your-api.com/webhooks/stripe). Select the events you want to listen for: customer.subscription.deleted, customer.subscription.updated, and charge.failed.

javascript
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const app = express();

app.post('/webhooks/stripe', 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) {
    console.error(`Webhook signature verification failed: ${err.message}`);
    return res.sendStatus(400);
  }
  
  res.sendStatus(200);
});

app.listen(3000, () => console.log('Webhook server running'));
Basic webhook listener that verifies Stripe's signature

Handle the subscription.deleted event

When a subscription is canceled, Stripe sends a customer.subscription.deleted event. Capture this in your webhook handler and log it to your database or analytics tool. Include the subscription.id, customer.id, and the canceled_at timestamp.

javascript
if (event.type === 'customer.subscription.deleted') {
  const subscription = event.data.object;
  
  console.log(`Churn detected:`);
  console.log(`  Customer: ${subscription.customer}`);
  console.log(`  Subscription: ${subscription.id}`);
  console.log(`  Canceled at: ${new Date(subscription.canceled_at * 1000)}`);
  console.log(`  Reason: ${subscription.cancellation_details?.reason}`);
  
  // Write to your database for analysis
  await db.insert('churn_events', {
    customer_id: subscription.customer,
    subscription_id: subscription.id,
    churned_at: new Date(subscription.canceled_at * 1000),
    reason: subscription.cancellation_details?.reason
  });
}
Extract churn data from the webhook event
Watch out: Test your webhook locally with stripe listen --forward-to localhost:3000/webhooks/stripe before deploying.

Query Subscription Data from Stripe

To calculate churn retrospectively, you need to pull subscription snapshots from Stripe. Use the Subscriptions API with time-based filters.

List all subscriptions in a date range

Use stripe.subscriptions.list() to fetch subscriptions created in a specific period. Stripe returns paginated results (100 max per request), so loop through all pages. Include the status parameter set to all to see active, canceled, and past-due subscriptions.

javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const startOfMonth = Math.floor(new Date(2024, 0, 1).getTime() / 1000);
const endOfMonth = Math.floor(new Date(2024, 0, 31).getTime() / 1000);

let allSubscriptions = [];
let hasMore = true;
let startingAfter = null;

while (hasMore) {
  const subscriptions = await stripe.subscriptions.list({
    limit: 100,
    status: 'all',
    created: { gte: startOfMonth, lte: endOfMonth },
    starting_after: startingAfter
  });
  
  allSubscriptions = allSubscriptions.concat(subscriptions.data);
  hasMore = subscriptions.has_more;
  startingAfter = subscriptions.data[subscriptions.data.length - 1]?.id;
}

console.log(`Found ${allSubscriptions.length} subscriptions`);
Paginate through all subscriptions in a date range

Calculate churn rate from subscription statuses

Count subscriptions with status: 'active' and status: 'canceled'. Churn rate is canceled subscriptions divided by total subscriptions at the start of your period, multiplied by 100. Subscriptions with status: 'past_due' are at risk but not yet churned.

javascript
const activeCount = allSubscriptions.filter(sub => sub.status === 'active').length;
const canceledCount = allSubscriptions.filter(sub => sub.status === 'canceled').length;

const totalAtStart = activeCount + canceledCount;
const churnRate = (canceledCount / totalAtStart) * 100;

console.log(`Active subscriptions: ${activeCount}`);
console.log(`Canceled subscriptions: ${canceledCount}`);
console.log(`Churn rate: ${churnRate.toFixed(2)}%`);

// Example output:
// Active subscriptions: 950
// Canceled subscriptions: 50
// Churn rate: 5.00%
Compute monthly churn rate from subscription counts
Tip: The Stripe API has rate limits (100 requests/second). Cache results for 1 hour if calculating churn frequently.

Track Cancellation Reasons

Knowing why customers cancel is as important as knowing how many. Stripe's Billing Portal prompts customers to pick a reason when they opt out.

Enable cancellation feedback in Billing Portal

In Billing Portal Settings, enable Cancellation options. This prompts customers to pick a reason when they cancel (e.g., 'Too expensive', 'Not using it', 'Switching services'). Stripe stores this in subscription.cancellation_details.reason.

javascript
const reason = subscription.cancellation_details?.reason;
const feedback = subscription.cancellation_details?.feedback;

switch (reason) {
  case 'cancellation_requested':
    console.log(`Customer canceled: "${feedback}"`);
    break;
  case 'billing_cycle_anchor_change':
    console.log('Subscription ended due to billing cycle change');
    break;
  case 'dunning_failure':
    console.log('Payment failed and dunning exhausted all retries');
    break;
  case 'upgrade':
    console.log('Customer upgraded to different plan');
    break;
}

// Group churn by reason
await db.insert('churn_reasons', {
  customer_id: subscription.customer,
  reason: reason,
  feedback: feedback,
  churned_at: new Date(subscription.canceled_at * 1000)
});
Log cancellation reasons for root cause analysis
Tip: If 40% of churn says 'Too expensive', you have a pricing problem. If 40% says 'Not using', you have a product problem.

Common Pitfalls

  • Confusing 'canceled' with 'past_due'. A past-due subscription might still pay. Define churn explicitly: only canceled subscriptions count.
  • Mixing subscription cohorts. Count subscriptions that *started* in the period and stayed or churned. Don't include ones created and canceled in the same month without context.
  • Forgetting to paginate. Stripe's list endpoint maxes at 100 items. If you have 500 subscriptions and only fetch once, your churn rate will be 5x too high.
  • Ignoring involuntary churn. If dunning fails and Stripe auto-cancels, that's churn too—but different from a customer choosing to leave. Track both.

Wrapping Up

Now you have real churn data in Stripe. Use webhooks to catch cancellations in real-time and the Subscriptions API to calculate historical rates by month. Pay attention to cancellation reasons—they're your north star for retention fixes. If you want to track this automatically across tools and correlate it with your product metrics, Product Analyst can help.

Track these metrics automatically

Product Analyst connects to your stack and surfaces the insights that matter.

Try Product Analyst — Free