Product-Market Fit
Measure and achieve product-market fit using the Sean Ellis survey, retention analysis, and qualitative signals
The Product-Market Fit (PMF) workflow helps you measure whether your product truly satisfies market demand. Using the Sean Ellis survey, retention analysis, and qualitative signals, this guide provides a systematic approach to understanding and achieving PMF.
Overview#
| Property | Value |
|---|---|
| Methods | 3 (Survey, Retention, Qualitative) |
| Tier | Free |
| Typical Duration | Ongoing measurement |
| Best For | Early-stage startups validating product |
Outcomes#
A successful PMF measurement process delivers:
- Clear PMF score using the Sean Ellis methodology
- Retention benchmarks for your category
- Qualitative understanding of user value
- Action plan to improve PMF if needed
- Confidence to scale (or not)
What is Product-Market Fit?#
Marc Andreessen's definition:
"Product-market fit means being in a good market with a product that can satisfy that market."
In practice, PMF feels like:
- Users actively seeking out your product
- Word-of-mouth growth happening naturally
- Retention curves that flatten (not decline to zero)
- Clear value proposition that resonates
NO PMF PMF
┌────────────────┐ ┌────────────────┐
│ Pushing users │ │ Users pulling │
│ to try product │ │ for product │
├────────────────┤ ├────────────────┤
│ High churn │ │ Strong │
│ flat retention │ │ retention │
├────────────────┤ ├────────────────┤
│ Unclear value │ │ Clear "aha" │
│ proposition │ │ moment │
├────────────────┤ ├────────────────┤
│ Struggling to │ │ Organic growth │
│ get traction │ │ happening │
└────────────────┘ └────────────────┘
Method 1: Sean Ellis Survey (PMF Score)#
The Question#
Ask users who have experienced your core value:
"How would you feel if you could no longer use [Product]?"
Answer options:
- Very disappointed
- Somewhat disappointed
- Not disappointed
The Benchmark#
40% or more answering "very disappointed" indicates PMF.
| PMF Score | Interpretation |
|---|---|
| 40%+ | Strong PMF - ready to scale |
| 25-40% | Getting close - iterate |
| <25% | Not yet PMF - major changes needed |
Implementation#
Database Schema:
1// prisma/schema.prisma
2model PMFSurvey {
3 id String @id @default(cuid())
4 userId String
5 response PMFResponse
6 feedback String?
7 userSegment String?
8 createdAt DateTime @default(now())
9
10 user User @relation(fields: [userId], references: [id])
11}
12
13enum PMFResponse {
14 VERY_DISAPPOINTED
15 SOMEWHAT_DISAPPOINTED
16 NOT_DISAPPOINTED
17}Survey Component:
1// components/PMFSurvey.tsx
2'use client';
3
4import { useState } from 'react';
5
6export function PMFSurvey({ onComplete }: { onComplete: () => void }) {
7 const [response, setResponse] = useState<string | null>(null);
8 const [feedback, setFeedback] = useState('');
9 const [step, setStep] = useState(1);
10
11 async function handleSubmit() {
12 await fetch('/api/surveys/pmf', {
13 method: 'POST',
14 headers: { 'Content-Type': 'application/json' },
15 body: JSON.stringify({ response, feedback }),
16 });
17 onComplete();
18 }
19
20 if (step === 1) {
21 return (
22 <div className="p-6 max-w-md mx-auto bg-card rounded-lg border">
23 <h3 className="text-lg font-semibold mb-4">
24 How would you feel if you could no longer use [Product]?
25 </h3>
26 <div className="space-y-3">
27 {[
28 { value: 'VERY_DISAPPOINTED', label: 'Very disappointed' },
29 { value: 'SOMEWHAT_DISAPPOINTED', label: 'Somewhat disappointed' },
30 { value: 'NOT_DISAPPOINTED', label: 'Not disappointed' },
31 ].map((option) => (
32 <label
33 key={option.value}
34 className={`flex items-center p-3 rounded border cursor-pointer ${
35 response === option.value
36 ? 'border-primary bg-primary/10'
37 : 'border-border hover:border-primary/50'
38 }`}
39 >
40 <input
41 type="radio"
42 name="pmf"
43 value={option.value}
44 checked={response === option.value}
45 onChange={(e) => setResponse(e.target.value)}
46 className="mr-3"
47 />
48 {option.label}
49 </label>
50 ))}
51 </div>
52 <button
53 onClick={() => setStep(2)}
54 disabled={!response}
55 className="mt-4 w-full py-2 bg-primary text-primary-foreground rounded disabled:opacity-50"
56 >
57 Continue
58 </button>
59 </div>
60 );
61 }
62
63 return (
64 <div className="p-6 max-w-md mx-auto bg-card rounded-lg border">
65 <h3 className="text-lg font-semibold mb-4">
66 {response === 'VERY_DISAPPOINTED'
67 ? 'What do you love most about [Product]?'
68 : 'What would make [Product] a must-have for you?'}
69 </h3>
70 <textarea
71 value={feedback}
72 onChange={(e) => setFeedback(e.target.value)}
73 placeholder="Your feedback..."
74 className="w-full p-3 border rounded min-h-[100px]"
75 />
76 <button
77 onClick={handleSubmit}
78 className="mt-4 w-full py-2 bg-primary text-primary-foreground rounded"
79 >
80 Submit
81 </button>
82 </div>
83 );
84}API Endpoint:
1// app/api/surveys/pmf/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { prisma } from '@/lib/prisma';
4import { auth } from '@/lib/auth';
5
6export async function POST(request: NextRequest) {
7 const session = await auth();
8 if (!session?.user) {
9 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
10 }
11
12 const { response, feedback } = await request.json();
13
14 await prisma.pMFSurvey.create({
15 data: {
16 userId: session.user.id,
17 response,
18 feedback,
19 },
20 });
21
22 return NextResponse.json({ success: true });
23}Calculating PMF Score:
1// lib/analytics/pmf.ts
2export async function getPMFScore() {
3 const responses = await prisma.pMFSurvey.groupBy({
4 by: ['response'],
5 _count: true,
6 });
7
8 const total = responses.reduce((sum, r) => sum + r._count, 0);
9
10 if (total === 0) {
11 return { score: 0, total: 0, hasPMF: false };
12 }
13
14 const veryDisappointed =
15 responses.find((r) => r.response === 'VERY_DISAPPOINTED')?._count || 0;
16
17 const score = Math.round((veryDisappointed / total) * 100);
18
19 return {
20 score,
21 total,
22 hasPMF: score >= 40,
23 breakdown: {
24 veryDisappointed,
25 somewhatDisappointed:
26 responses.find((r) => r.response === 'SOMEWHAT_DISAPPOINTED')?._count || 0,
27 notDisappointed:
28 responses.find((r) => r.response === 'NOT_DISAPPOINTED')?._count || 0,
29 },
30 };
31}When to Send the Survey#
Trigger after users experience core value:
- Completed onboarding
- Used key feature X times
- Been active for Y days
- Reached a milestone
Sample size:
- Minimum: 40 responses for statistical significance
- Ideal: 100+ responses
- Segment by user type for deeper insights
Method 2: Retention Analysis#
Retention Curves#
Retention is the clearest signal of PMF.
RETENTION CURVES
100% ┤
│ ╲
│ ╲ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ PMF (flattens)
50% ┤ ╲
│ ╲
│ ╲_ _ _ _ _ _ No PMF (keeps dropping)
0% ┤ ╲ _ _ _
└────┴────┴────┴────┴────
D1 D7 D14 D30 D60
Retention SQL Queries#
Daily Retention Cohort:
1-- Calculate D1, D7, D30 retention by signup cohort
2WITH cohorts AS (
3 SELECT
4 id AS user_id,
5 DATE_TRUNC('week', created_at) AS cohort_week
6 FROM users
7),
8user_activity AS (
9 SELECT
10 user_id,
11 DATE(created_at) AS activity_date
12 FROM events
13 WHERE event_type = 'session_start'
14 GROUP BY user_id, DATE(created_at)
15)
16SELECT
17 c.cohort_week,
18 COUNT(DISTINCT c.user_id) AS cohort_size,
19 COUNT(DISTINCT CASE
20 WHEN ua.activity_date = DATE(c.cohort_week) + INTERVAL '1 day'
21 THEN c.user_id
22 END)::FLOAT / COUNT(DISTINCT c.user_id) * 100 AS d1_retention,
23 COUNT(DISTINCT CASE
24 WHEN ua.activity_date = DATE(c.cohort_week) + INTERVAL '7 days'
25 THEN c.user_id
26 END)::FLOAT / COUNT(DISTINCT c.user_id) * 100 AS d7_retention,
27 COUNT(DISTINCT CASE
28 WHEN ua.activity_date = DATE(c.cohort_week) + INTERVAL '30 days'
29 THEN c.user_id
30 END)::FLOAT / COUNT(DISTINCT c.user_id) * 100 AS d30_retention
31FROM cohorts c
32LEFT JOIN user_activity ua ON c.user_id = ua.user_id
33GROUP BY c.cohort_week
34ORDER BY c.cohort_week;Retention Benchmarks by Category#
| Category | Good D1 | Good D7 | Good D30 |
|---|---|---|---|
| B2B SaaS | 50%+ | 30%+ | 20%+ |
| B2C App | 40%+ | 20%+ | 10%+ |
| E-commerce | 15%+ | 8%+ | 3%+ |
| Social | 50%+ | 25%+ | 15%+ |
| Gaming | 40%+ | 15%+ | 5%+ |
Improving Retention#
Analyze drop-off points:
1-- Find where users drop off
2SELECT
3 step,
4 COUNT(DISTINCT user_id) AS users,
5 COUNT(DISTINCT user_id)::FLOAT /
6 FIRST_VALUE(COUNT(DISTINCT user_id)) OVER (ORDER BY step) * 100
7 AS completion_rate
8FROM onboarding_events
9GROUP BY step
10ORDER BY step;Identify power users:
1-- What do retained users do differently?
2SELECT
3 e.event_type,
4 COUNT(DISTINCT CASE WHEN retained THEN e.user_id END) AS retained_users,
5 COUNT(DISTINCT CASE WHEN NOT retained THEN e.user_id END) AS churned_users
6FROM events e
7JOIN (
8 SELECT
9 user_id,
10 MAX(created_at) > NOW() - INTERVAL '30 days' AS retained
11 FROM events
12 GROUP BY user_id
13) r ON e.user_id = r.user_id
14WHERE e.created_at < e.user_created_at + INTERVAL '7 days'
15GROUP BY e.event_type
16ORDER BY retained_users DESC;Method 3: Qualitative Signals#
Leading Indicators of PMF#
| Signal | What to Look For |
|---|---|
| Word of mouth | Users referring without being asked |
| Organic growth | Signups without paid marketing |
| Usage depth | Users exploring beyond core features |
| Emotional connection | Users expressing love, not just utility |
| Pull vs. push | Users asking for features vs. you pushing adoption |
User Interview Questions#
For power users:
- What would you use if we didn't exist?
- What problem does [Product] solve for you?
- Tell me about the last time you used [Product]
- Who else should be using this?
For churned users:
- Why did you stop using [Product]?
- What would have kept you?
- What are you using instead?
Analyzing Qualitative Data#
Create a signal tracker:
1interface PMFSignal {
2 type: 'positive' | 'negative';
3 category: 'word_of_mouth' | 'usage' | 'feedback' | 'churn';
4 description: string;
5 source: string;
6 date: Date;
7}
8
9const signals: PMFSignal[] = [
10 {
11 type: 'positive',
12 category: 'word_of_mouth',
13 description: 'User shared product in industry Slack without asking',
14 source: 'Slack notification',
15 date: new Date('2024-01-15'),
16 },
17 // ... more signals
18];PMF Dashboard#
Key Metrics to Track#
1interface PMFDashboard {
2 // Sean Ellis Score
3 pmfScore: number;
4 pmfTrend: number; // change from last period
5
6 // Retention
7 d1Retention: number;
8 d7Retention: number;
9 d30Retention: number;
10 retentionTrend: number;
11
12 // Growth
13 organicSignupRate: number;
14 referralRate: number;
15 nps: number;
16
17 // Engagement
18 dau: number;
19 dauWauRatio: number; // stickiness
20 avgSessionsPerUser: number;
21}Segmented Analysis#
PMF often exists in specific segments first:
1-- PMF score by user segment
2SELECT
3 u.segment,
4 COUNT(*) AS total,
5 COUNT(CASE WHEN s.response = 'VERY_DISAPPOINTED' THEN 1 END)::FLOAT
6 / COUNT(*) * 100 AS pmf_score
7FROM pmf_surveys s
8JOIN users u ON s.user_id = u.id
9GROUP BY u.segment
10ORDER BY pmf_score DESC;Recommended Agents#
| Phase | Agent | Purpose |
|---|---|---|
| Survey | frontend-expert | Survey implementation |
| Analytics | analytics-expert | Retention analysis |
| Research | research-expert | User interviews |
| Strategy | strategy-expert | PMF improvement plan |
Action Plan Based on Score#
PMF Score < 25%: Major Pivot Needed#
Actions:
- Talk to 20+ users immediately
- Identify who (if anyone) is getting value
- Consider pivoting or major product changes
- Don't scale - you'll accelerate failure
PMF Score 25-40%: Getting Close#
Actions:
- Double down on what's working
- Identify friction points
- Improve onboarding to core value
- Test changes with specific segments
PMF Score 40%+: Ready to Scale#
Actions:
- Document what makes you successful
- Build repeatable acquisition channels
- Start scaling team and marketing
- Continue monitoring PMF as you grow
Deliverables#
| Deliverable | Description |
|---|---|
| PMF survey | Implemented and deployed |
| PMF dashboard | Score, retention, trends |
| User research | Interview findings |
| Segment analysis | PMF by user type |
| Action plan | Improvement roadmap |
Best Practices#
- Survey regularly - PMF can change over time
- Segment your data - PMF in one segment first
- Combine methods - Survey + retention + qualitative
- Don't fake it - Honest assessment is crucial
- Act on feedback - Use "somewhat disappointed" insights
- Be patient - PMF takes time to achieve
Common Pitfalls#
- Surveying too early - Wait for users to experience value
- Small sample size - Need 40+ responses minimum
- Ignoring segments - Overall score masks segment differences
- Premature scaling - Scaling without PMF accelerates failure
- Vanity metrics - Downloads and signups don't equal PMF
Related Workflows#
- Post-Launch - Iterate after launch
- Retention - Improve retention
- Metrics - Track KPIs
- Acquisition - Scale after PMF