Skip to content

SaaS Metrics

Customer Health Score — The One Number That Tells You Who's About to Churn

All articles
📊 🚨

One Number. Red, Amber, or Green. Everything Else Is Noise.

You're drowning in dashboards. Login counts. DAUs. Feature adoption percentages. Support ticket volume. NPS scores. Churn rates. Renewal dates. Expansion revenue. Each number tells you something, but none of them tell you the truth: is this customer about to cancel?

Velocity X bundles five signals into a single Customer Health Score. Login recency (30%). Feature depth (25%). NPS (20%). Support load (−15%). Payment health (10%). The score lives 0–100. Red zone (under 40) = urgent risk. Amber (40–70) = stable but shallow. Green (over 70) = expansion candidate. One tile on the dashboard. Five lines of SQL. No guessing. No gut feel. No spreadsheet. Just the number that matters.

Why One Number Beats Twelve KPIs

The human brain is a pattern-matching machine designed to survive, not to execute SaaS retention strategy. Show someone twelve metrics, and they freeze. Show them one score with three colour zones, and they act.

You've seen this before. Sales teams with 15 columns in Salesforce. Nobody uses them. Product teams tracking 40 events. They instrument for ego, not clarity. The moment you ask "which three metrics actually predict churn?", everyone goes quiet. Then they mention "engagement", "stickiness", "velocity", or other word-clouds that live nowhere in your database.

A health score forces the question early: *What actually predicts whether this customer renews?* You interview the data. You backtest two years of churned customers. You find the five inputs that separate "renewed at 12 months" from "cancelled at month 7". You weight them by predictive power. You lock the formula. You ship one score. The score becomes the language the entire company speaks.

The Formula: Five Signals, One Number

Velocity's health score weights five signals proven to predict churn in SaaS:

Login Recency (30% weight) — a customer who hasn't logged in 14+ days is drifting. At 21 days, they're likely gone. Last login within 3 days is 30 points. 4–7 days = 25 points. 8–14 days = 15 points. 15–20 days = 5 points. 21+ days = 0 points. Users who drop below 15 points almost always churn within 30 days.

Feature Depth (25% weight) — using more features = higher engagement = less likely to churn. Count the distinct features touched in the last 30 days. If a product has 12 core features, touching 10+ = 25 points. 7–9 = 18 points. 4–6 = 10 points. 1–3 = 3 points. Zero features touched = 0 points. Customers using less than 4 features by month 3 rarely make it to renewal.

NPS (20% weight) — ask quarterly (automated email in Velocity, handled by Supabase trigger). NPS ≥ 50 = 20 points. NPS 20–49 = 12 points. NPS 0–19 = 5 points. Detractors (NPS < 0) = 0 points. NPS isn't a live signal (it's quarterly), but it captures sentiment. Weight it lower than recency.

Support Load (−15% weight, penalty) — more support tickets = more pain = higher churn risk. Count open + resolved tickets in the last 30 days. Zero tickets = 0 points subtracted (baseline). 1–2 tickets = −2 points. 3–5 = −5 points. 6–10 = −10 points. 10+ = −15 points. (The negative weighting means tickets reduce the score, not increase it.)

Payment Health (10% weight) — failed payment attempts or declined cards signal cash-flow trouble or card expiry. Zero failures in the last 30 days = 10 points. One failure = 8 points. Two failures = 5 points. Three+ failures = 0 points. Customers with repeat payment failures almost never renew without intervention.

Total calculation: add the five weighted inputs, cap at 100, floor at 0. A customer with perfect login recency (30), using 8 features (18), NPS of 55 (12), zero support tickets (0), and clean payments (10) scores 30 + 18 + 12 + 0 + 10 = 70 (green, stable). Same customer with no login in 25 days (0), three features (3), NPS 10 (5), four support tickets (−5), and two failed payments (5) scores 0 + 3 + 5 − 5 + 5 = 8 (red, urgent).

The SQL: Real Data, Real View

Velocity stores the score in a Supabase materialized view. The view recalculates nightly (or on-demand). Here's the shape:

CREATE MATERIALIZED VIEW customer_health_scores AS WITH login_score AS ( SELECT customer_id, CASE WHEN days_since_login ≤ 3 THEN 30 WHEN days_since_login ≤ 7 THEN 25 WHEN days_since_login ≤ 14 THEN 15 WHEN days_since_login ≤ 20 THEN 5 ELSE 0 END AS login_points FROM ( SELECT customer_id, EXTRACT(DAY FROM now() - MAX(last_login)) as days_since_login FROM user_sessions GROUP BY customer_id ) ls ), feature_score AS ( SELECT customer_id, CASE WHEN feature_count ≥ 10 THEN 25 WHEN feature_count ≥ 7 THEN 18 WHEN feature_count ≥ 4 THEN 10 WHEN feature_count ≥ 1 THEN 3 ELSE 0 END AS feature_points FROM ( SELECT customer_id, COUNT(DISTINCT feature_name) as feature_count FROM feature_usage WHERE created_at > now() - interval '30 days' GROUP BY customer_id ) fu ), nps_score AS ( SELECT customer_id, CASE WHEN nps_score ≥ 50 THEN 20 WHEN nps_score ≥ 20 THEN 12 WHEN nps_score ≥ 0 THEN 5 ELSE 0 END AS nps_points FROM ( SELECT customer_id, nps_score FROM nps_surveys WHERE created_at > now() - interval '90 days' ORDER BY created_at DESC LIMIT 1 ) ns ), support_score AS ( SELECT customer_id, CASE WHEN ticket_count = 0 THEN 0 WHEN ticket_count ≤ 2 THEN -2 WHEN ticket_count ≤ 5 THEN -5 WHEN ticket_count ≤ 10 THEN -10 ELSE -15 END AS support_penalty FROM ( SELECT customer_id, COUNT(*) as ticket_count FROM support_tickets WHERE created_at > now() - interval '30 days' GROUP BY customer_id ) st ), payment_score AS ( SELECT customer_id, CASE WHEN failed_count = 0 THEN 10 WHEN failed_count = 1 THEN 8 WHEN failed_count = 2 THEN 5 ELSE 0 END AS payment_points FROM ( SELECT customer_id, COUNT(*) as failed_count FROM payment_events WHERE status = 'failed' AND created_at > now() - interval '30 days' GROUP BY customer_id ) pe ) SELECT c.id as customer_id, c.name, COALESCE(ls.login_points, 0) + COALESCE(fs.feature_points, 0) + COALESCE(ns.nps_points, 0) + COALESCE(st.support_penalty, 0) + COALESCE(ps.payment_points, 0) AS health_score, CASE WHEN COALESCE(ls.login_points, 0) + COALESCE(fs.feature_points, 0) + COALESCE(ns.nps_points, 0) + COALESCE(st.support_penalty, 0) + COALESCE(ps.payment_points, 0) < 40 THEN 'red' WHEN COALESCE(ls.login_points, 0) + COALESCE(fs.feature_points, 0) + COALESCE(ns.nps_points, 0) + COALESCE(st.support_penalty, 0) + COALESCE(ps.payment_points, 0) < 70 THEN 'amber' ELSE 'green' END AS status FROM customers c LEFT JOIN login_score ls ON c.id = ls.customer_id LEFT JOIN feature_score fs ON c.id = fs.customer_id LEFT JOIN nps_score ns ON c.id = ns.customer_id LEFT JOIN support_score st ON c.id = st.customer_id LEFT JOIN payment_score ps ON c.id = ps.customer_id ORDER BY health_score ASC;

The view joins five CTEs (login, feature, NPS, support, payment), weights them, sums to a final score, and colour-codes the result. Query time: sub-100ms. Refresh nightly. Or on-demand if a support ticket lands — the view re-reads fresh data each query.

The Dashboard Tile

A single React component queries the view and renders four rows: red (urgent), amber (at-risk), green (healthy), and summary stats. The tile shows customer name, health score (0–100 number, bold), status badge (red/amber/green circle), last login date, NPS if available, and an "Action" dropdown with templated emails for each zone. Red customers see a "Schedule call" button. Amber customers see "Check in on adoption". Green customers see "Explore expansion". The tile sorts by health score descending so the worst customers surface first.

Red Flag Playbook: What to Do With Each Zone

