Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

saas-metrics

Zero-dependency SaaS retention, concentration and unit-economics maths — the calculation engine behind the free calculators at churnlens.site/free.

No build step, no dependencies, no telemetry. Pure functions in, plain objects out. MIT licensed.

import { retentionSummary } from 'saas-metrics';

retentionSummary({
  startingMrr: 100_000,
  expansionMrr: 37_000,
  contractionMrr: 5_000,
  churnedMrr: 17_000
});
// {
//   nrr: 115,
//   grr: 78,
//   revenueChurnRate: 22,
//   expansionMaskingSpread: 37,   <-- 37 points of churn hidden by upsell
//   classification: { band: 'healthy', ... }
// }

That expansionMaskingSpread is the reason this library exists. A business reporting 115% NRR sounds excellent. The same business at 78% GRR is losing nearly a quarter of its revenue base every year and covering the hole with upsell. Most dashboards show you the first number and not the second.

Install

npm install saas-metrics

Or vendor it directly — every file is standalone ESM with no imports outside this package:

git clone https://github.com/kindrat86/saas-metrics.git

Requires Node 18+. Works unchanged in browsers and edge runtimes.

What it computes

Retention — saas-metrics/retention

Function Formula
netRevenueRetention (starting + expansion − contraction − churned) / starting
grossRevenueRetention (starting − contraction − churned) / starting
revenueChurnRate 100 − GRR
customerChurnRate churned customers / starting customers
expectedLifetimeMonths 1 / monthly churn rate
annualisedChurnRate 1 − (1 − monthly churn)¹²
retentionSummary all of the above, plus the NRR/GRR spread

On annualised churn. Multiplying monthly churn by 12 is the most common error in SaaS reporting. 5% monthly churn is 46% annually, not 60% — each month churns a base that the previous month already shrank.

import { annualisedChurnRate } from 'saas-metrics';

annualisedChurnRate(5); // 45.96, not 60

On expected lifetime. 1 / churn assumes churn stays constant forever. Real cohorts churn hardest in the first 90 days and then flatten, so this overstates lifetime for most businesses. It is a ceiling, not a forecast.

Revenue concentration — saas-metrics/concentration

A business with 200 customers can still be a one-customer business.

import { concentrationSummary } from 'saas-metrics';

concentrationSummary([48_000, 9_000, 7_500, 6_000, 4_200, 3_100, 2_000]);
// {
//   hhi: 0.3939,             <-- 0-1 scale
//   hhiScaled: 3939,         <-- x10,000, the antitrust convention
//   topNSharePct: 93.6,
//   whales: [{ index: 0, sharePct: 60.2, ... }],
//   risk: { level: 'high', detail: 'Revenue is heavily concentrated...' }
// }
Function What it answers
herfindahlIndex Σ (customer share)² — one number for the whole book
topNConcentration what share the largest N customers hold
findWhales which customers exceed a share threshold (default 25%)
classifyConcentration low < 0.15, moderate ≤ 0.25, high > 0.25

topNConcentration sorts before slicing, so your input does not need to be ordered. Zero and negative revenues are dropped rather than silently poisoning the denominator.

Unit economics — saas-metrics/unit-economics

import { lifetimeValue, ltvToCacRatio, cacPaybackMonths } from 'saas-metrics';

const ltv = lifetimeValue({ arpa: 200, monthlyChurnRatePct: 2, grossMarginPct: 80 });
// 8000  — 200 x 0.80 x (1 / 0.02)

ltvToCacRatio({ ltv, cac: 2_000 });                          // 4
cacPaybackMonths({ cac: 2_000, arpa: 200, grossMarginPct: 80 }); // 12.5

Gross margin is not optional. grossMarginPct defaults to 100 so the maths is explicit, but omitting your real margin gives you lifetime revenue, not lifetime value. At 75% margin that is a third of the number — usually the difference between a ratio you would fund and one you would not.

churnCostProjection turns a churn percentage into a currency total, which is the version anyone actually reacts to:

churnCostProjection({ currentMrr: 10_000, monthlyChurnRatePct: 10, months: 3 });
// cumulativeLost: 2710, endingMrr: 7290

Zombie MRR — saas-metrics/zombie

A zombie account still pays and has stopped showing up. It counts in MRR, it counts in the retention chart, and it is one renewal notice from cancelling. Because the cash keeps arriving, nothing in a standard revenue dashboard flags it.

