MRR should be simple. Count your active subscriptions, sum the monthly amounts. Done. Except it's never that simple in practice, because Stripe's subscription model has a dozen features that make a naive calculation wrong: trials (count them or not?), annual plans (divide by 12?), prorations (include them?), coupons (before or after discount?), metered billing (last invoice or average?), multi-currency (which exchange rate?), paused subscriptions (MRR or not?).
I've seen teams at the same company report three different MRR numbers depending on which spreadsheet they pull from. The finance team says $87k. The Stripe dashboard says $91k. The CEO's board deck says $95k. Nobody is lying — they just made different definitional choices about which edge cases to include.
Here's production-ready Python code that calculates MRR from Stripe's API with explicit choices for every edge case, so you always know exactly what number you're reporting and why.
The basic calculation (and why it's wrong)
The naive approach:
import stripe
stripe.api_key = "sk_live_..."
mrr = 0
for sub in stripe.Subscription.list(status="active").auto_paging_iter():
for item in sub["items"]["data"]:
price = item["price"]
if price["recurring"]["interval"] == "month":
mrr += price["unit_amount"] * item["quantity"]
elif price["recurring"]["interval"] == "year":
mrr += (price["unit_amount"] * item["quantity"]) / 12
# Ignore one-time items
print(f"MRR: ${mrr / 100:.2f}")This gets you ~70% of the way. Here's what it misses:
- Coupons/discounts — a customer on a $100/mo plan with a 50% coupon is $50 MRR, not $100
- Trials — a trialing subscription hasn't generated revenue yet
- Metered billing — the price has no fixed
unit_amount; MRR depends on usage - Multi-currency — a €100/mo subscription isn't the same as a $100/mo subscription
- Paused subscriptions — still "active" in Stripe's status but generating $0
- Past-due subscriptions — should you count them? They might recover.
- Scheduled changes — subscription updating next period (plan change queued)
The complete calculation
# calculate_mrr.py
import os
import stripe
from datetime import datetime
from dataclasses import dataclass, field
from dotenv import load_dotenv
load_dotenv()
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]
@dataclass
class MRRConfig:
"""Configuration for MRR calculation edge cases."""
include_trials: bool = False
include_past_due: bool = True
include_paused: bool = False
coupon_handling: str = "net" # "net" (after discount) or "gross" (before)
metered_handling: str = "last_invoice" # "last_invoice", "average_3mo", "zero"
annual_normalization: bool = True # Divide annual by 12
multi_currency: str = "convert" # "convert" (to USD) or "native" (ignore)
base_currency: str = "usd"
@dataclass
class MRRBreakdown:
"""MRR broken down by component."""
total: float = 0.0
recurring_monthly: float = 0.0
recurring_annual_normalized: float = 0.0
metered: float = 0.0
discount_impact: float = 0.0
by_plan: dict = field(default_factory=dict)
by_status: dict = field(default_factory=dict)
customer_count: int = 0
subscription_count: int = 0
excluded_trials: int = 0
excluded_paused: int = 0
# Approximate exchange rates (use a real FX API in production)
EXCHANGE_RATES_TO_USD = {
"usd": 1.0,
"eur": 1.08,
"gbp": 1.26,
"cad": 0.74,
"aud": 0.65,
"jpy": 0.0067,
}
def get_subscription_mrr(
subscription: dict,
config: MRRConfig,
) -> tuple[float, str]:
"""
Calculate MRR contribution for a single subscription.
Returns (mrr_in_cents, plan_name).
"""
status = subscription["status"]
# Handle status-based exclusions
if status == "trialing" and not config.include_trials:
return 0.0, "trial_excluded"
if status == "paused" and not config.include_paused:
return 0.0, "paused_excluded"
if status == "past_due" and not config.include_past_due:
return 0.0, "past_due_excluded"
if status not in ("active", "trialing", "past_due", "paused"):
return 0.0, "inactive"
mrr = 0.0
plan_name = "unknown"
for item in subscription["items"]["data"]:
price = item["price"]
plan_name = price.get("nickname") or price.get("product", "unknown")
currency = price["currency"]
# Currency conversion
fx_rate = 1.0
if config.multi_currency == "convert" and currency != config.base_currency:
fx_rate = EXCHANGE_RATES_TO_USD.get(currency, 1.0)
if price["type"] == "recurring":
interval = price["recurring"]["interval"]
interval_count = price["recurring"]["interval_count"]
if price.get("billing_scheme") == "tiered":
# Tiered pricing — use the last invoice amount
mrr += get_metered_mrr(subscription, config) * fx_rate
elif price.get("unit_amount") is not None:
# Fixed recurring
item_amount = price["unit_amount"] * item["quantity"]
# Normalize to monthly
if interval == "month":
item_amount = item_amount / interval_count
elif interval == "year" and config.annual_normalization:
item_amount = item_amount / (12 * interval_count)
elif interval == "week":
item_amount = item_amount * (52 / 12) / interval_count
elif interval == "day":
item_amount = item_amount * (365 / 12) / interval_count
mrr += item_amount * fx_rate
else:
# Metered/usage-based (no unit_amount)
mrr += get_metered_mrr(subscription, config) * fx_rate
# Apply discount
if config.coupon_handling == "net" and subscription.get("discount"):
discount = subscription["discount"]
coupon = discount.get("coupon", {})
if coupon.get("percent_off"):
mrr = mrr * (1 - coupon["percent_off"] / 100)
elif coupon.get("amount_off"):
mrr = max(0, mrr - coupon["amount_off"])
return mrr, plan_name
def get_metered_mrr(subscription: dict, config: MRRConfig) -> float:
"""
Calculate MRR for metered/usage-based subscriptions.
Uses the configured strategy (last invoice, average, or zero).
"""
if config.metered_handling == "zero":
return 0.0
# Get recent invoices for this subscription
try:
invoices = stripe.Invoice.list(
subscription=subscription["id"],
status="paid",
limit=3,
)
if not invoices.data:
return 0.0
if config.metered_handling == "last_invoice":
return invoices.data[0]["amount_paid"]
elif config.metered_handling == "average_3mo":
amounts = [inv["amount_paid"] for inv in invoices.data]
return sum(amounts) / len(amounts)
except Exception:
return 0.0
return 0.0
def calculate_mrr(config: MRRConfig | None = None) -> MRRBreakdown:
"""
Calculate total MRR from all Stripe subscriptions.
Returns a detailed breakdown.
"""
if config is None:
config = MRRConfig()
breakdown = MRRBreakdown()
seen_customers = set()
statuses = ["active"]
if config.include_trials:
statuses.append("trialing")
if config.include_past_due:
statuses.append("past_due")
if config.include_paused:
statuses.append("paused")
for status in statuses:
for sub in stripe.Subscription.list(
status=status, limit=100
).auto_paging_iter():
mrr, plan_name = get_subscription_mrr(sub, config)
if mrr == 0 and plan_name in (
"trial_excluded", "paused_excluded", "past_due_excluded"
):
if plan_name == "trial_excluded":
breakdown.excluded_trials += 1
elif plan_name == "paused_excluded":
breakdown.excluded_paused += 1
continue
breakdown.total += mrr
breakdown.subscription_count += 1
seen_customers.add(sub["customer"])
# Categorize
interval = sub["items"]["data"][0]["price"]["recurring"]["interval"]
if interval == "month":
breakdown.recurring_monthly += mrr
else:
breakdown.recurring_annual_normalized += mrr
# By plan
breakdown.by_plan[plan_name] = (
breakdown.by_plan.get(plan_name, 0) + mrr
)
# By status
breakdown.by_status[sub["status"]] = (
breakdown.by_status.get(sub["status"], 0) + mrr
)
breakdown.customer_count = len(seen_customers)
return breakdown
def print_mrr_report(breakdown: MRRBreakdown):
"""Print a formatted MRR report."""
print("=" * 50)
print("MRR REPORT")
print("=" * 50)
print(f"Total MRR: ${breakdown.total / 100:>12,.2f}")
print(f" Monthly plans: ${breakdown.recurring_monthly / 100:>12,.2f}")
print(f" Annual (norm.): ${breakdown.recurring_annual_normalized / 100:>12,.2f}")
print(f" Metered: ${breakdown.metered / 100:>12,.2f}")
print("-" * 50)
print(f"Customers: {breakdown.customer_count:>12}")
print(f"Subscriptions: {breakdown.subscription_count:>12}")
print(f"Excluded (trial): {breakdown.excluded_trials:>12}")
print(f"Excluded (paused): {breakdown.excluded_paused:>12}")
print("-" * 50)
print("By plan:")
for plan, amount in sorted(
breakdown.by_plan.items(), key=lambda x: x[1], reverse=True
):
print(f" {plan:<20} ${amount / 100:>10,.2f}")
print("-" * 50)
print("By status:")
for status, amount in breakdown.by_status.items():
print(f" {status:<20} ${amount / 100:>10,.2f}")
if __name__ == "__main__":
config = MRRConfig(
include_trials=False,
include_past_due=True,
include_paused=False,
coupon_handling="net",
metered_handling="last_invoice",
annual_normalization=True,
)
breakdown = calculate_mrr(config)
print_mrr_report(breakdown)The definitional choices explained
Every MRR calculation requires decisions. Here's how different teams typically choose:
| Question | Conservative (Finance) | Aggressive (Sales) | Recommended |
|---|---|---|---|
| Include trials? | No | Yes | No — hasn't generated revenue |
| Include past-due? | No | Yes | Yes — most recover within 30d |
| Include paused? | No | Depends | No — $0 revenue right now |
| Coupons: net or gross? | Net | Gross | Net — reflects actual revenue |
| Annual plans? | Divide by 12 | Divide by 12 | Divide by 12 |
| Metered billing? | Last invoice | Average 3mo | Last invoice |
The most common mistake: reporting gross MRR (before coupons) in board meetings while using net MRR (after coupons) for financial planning. These can differ by 5-15% if you're generous with discounts.
MRR movements: new, expansion, contraction, churn
Total MRR is useful, but MRR movement tells you whether you're healthy:
# mrr_movements.py
def calculate_mrr_movements(current_month: str, previous_month: str):
"""
Compare two months of subscription data to compute movements.
Requires subscription data stored in your database.
"""
# This is pseudocode — actual implementation requires
# storing historical subscription state
movements = {
"new": 0, # Subscriptions created this month
"expansion": 0, # Existing subs that increased
"contraction": 0, # Existing subs that decreased
"churn": 0, # Subscriptions canceled this month
"reactivation": 0, # Previously canceled, now active again
}
for sub in current_subscriptions:
if sub.created_this_month:
movements["new"] += sub.mrr
elif sub.mrr > sub.previous_mrr:
movements["expansion"] += sub.mrr - sub.previous_mrr
elif sub.mrr < sub.previous_mrr:
movements["contraction"] += sub.previous_mrr - sub.mrr
for sub in canceled_this_month:
movements["churn"] += sub.previous_mrr
for sub in reactivated_this_month:
movements["reactivation"] += sub.mrr
net_new = (
movements["new"]
+ movements["expansion"]
+ movements["reactivation"]
- movements["contraction"]
- movements["churn"]
)
return movements, net_newWhy Stripe's dashboard MRR might differ from yours
Stripe has its own MRR calculation in the Billing dashboard. It typically differs from manual calculations because:
- Stripe counts trials in some views (your config might exclude them)
- Stripe uses the subscription's
plan.amount, not the invoice amount (pre-coupon vs. post-coupon) - Stripe handles proration differently — mid-cycle changes show full next-period amount
- Stripe's metered billing shows the most recent invoice amount
- Stripe ignores tax — MRR is pre-tax, but if your prices include tax (common in EU), Stripe might show a different number than your net revenue
If your number differs from Stripe's by < 5%, it's probably definitional. If it differs by > 10%, one of you has a bug.
From calculation to monitoring
Calculating MRR once is a script. Monitoring it continuously — detecting when it drops unexpectedly, when specific customers churn, when expansion slows — is an operational system.
The Python code above gives you a point-in-time snapshot. For ongoing monitoring, you need:
- Daily snapshots stored in a database for trend analysis
- Alert thresholds ("MRR dropped 5% day-over-day — investigate")
- Customer-level attribution ("who churned today and how much did they represent?")
- Forecast integration ("at current growth rate, when do we hit $X MRR?")
You can build this yourself (store daily snapshots in Postgres, run cron jobs, wire Slack alerts) or use tools designed for it. Fastero connects directly to Stripe and monitors MRR movements in real time, alerting you when individual customers churn, when MRR drifts from CRM expectations, or when failed payments put revenue at risk. But even running the script above weekly and comparing to last week's output will catch most problems before they compound.
The important thing is having a number you trust, that uses explicit definitional choices you've documented, and that matches what you tell your board.
Try Fastero free — connect your CRM and billing data, get live revenue dashboards, and set up alerts that catch leaks before your next board meeting. No credit card required.
Last updated: July 2026.

