Which of your customers are about to churn? A practical risk check for a SaaS with 50 customers
With around 50 customers you don't need a prediction model. Judge accounts, watch for change, use a transparent score and contact the top five every week. SQL and a scoring function included.
By Christopher Krohn, Founder of Atune · Updated October 19, 2026
Christopher is a SaaS founder and startup advisor who has seen first-hand how much easy-to-use, well-connected analytics matter to the success of SaaS companies. He is building Atune to help the next generation of software builders succeed with their startups.
To see which customers are about to churn in a small SaaS, look for change at the account level: fewer active days, less use of what they pay for, more days since anyone was seen, a falling trend, a champion who went quiet, or lower spend. Score it transparently and review the riskiest accounts every week.
With 50 customers you can't run a churn-prediction model. There isn't enough history for one to mean anything, and you don't need one. You need a short list each week: which customers look different from last week, and who should I talk to? This guide gives you that list with a SQL query, a score you can explain to yourself, and a weekly routine. At the end there's a section on how Atune does it for you.
Judge accounts, not users
If you sell to companies, churn happens at the account: the team or company that pays. One person going quiet in a ten-seat account is a different story from the only user going quiet. So group activity by account.
If you can't say what the account is, ask: what would you bill as one unit? That's the account. For a B2C product, each person is their own account.
The signals that matter at this size
Before customers cancel, their usage usually changes. You're looking for change, not low numbers in isolation:
- Active days. On how many distinct days in the last week did anyone at the account use the product? Count days, not events: one long session shouldn't look like a healthy week.
- Value use. Did they do the thing they pay for (sent invoices, generated reports), or just log in?
- Recency. Days since anyone at the account was last seen.
- Trend. This week compared with last week.
- Champion went quiet. The most active person at the account dropped sharply. Often the clearest early signal in small accounts.
- Spend went down. Fewer seats, a cheaper plan, lower MRR. Lower spend often comes before churn.
- Trial that never reached value. Not churn yet, but a trial that hasn't done the core action by the end of the trial won't convert.
Step 1: Pull the numbers
Assuming an events table with account_id, user_id, name and
created_at, and an accounts table with every customer:
with daily as (
select account_id,
created_at::date as day,
count(*) as events,
count(*) filter (where name in ('report.generated')) as value_events -- your value events
from events
where created_at > now() - interval '14 days'
group by 1, 2
),
w as (
select account_id,
count(distinct day) filter (where day > current_date - 7) as active_days_7,
coalesce(sum(value_events) filter (where day > current_date - 7), 0) as value_7,
coalesce(sum(events) filter (where day > current_date - 7), 0) as events_this_week,
coalesce(sum(events) filter (where day <= current_date - 7), 0) as events_last_week,
max(day) as last_active
from daily
group by 1
)
select a.id as account_id,
a.name,
coalesce(w.active_days_7, 0) as active_days_7,
coalesce(w.value_7, 0) as value_7,
coalesce(current_date - w.last_active, 14) as days_quiet, -- 14+ if nothing in two weeks
coalesce(w.events_this_week, 0) as events_this_week,
coalesce(w.events_last_week, 0) as events_last_week
from accounts a
left join w on w.account_id = a.id
order by days_quiet desc, active_days_7 asc;Start from accounts, not events. The customers that matter most are the
ones with no rows this week, and a query over events alone silently drops
them.
Step 2: Turn it into a score you can explain
You don't need a black box. Here's a simple, transparent model. The weights and thresholds are the ones Atune publishes for its own user health score: a public starting point you can adjust.
| Factor | Weight | Measure |
|---|---|---|
| Active days | 35% | Distinct active days in the last 7. 5 of 7 counts as fully engaged, so a weekday-only B2B customer isn't penalised for weekends. |
| Value use | 35% | Value events in the last 7 days against what a healthy account does. |
| Recency | 20% | 100 if seen today, falling to 0 after 14 days without activity. |
| Trend | 10% | This week compared with last week. |
type Row = {
activeDays7: number;
value7: number;
daysQuiet: number;
eventsThisWeek: number;
eventsLastWeek: number;
};
// expectedValue7: value events a healthy account does in a week (look at your best customers)
function healthScore(r: Row, expectedValue7: number): number {
const activeDays = Math.min(r.activeDays7 / 5, 1) * 100;
const valueUse = expectedValue7 > 0 ? Math.min(r.value7 / expectedValue7, 1) * 100 : activeDays;
const recency = Math.max(0, 1 - r.daysQuiet / 14) * 100; // linear here; adjust to taste
// Trend: -50% or worse → 0, flat → 50, +50% or better → 100 (a simple choice, not a standard)
const change = r.eventsLastWeek > 0 ? (r.eventsThisWeek - r.eventsLastWeek) / r.eventsLastWeek : 0;
const trend = Math.min(Math.max(50 + change * 100, 0), 100);
return Math.round(0.35 * activeDays + 0.35 * valueUse + 0.2 * recency + 0.1 * trend);
}Then label each account, with the same thresholds as Atune's published model:
| Label | Rule |
|---|---|
| Inactive | No activity in 14 days |
| New | First seen less than 7 days ago: don't judge yet |
| Risk | Score below 40, or below 55 and down more than 10% in a week |
| Healthy | Score 65 or higher |
| Watch | Everything in between |
Add two flags the score doesn't capture: spend went down (from your billing data) and champion quiet (the top user's events this week far below their usual week).
Step 3: The weekly 30-minute review
Every Monday:
-
Sort by label: Inactive and Risk first, then anything with a flag.
-
For each account at the top, write one sentence of why from the numbers: "No login in 14 days, used to be daily." If you can't write the sentence, the score is misleading you; look at the raw events.
-
Send a short, personal message. Name what you saw, ask one question, offer help. For example:
Hi Maria, I noticed the team hasn't been in since the CSV import failed last week. Want me to run it for you, or send a file that works?
-
Note what they say. After a few weeks you'll know which signals actually come before cancellations in your product, and you can reweight.
Keep the list short. Five accounts you actually contact beat a dashboard of 50 colours.
What doesn't work at this size
- Predictive churn scores. With a handful of cancellations a year, any "model" mostly memorises noise.
- NPS as the main signal. Useful context, but slow and sparse.
- A health dashboard nobody opens. If it isn't a list you act on weekly, it decays.
The automated route: Atune
Disclosure: I build Atune.
Atune does this job without you maintaining the query, the score or the routine. It reads how people use your product and puts the few customers who need you in an Inbox: who is stuck, who is slipping, why, how sure it is, and a drafted message.
- Accounts and users scored with nothing to set up. The user health model above is Atune's own, computed for every person; accounts are scored too, on the same thresholds.
- It works out your value events. You don't have to tell it which actions count.
- It knows who pays, once you say so. Send the account's stage and, optionally, plan, seats and MRR with your events, or map the Stripe subscription or Clerk Billing plan your app already has (Billing status). Until you do, accounts are shown as an assumed trial and the advice that only applies to paying customers, such as churn risk and champion cooling, stays off. Atune doesn't guess who pays.
- Downgrades become actions. A paying account that lowers its MRR, drops seats, moves to a cheaper plan or goes back to trial gets a Downgraded action in the Inbox for 14 days (Trial, paying and subscription value).
- It says when it doesn't know. Each action explains why and how confident
it is. Developers can ask the same questions in code with
understand("account:acme", "how healthy is this account?"), which returnsknows: falserather than guessing.
What it doesn't do: it doesn't contact your customers for you (it drafts; you send), and it only sees the behaviour you send it, server-side.
Availability: closed beta for software teams in the EU and EEA; apply for access. The Build plan is free for up to 25 active customer accounts; paid plans are on the pricing page.
Summary
- Judge accounts, not users.
- Look for change: active days, value use, recency, trend, a quiet champion, lower spend.
- Use a transparent score with published weights, then reweight from what customers tell you.
- Every week, contact the top five with a specific, personal note.
Is anyone actually using your SaaS? How to find out who activated and who never came back
Define one activation event, record it server-side, and sort every new user into never used, one and done, exploring or activated. A method you can run this week, with SQL.
Changelog
Product and developer-platform changes, newest first.