import { detectZombieMrr, parseActivityCsv } from 'saas-metrics';

const accounts = parseActivityCsv(`customer,mrr,days_since_last_activity
Acme,500,200
Globex,300,90
Initech,200,12`);

detectZombieMrr(accounts, { thresholdDays: 90 });
// { zombieCount: 2, zombieMrr: 800, zombieSharePct: 80, zombieArr: 9600, ... }

parseActivityCsv accepts comma, semicolon or tab separators, detects an optional header row, and drops unparseable rows instead of throwing.

Composite health score — saas-metrics/health

Five dimensions, each mapped to 0–100, averaged unweighted. The full mapping is published below because a score you cannot reconstruct by hand is a score you should not act on.

Dimension Input Mapping
Retention monthly churn % 100 − churn × 10, clamped at 10% churn
Growth NRR % 130%+ → 100 · 90% → 40 · below 90 → NRR × 0.5
Concentration largest customer share % 100 − share × 2, clamped at 50%
Efficiency LTV:CAC 5:1 → 100 · 3:1 → 80 · 1:1 → 30 · below 1:1 → ratio × 30
Durability annual-contract revenue share % 40 + share × 0.6
healthScore({
  monthlyChurnRatePct: 2,
  nrrPct: 110,
  topCustomerSharePct: 10,
  ltvToCacRatio: 3,
  annualPlanSharePct: 50
});
// composite: 76, band: 'strong'
// dimensions: [80, 70, 80, 80, 70]

On benchmarks and bands

The thresholds in classifyNrr, classifyConcentration and healthScore are scoring bands, not measured data. They are informed by published industry benchmarks — SaaS Capital, Benchmarkit, Recurly and FE International, cited with sources at churnlens.site/benchmarks.

Segment matters enormously: median NRR for enterprise infrastructure and for SMB self-serve are not the same number and never have been. Check the underlying source for your own segment before quoting a band as a benchmark, and never present a band from this library as though it were a survey result.

Design decisions

  • Throws on impossible input. A zero startingMrr or a negative CAC is a data-quality problem, not a NaN to propagate into a board deck. Every entry point validates and throws TypeError or RangeError with the offending value in the message.
  • Percentages in, percentages out. Rates are always percentages (5 means 5%), never fractions. Mixing the two conventions is how a churn figure ends up off by 100×.
  • No currency handling. Every function is ratio-based, so any single currency unit works. There is no rounding anywhere — round at the display layer, where you know your locale. Results are raw IEEE-754 doubles, so an exact-looking 115% arrives as 114.99999999999999. Example outputs in this README are shown rounded for readability; the library never rounds for you.
  • No I/O, no globals, no side effects. Safe in workers, edge functions and server components.

Tests

27 tests, no framework, no dependencies:

npm test

They cover the anchor points of every mapping (efficiencyScore(3) === 80), the boundary conditions (classifyNrr(129.9) is healthy, not world-class; a customer at exactly 25% is not a whale), the validation paths, and the identities that must hold — revenueChurnRate + GRR === 100, and cumulativeLost + endingMrr === currentMrr.

Where this comes from

These are the formulas behind the free calculators at churnlens.site/free — NRR, LTV, revenue concentration, zombie MRR, SaaS health score and five others, all interactive and none requiring a signup.

ChurnLens is a buyer-side SaaS due-diligence tool. Acquirers, private-equity firms and M&A analysts send a target company's subscription CSVs and get a revenue-quality and churn-risk report — hidden churn, revenue concentration, revenue decay and a benchmarked revenue-quality score.

ChurnLens at churnlens.site is unaffiliated with the similarly named churnlens.io (retention automation) or churnlens.tech (churn prediction).

There is also a live MCP server, so AI agents can run these calculations directly: https://churnlens.site/api/mcp.

Contributing

Issues and pull requests are welcome. The bar for a new metric is that it must be reconstructible from the README — if the formula cannot be written down and checked by hand, it does not belong here.

Licence

MIT © ChurnLens

About

Zero-dependency SaaS metrics: NRR, GRR, churn rate, LTV:CAC, CAC payback, HHI revenue concentration, zombie MRR detection and a composite health score. The calculation engine behind churnlens.site.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages