6 min read

How to Visualize Churn Rate in Stripe

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.

javascript
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'));
Verify the signature using your webhook secret from **Developers > API keys**.

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.

javascript
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'
  });
}
MRR is stored in cents in Stripe. Divide by 100 for dollar amounts.
Watch out: 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.

javascript
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.

javascript
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 };
}
Tip: Stripe's 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.

javascript
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)}`);
});
Schedule this in AWS Lambda, Vercel Cron, or a background job runner.
Watch out: Stripe webhook events can arrive out of order or delayed. Always query by timestamp when calculating rates, not by webhook arrival order.

Common Pitfalls

  • Treating all customer.subscription.updated events 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: true to 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.

Track these metrics automatically

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

Try Product Analyst — Free