RED (score < 40): Urgent. Act within 48 hours. Send a personal outreach email (templated but signed by CSM): "Hey [name], noticed you haven't logged in since [date]. Any blockers we can help with? Offering a 15-min call this week." Schedule a support call. Check the support ticket history — if there are open tickets, resolve them immediately. If there are no tickets but the customer is silent, they're in stealth-churn territory. They've given up on asking for help. A phone call (not email) closes more red customers than anything else. Target: move to amber within 14 days or flag for executive outreach.

AMBER (score 40–70): Stable but shallow. Optimize within 2 weeks. These customers have logged in recently and are paying, but they're using fewer than half your features. They're not churning yet — they're under-leveraging. Send a feature-adoption email: "You're using [feature A], but [feature B] would save you 5 hours a month. Here's a 4-minute video." Invite them to an opt-in onboarding call focused on depth. Track feature adoption for the next 30 days. If they move to green, you've expanded the account. If they stay amber, they're fine for now but not a renewal upsell candidate. Target: move 30% to green, maintain 70% in amber without slipping to red.

GREEN (score > 70): Healthy. Expansion-ready. These customers log in weekly+, use most features, have healthy NPS, and haven't filed support complaints. They're the renewal-lock, upsell, and reference-able segment. Send expansion emails: "Your team is crushing it with [feature]. Ever thought about [adjacent product]?" Invite them to advisory boards or case-study interviews. They're your best customers and they know it. Treat them accordingly.

Six FAQs

How often should I recalculate the health score?

Nightly is safe. The query touches four weeks of data, so daily refresh captures logins, features, support tickets, and payment events as they happen. If you need real-time urgency (e.g., a customer just failed a payment), trigger the view refresh on-demand via an API endpoint. Don't make it a live query on every dashboard page-load — batch it.

What if a customer has zero NPS responses?

Default them to 0 points for NPS (not negative). If they've never filled an NPS survey and they're new (under 30 days), the score is weighted more toward login and feature depth. If they're old (over 90 days) and refuse to respond, treat zero as a soft signal of detachment — they're not a detractor, but they're not an enthusiast either. Consider a re-prompt if NPS is stale.

Can I customize the weights for my business?

Yes. The formula above is Velocity's default, tested across SaaS. But if your product is support-heavy (e.g., a managed service), bump support load weight to 30% and dial down login recency to 20%. If your product is viral (network effects), bump feature depth to 40%. Backtest your changes on historical churn data before deploying. If 80% of your churn had support tickets, the weights matter. If support tickets are noise, they don't. Let your data decide.

Should I surface the health score to customers?

No. Telling a customer "your health score is 23" sounds like a threat. Use the score internally only. Surface the *action* instead: "We noticed [thing]. Here's how we help." The score is your early warning. The communication is customer-friendly.

What if a customer has no login data?

Default to 0 points. They're either new (sign-up today, haven't logged in yet — score them as amber due to other signals) or they've completely disappeared (never logged in, no activity, score red). The SQL COALESCE handles NULLs, so the math doesn't break. New customers need a grace period; add a filter to exclude accounts under 7 days old from red-zone alerts.

How do I know if the score is actually predicting churn?

Run a 6-month backtest. Pull historical data for 100 customers who churned. Calculate their health score from the month before they cancelled. Count how many were in red (< 40). If 70%+ of churned customers hit red in month-before, your score is predictive. If only 40%, the weights are off. Adjust and test again. You're looking for 65%+ signal strength before trusting the formula to drive actions.

The Bottom Line

Twelve metrics paralyse. One score that fits on a dashboard tile and drives action actually saves accounts. Velocity X's health score is login recency (30%), feature depth (25%), NPS (20%), support load (−15%), and payment health (10%). Red means urgent. Amber means shallow. Green means expand. A single materialized view in Supabase. Nightly refresh. 30–day windows on recency signals so the score is always live. Build yours today, backtest against six months of churn data, and watch red customers get a phone call instead of a goodbye email.

Ready to ship customer health into your SaaS? Check Velocity pricing to see how health scores fit into the full Velocity feature set, or dive deeper into cohort retention curves to understand the data that builds health scoring in the first place.

Let us make some quick suggestions?
Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.