2 min read
SaaSSaaS Analytics & Metrics
SaaS Metriken verstehen und tracken. MRR, Churn, LTV, CAC und Product Analytics für datengetriebenes Wachstum.
SaaS MetricsMRRARRChurn RateLTVCAC

SaaS Analytics & Metrics
Meta-Description: SaaS Metriken verstehen und tracken. MRR, Churn, LTV, CAC und Product Analytics für datengetriebenes Wachstum.
Keywords: SaaS Metrics, MRR, ARR, Churn Rate, LTV, CAC, Product Analytics, Cohort Analysis, Retention
Einführung
SaaS Metriken sind das Cockpit für Wachstum. Von MRR bis Churn Rate – die richtigen KPIs messen und verstehen ist entscheidend für fundierte Entscheidungen. Dieser Guide zeigt die wichtigsten Metriken und deren Implementation.
SaaS Metrics Overview
┌─────────────────────────────────────────────────────────────┐
│ SAAS METRICS FRAMEWORK │
├─────────────────────────────────────────────────────────────┤
│ │
│ Revenue Metrics: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ MRR (Monthly Recurring Revenue): │ │
│ │ ├── New MRR (New customers) │ │
│ │ ├── Expansion MRR (Upgrades, add-ons) │ │
│ │ ├── Contraction MRR (Downgrades) │ │
│ │ └── Churned MRR (Cancellations) │ │
│ │ │ │
│ │ Net MRR = New + Expansion - Contraction - Churn │ │
│ │ │ │
│ │ ARR = MRR × 12 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Growth Metrics: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ CAC (Customer Acquisition Cost): │ │
│ │ = (Sales + Marketing Costs) / New Customers │ │
│ │ │ │
│ │ LTV (Lifetime Value): │ │
│ │ = ARPU / Churn Rate │ │
│ │ │ │
│ │ LTV:CAC Ratio: │ │
│ │ ├── < 1:1 = Losing money │ │
│ │ ├── 3:1 = Healthy │ │
│ │ └── > 5:1 = Under-investing in growth │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Retention Metrics: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Churn Rate: │ │
│ │ = Customers Lost / Total Customers × 100 │ │
│ │ │ │
│ │ Net Revenue Retention (NRR): │ │
│ │ = (MRR + Expansion - Contraction - Churn) / MRR │ │
│ │ ├── < 100% = Net negative (shrinking) │ │
│ │ ├── 100% = Flat │ │
│ │ └── > 100% = Net positive (growing) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Product Metrics: │
│ ├── DAU/MAU (Daily/Monthly Active Users) │
│ ├── Feature Adoption Rate │
│ ├── Time to Value │
│ └── NPS (Net Promoter Score) │
│ │
└─────────────────────────────────────────────────────────────┘Metrics Implementation
// lib/analytics/metrics.ts
import { db } from '@/lib/db';
import { startOfMonth, endOfMonth, subMonths, differenceInDays } from 'date-fns';
interface MRRBreakdown {
newMRR: number;
expansionMRR: number;
contractionMRR: number;
churnedMRR: number;
netMRR: number;
totalMRR: number;
}
export async function calculateMRR(date: Date = new Date()): Promise<MRRBreakdown> {
const monthStart = startOfMonth(date);
const monthEnd = endOfMonth(date);
const prevMonthStart = startOfMonth(subMonths(date, 1));
const prevMonthEnd = endOfMonth(subMonths(date, 1));
// Get active subscriptions at end of month
const activeSubscriptions = await db.subscription.findMany({
where: {
status: 'active',
startDate: { lte: monthEnd }
},
include: { plan: true }
});
const totalMRR = activeSubscriptions.reduce(
(sum, sub) => sum + sub.plan.monthlyPrice,
0
);
// New subscriptions this month
const newSubscriptions = await db.subscription.findMany({
where: {
startDate: { gte: monthStart, lte: monthEnd },
previousSubscriptionId: null
},
include: { plan: true }
});
const newMRR = newSubscriptions.reduce(
(sum, sub) => sum + sub.plan.monthlyPrice,
0
);
// Upgrades this month
const upgrades = await db.subscriptionChange.findMany({
where: {
type: 'upgrade',
changedAt: { gte: monthStart, lte: monthEnd }
}
});
const expansionMRR = upgrades.reduce(
(sum, change) => sum + change.mrrDelta,
0
);
// Downgrades this month
const downgrades = await db.subscriptionChange.findMany({
where: {
type: 'downgrade',
changedAt: { gte: monthStart, lte: monthEnd }
}
});
const contractionMRR = Math.abs(downgrades.reduce(
(sum, change) => sum + change.mrrDelta,
0
));
// Churned this month
const churned = await db.subscription.findMany({
where: {
status: 'canceled',
canceledAt: { gte: monthStart, lte: monthEnd }
},
include: { plan: true }
});
const churnedMRR = churned.reduce(
(sum, sub) => sum + sub.plan.monthlyPrice,
0
);
const netMRR = newMRR + expansionMRR - contractionMRR - churnedMRR;
return {
newMRR,
expansionMRR,
contractionMRR,
churnedMRR,
netMRR,
totalMRR
};
}
export async function calculateChurnRate(date: Date = new Date()): Promise<{
customerChurn: number;
revenueChurn: number;
netRevenueRetention: number;
}> {
const monthStart = startOfMonth(date);
const prevMonthStart = startOfMonth(subMonths(date, 1));
const prevMonthEnd = endOfMonth(subMonths(date, 1));
// Customers at start of month
const customersAtStart = await db.subscription.count({
where: {
status: 'active',
startDate: { lt: monthStart }
}
});
// Customers churned this month
const customersChurned = await db.subscription.count({
where: {
status: 'canceled',
canceledAt: { gte: monthStart }
}
});
const customerChurn = customersAtStart > 0
? (customersChurned / customersAtStart) * 100
: 0;
// MRR calculations for revenue churn
const mrrAtStart = await db.subscription.aggregate({
where: {
status: 'active',
startDate: { lt: monthStart }
},
_sum: { monthlyAmount: true }
});
const startMRR = mrrAtStart._sum.monthlyAmount || 0;
const { churnedMRR, expansionMRR, contractionMRR } = await calculateMRR(date);
const revenueChurn = startMRR > 0
? ((churnedMRR + contractionMRR) / startMRR) * 100
: 0;
const netRevenueRetention = startMRR > 0
? ((startMRR + expansionMRR - contractionMRR - churnedMRR) / startMRR) * 100
: 100;
return {
customerChurn,
revenueChurn,
netRevenueRetention
};
}
export async function calculateLTV(): Promise<{
averageLTV: number;
ltv_by_plan: Record<string, number>;
}> {
// Average customer lifespan
const canceledSubscriptions = await db.subscription.findMany({
where: { status: 'canceled' },
select: {
startDate: true,
canceledAt: true,
monthlyAmount: true,
plan: { select: { name: true } }
}
});
if (canceledSubscriptions.length === 0) {
return { averageLTV: 0, ltv_by_plan: {} };
}
// Calculate average lifespan and ARPU
let totalLifespanMonths = 0;
let totalRevenue = 0;
const planData: Record<string, { revenue: number; months: number; count: number }> = {};
canceledSubscriptions.forEach(sub => {
const months = differenceInDays(sub.canceledAt!, sub.startDate) / 30;
const revenue = months * sub.monthlyAmount;
totalLifespanMonths += months;
totalRevenue += revenue;
const planName = sub.plan.name;
if (!planData[planName]) {
planData[planName] = { revenue: 0, months: 0, count: 0 };
}
planData[planName].revenue += revenue;
planData[planName].months += months;
planData[planName].count += 1;
});
const averageLTV = totalRevenue / canceledSubscriptions.length;
const ltv_by_plan: Record<string, number> = {};
Object.entries(planData).forEach(([plan, data]) => {
ltv_by_plan[plan] = data.revenue / data.count;
});
return { averageLTV, ltv_by_plan };
}
export async function calculateCAC(
startDate: Date,
endDate: Date
): Promise<{
totalCAC: number;
cacByChannel: Record<string, number>;
}> {
// Get marketing and sales costs
const costs = await db.marketingCost.aggregate({
where: {
date: { gte: startDate, lte: endDate }
},
_sum: { amount: true }
});
// Get new customers in period
const newCustomers = await db.subscription.count({
where: {
startDate: { gte: startDate, lte: endDate },
previousSubscriptionId: null
}
});
const totalCosts = costs._sum.amount || 0;
const totalCAC = newCustomers > 0 ? totalCosts / newCustomers : 0;
// CAC by channel
const costsByChannel = await db.marketingCost.groupBy({
by: ['channel'],
where: { date: { gte: startDate, lte: endDate } },
_sum: { amount: true }
});
const customersByChannel = await db.subscription.groupBy({
by: ['acquisitionChannel'],
where: {
startDate: { gte: startDate, lte: endDate },
previousSubscriptionId: null
},
_count: true
});
const cacByChannel: Record<string, number> = {};
costsByChannel.forEach(cost => {
const customers = customersByChannel.find(
c => c.acquisitionChannel === cost.channel
)?._count || 0;
cacByChannel[cost.channel] = customers > 0
? (cost._sum.amount || 0) / customers
: 0;
});
return { totalCAC, cacByChannel };
}Cohort Analysis
// lib/analytics/cohorts.ts
import { db } from '@/lib/db';
import { startOfMonth, format, differenceInMonths } from 'date-fns';
interface CohortData {
cohort: string; // "2024-01"
size: number;
retention: number[]; // Retention % for each month
revenue: number[]; // Revenue for each month
}
export async function generateRetentionCohorts(
months: number = 12
): Promise<CohortData[]> {
const cohorts: CohortData[] = [];
const today = new Date();
for (let i = months - 1; i >= 0; i--) {
const cohortDate = startOfMonth(subMonths(today, i));
const cohortKey = format(cohortDate, 'yyyy-MM');
// Get users who signed up in this cohort
const cohortUsers = await db.user.findMany({
where: {
createdAt: {
gte: cohortDate,
lt: startOfMonth(subMonths(today, i - 1))
}
},
select: { id: true }
});
const cohortSize = cohortUsers.length;
if (cohortSize === 0) continue;
const userIds = cohortUsers.map(u => u.id);
const retention: number[] = [];
const revenue: number[] = [];
// Calculate retention for each subsequent month
const monthsSinceCohor = differenceInMonths(today, cohortDate);
for (let m = 0; m <= monthsSinceCohor; m++) {
const checkMonth = startOfMonth(subMonths(today, monthsSinceCohor - m));
// Count active users in this month
const activeUsers = await db.userActivity.groupBy({
by: ['userId'],
where: {
userId: { in: userIds },
activityDate: {
gte: checkMonth,
lt: startOfMonth(subMonths(today, monthsSinceCohor - m - 1))
}
}
});
const retentionRate = (activeUsers.length / cohortSize) * 100;
retention.push(Math.round(retentionRate * 10) / 10);
// Calculate revenue from this cohort in this month
const monthRevenue = await db.payment.aggregate({
where: {
userId: { in: userIds },
paidAt: {
gte: checkMonth,
lt: startOfMonth(subMonths(today, monthsSinceCohor - m - 1))
}
},
_sum: { amount: true }
});
revenue.push(monthRevenue._sum.amount || 0);
}
cohorts.push({
cohort: cohortKey,
size: cohortSize,
retention,
revenue
});
}
return cohorts;
}Analytics Dashboard
// app/admin/analytics/page.tsx
'use client';
import { useState, useEffect } from 'react';
import { Line, Bar } from 'react-chartjs-2';
interface DashboardMetrics {
mrr: {
total: number;
growth: number;
breakdown: {
new: number;
expansion: number;
contraction: number;
churn: number;
};
};
customers: {
total: number;
new: number;
churned: number;
churnRate: number;
};
revenue: {
ltv: number;
cac: number;
ltvCacRatio: number;
nrr: number;
};
mrrHistory: Array<{ month: string; mrr: number }>;
}
export default function AnalyticsDashboard() {
const [metrics, setMetrics] = useState<DashboardMetrics | null>(null);
const [dateRange, setDateRange] = useState('12m');
useEffect(() => {
fetch(`/api/analytics/dashboard?range=${dateRange}`)
.then(res => res.json())
.then(setMetrics);
}, [dateRange]);
if (!metrics) return <div>Loading...</div>;
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<h1 className="text-2xl font-bold">Analytics Dashboard</h1>
<select
value={dateRange}
onChange={(e) => setDateRange(e.target.value)}
className="border rounded px-3 py-2"
>
<option value="3m">Last 3 months</option>
<option value="6m">Last 6 months</option>
<option value="12m">Last 12 months</option>
</select>
</div>
{/* Key Metrics */}
<div className="grid grid-cols-4 gap-4">
<MetricCard
title="MRR"
value={`$${metrics.mrr.total.toLocaleString()}`}
change={metrics.mrr.growth}
changeLabel="vs last month"
/>
<MetricCard
title="Customers"
value={metrics.customers.total.toLocaleString()}
subtitle={`+${metrics.customers.new} new this month`}
/>
<MetricCard
title="Churn Rate"
value={`${metrics.customers.churnRate.toFixed(1)}%`}
status={metrics.customers.churnRate < 5 ? 'good' : 'warning'}
/>
<MetricCard
title="LTV:CAC"
value={`${metrics.revenue.ltvCacRatio.toFixed(1)}:1`}
status={metrics.revenue.ltvCacRatio >= 3 ? 'good' : 'warning'}
/>
</div>
{/* MRR Chart */}
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-semibold mb-4">MRR Growth</h2>
<Line
data={{
labels: metrics.mrrHistory.map(h => h.month),
datasets: [{
label: 'MRR',
data: metrics.mrrHistory.map(h => h.mrr),
borderColor: '#3b82f6',
tension: 0.3
}]
}}
options={{
scales: {
y: {
beginAtZero: true,
ticks: {
callback: (value) => `$${value.toLocaleString()}`
}
}
}
}}
/>
</div>
{/* MRR Breakdown */}
<div className="grid grid-cols-2 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-semibold mb-4">MRR Breakdown</h2>
<MRRWaterfall data={metrics.mrr.breakdown} />
</div>
<div className="bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-semibold mb-4">Unit Economics</h2>
<div className="space-y-4">
<div className="flex justify-between">
<span>Customer Lifetime Value (LTV)</span>
<span className="font-semibold">
${metrics.revenue.ltv.toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span>Customer Acquisition Cost (CAC)</span>
<span className="font-semibold">
${metrics.revenue.cac.toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span>Net Revenue Retention (NRR)</span>
<span className={`font-semibold ${
metrics.revenue.nrr >= 100 ? 'text-green-600' : 'text-red-600'
}`}>
{metrics.revenue.nrr.toFixed(0)}%
</span>
</div>
</div>
</div>
</div>
</div>
);
}
function MetricCard({
title,
value,
change,
changeLabel,
subtitle,
status
}: {
title: string;
value: string;
change?: number;
changeLabel?: string;
subtitle?: string;
status?: 'good' | 'warning' | 'bad';
}) {
return (
<div className="bg-white p-6 rounded-lg shadow">
<h3 className="text-sm text-gray-500 mb-1">{title}</h3>
<div className="flex items-baseline gap-2">
<span className="text-3xl font-bold">{value}</span>
{status && (
<span className={`w-3 h-3 rounded-full ${
status === 'good' ? 'bg-green-500' :
status === 'warning' ? 'bg-yellow-500' : 'bg-red-500'
}`} />
)}
</div>
{change !== undefined && (
<p className={`text-sm mt-1 ${change >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{change >= 0 ? '+' : ''}{change.toFixed(1)}% {changeLabel}
</p>
)}
{subtitle && (
<p className="text-sm text-gray-500 mt-1">{subtitle}</p>
)}
</div>
);
}Metrics Benchmarks
| Metric | Seed/Early | Series A | Scale |
|---|---|---|---|
| **MRR Growth** | 15-20%/mo | 10-15%/mo | 5-10%/mo |
| **Churn Rate** | < 8% | < 5% | < 3% |
| **NRR** | > 90% | > 100% | > 120% |
| **LTV:CAC** | > 1:1 | > 3:1 | > 5:1 |
| **CAC Payback** | < 18mo | < 12mo | < 8mo |
Fazit
SaaS Analytics erfordert:
- Revenue Metrics: MRR, ARR, NRR verstehen
- Cohort Analysis: Retention über Zeit tracken
- Unit Economics: LTV:CAC optimieren
- Automation: Dashboards automatisch aktualisieren
Datengetriebene Entscheidungen führen zu nachhaltigem Wachstum.
Bildprompts
- "SaaS metrics dashboard, MRR chart and key performance indicators"
- "Cohort retention heatmap, color-coded percentages"
- "LTV:CAC ratio visualization, balance scale metaphor"