6 min read

How to Monitor Churn Rate in Stripe

Stripe doesn't surface churn rate directly, so you need to calculate it from subscription data. The key is knowing where to pull the numbers and understanding what counts as a churned customer. Let's walk through it.

Extract Subscription Data from Stripe

The Stripe API gives you all the raw subscription events. You pull them, filter by date and status, then count.

Fetch subscriptions in a date range

Use the stripe.subscriptions.list() method with a created filter to grab all subscriptions created or updated during your measurement window. Include the status: 'all' parameter so you get active, canceled, and everything else.

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

const subscriptions = await stripe.subscriptions.list({
  limit: 100,
  status: 'all',
  created: {
    gte: Math.floor(new Date('2026-03-01').getTime() / 1000),
    lt: Math.floor(new Date('2026-04-01').getTime() / 1000)
  }
});
Fetch all subscriptions created in March. Stripe returns paginated results; use the `starting_after` parameter to fetch more.

Handle pagination for large customer bases

The API returns 100 results by default. If you have more subscriptions, loop through pages using the starting_after cursor.

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

while (hasMore) {
  const batch = await stripe.subscriptions.list({
    limit: 100,
    status: 'all',
    starting_after: startingAfter,
    created: { gte: startTimestamp, lt: endTimestamp }
  });
  
  allSubscriptions = allSubscriptions.concat(batch.data);
  hasMore = batch.has_more;
  startingAfter = batch.data[batch.data.length - 1]?.id;
}
Paginate through results to get your full subscription list.
Watch out: Stripe's created timestamp is when the subscription was created, not when it was canceled. For canceled subscriptions, use canceled_at or the customer.subscription.deleted webhook event instead.

Calculate Churn Rate from the Data

Now that you have the subscription list, separate canceled from active and compute the rate.

Count active and canceled subscriptions

Filter subscriptions by status. active means the customer has a live subscription; canceled means they've churned. Divide canceled by your baseline to get the churn rate.

javascript
const active = allSubscriptions.filter(sub => sub.status === 'active').length;
const canceled = allSubscriptions.filter(sub => sub.status === 'canceled').length;
const churnRate = (canceled / (active + canceled)) * 100;

console.log(`Active: ${active}, Canceled: ${canceled}, Churn Rate: ${churnRate.toFixed(2)}%`);
Basic churn calculation. The denominator is total subscriptions in the period.

Segment by subscription plan

Churn often varies by plan tier. Filter by items.data[0].price.id to see which plans have the highest churn and identify your most at-risk segments.

javascript
const churnByPlan = {};

allSubscriptions.forEach(sub => {
  const planId = sub.items.data[0]?.price.id;
  if (!churnByPlan[planId]) churnByPlan[planId] = { active: 0, canceled: 0 };
  
  if (sub.status === 'active') churnByPlan[planId].active++;
  if (sub.status === 'canceled') churnByPlan[planId].canceled++;
});

Object.entries(churnByPlan).forEach(([plan, counts]) => {
  const rate = (counts.canceled / (counts.active + counts.canceled)) * 100;
  console.log(`Plan ${plan}: ${rate.toFixed(2)}% churn`);
});
Break down churn by plan to spot which tiers are bleeding customers.
Tip: Use sub.canceled_at to filter for cancellations in a specific window. This is more accurate than relying on the subscription's creation date.

Automate Churn Monitoring with Webhooks

Running manual queries every week gets stale. Listen to Stripe's webhook events and log churn as it happens.

Listen for the customer.subscription.deleted webhook

Set up a webhook endpoint in your Stripe Dashboard > Developers > Webhooks. Add an event listener for customer.subscription.deleted to capture cancellations in real time.

javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const bodyParser = require('body-parser');
const express = require('express');

const app = express();
app.post('/webhook', bodyParser.raw({type: 'application/json'}), (req, res) => {
  const sig = req.headers['stripe-signature'];
  
  try {
    const event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
    
    if (event.type === 'customer.subscription.deleted') {
      const subscription = event.data.object;
      console.log(`Subscription ${subscription.id} canceled`);
      // Log to your database or analytics platform
    }
    res.json({received: true});
  } catch (err) {
    res.status(400).send(`Webhook Error: ${err.message}`);
  }
});
Verify the webhook signature and handle cancellation events immediately.

Store churn events and aggregate daily

For each webhook, record the customer ID, subscription ID, and cancellation timestamp. At the end of each day, aggregate to calculate rolling churn rates.

javascript
if (event.type === 'customer.subscription.deleted') {
  const { customer, id, canceled_at } = event.data.object;
  
  // Store in your database
  await db.query(
    'INSERT INTO churn_log (customer_id, subscription_id, canceled_at) VALUES ($1, $2, $3)',
    [customer, id, new Date(canceled_at * 1000)]
  );
}

// Daily aggregation
const today = new Date();
const startOfDay = new Date(today.setHours(0, 0, 0, 0));

const churnedToday = await db.query(
  'SELECT COUNT(*) as count FROM churn_log WHERE canceled_at >= $1',
  [startOfDay]
);
console.log(`Churn events today: ${churnedToday.rows[0].count}`);
Log cancellations and aggregate daily metrics.
Tip: Stripe retries failed webhooks, so always use idempotency keys and check for duplicates before inserting to avoid double-counting cancellations.

Common Pitfalls

  • Confusing subscription status with cancellation date. A subscription can be canceled but still have an active current_period_end—the customer hasn't lost access yet.
  • Forgetting to account for trial subscriptions. You may want to exclude trials from your churn calculation if they never paid.
  • Using created timestamp for cancellations instead of canceled_at. This shifts your numbers and makes trends hard to track.
  • Not handling webhook retries. Stripe retries failed webhooks, so you need idempotency to avoid double-counting cancellations.

Wrapping Up

Churn rate in Stripe boils down to pulling subscription data via the API, filtering for canceled subscriptions, and dividing by your baseline. Webhooks let you automate this and catch churn as it happens. If you want to track this automatically across tools, Product Analyst can help.

Track these metrics automatically

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

Try Product Analyst — Free