What is expansion revenue rate?
Definition
Expansion revenue rate is the new recurring revenue generated inside the existing customer base — upsell, cross-sell, and seat growth — divided by the MRR that base started the period with. It is the growth component inside net revenue retention, isolated. Report expansion and contraction side by side, never netted: a base where heavy expansion masks equally heavy downgrades is a very different business from a quiet one, and a single net number cannot tell them apart.
What data do you need?
- A subscription-change history with one row per MRR delta, typed as upsell, cross-sell, seat change, downgrade, or churn — current MRR alone cannot produce this metric
- A starting-MRR snapshot per period, written by a scheduled job
- Change effective dates, so a delta lands in the period the revenue actually moves
- Subscription start dates, so expansion can be cohorted by customer age (time to first expansion)
- A stable account key that survives plan changes and entity renames
SQL pattern
-- subscription_changes has one row per MRR delta with a change_type.
-- Expansion and contraction are reported side by side, never netted.
WITH months AS (
SELECT generate_series(
date_trunc('month', CURRENT_DATE - INTERVAL '12 months'),
date_trunc('month', CURRENT_DATE - INTERVAL '1 month'),
INTERVAL '1 month'
)::date AS month
), starting AS (
-- Snapshot written on the first of each month; the denominator
-- must not drift as the month plays out.
SELECT snapshot_month AS month, SUM(mrr_usd) AS starting_mrr
FROM mrr_snapshots
GROUP BY 1
), changes AS (
SELECT
date_trunc('month', effective_at)::date AS month,
SUM(mrr_delta_usd) FILTER (
WHERE change_type IN ('upsell', 'cross_sell', 'seat_expansion')
) AS expansion_mrr,
SUM(-mrr_delta_usd) FILTER (
WHERE change_type IN ('downgrade', 'seat_reduction')
) AS contraction_mrr,
COUNT(DISTINCT account_id) FILTER (
WHERE change_type IN ('upsell', 'cross_sell', 'seat_expansion')
) AS expanding_accounts
FROM subscription_changes
GROUP BY 1
)
SELECT
m.month,
ROUND(st.starting_mrr, 2) AS starting_mrr,
ROUND(COALESCE(c.expansion_mrr, 0), 2) AS expansion_mrr,
ROUND(COALESCE(c.contraction_mrr, 0), 2) AS contraction_mrr,
ROUND(
100.0 * COALESCE(c.expansion_mrr, 0)
/ NULLIF(st.starting_mrr, 0), 2
) AS expansion_rate_pct,
ROUND(
100.0 * COALESCE(c.contraction_mrr, 0)
/ NULLIF(st.starting_mrr, 0), 2
) AS contraction_rate_pct,
COALESCE(c.expanding_accounts, 0) AS expanding_accounts
FROM months m
JOIN starting st USING (month)
LEFT JOIN changes c USING (month)
ORDER BY m.month;Common pitfalls
Where does this metric apply?
This metric commonly uses data from Stripe, Chargebee, Zuora, Maxio, Gainsight, plus any warehouse models built on account, subscription, and product-usage tables at the same grain.