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#

PropertyValue
Methods3 (Survey, Retention, Qualitative)
TierFree
Typical DurationOngoing measurement
Best ForEarly-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:

  1. Very disappointed
  2. Somewhat disappointed
  3. Not disappointed

The Benchmark#

40% or more answering "very disappointed" indicates PMF.

PMF ScoreInterpretation
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#

CategoryGood D1Good D7Good D30
B2B SaaS50%+30%+20%+
B2C App40%+20%+10%+
E-commerce15%+8%+3%+
Social50%+25%+15%+
Gaming40%+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#

SignalWhat to Look For
Word of mouthUsers referring without being asked
Organic growthSignups without paid marketing
Usage depthUsers exploring beyond core features
Emotional connectionUsers expressing love, not just utility
Pull vs. pushUsers asking for features vs. you pushing adoption

User Interview Questions#

For power users:

  1. What would you use if we didn't exist?
  2. What problem does [Product] solve for you?
  3. Tell me about the last time you used [Product]
  4. Who else should be using this?

For churned users:

  1. Why did you stop using [Product]?
  2. What would have kept you?
  3. 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;
PhaseAgentPurpose
Surveyfrontend-expertSurvey implementation
Analyticsanalytics-expertRetention analysis
Researchresearch-expertUser interviews
Strategystrategy-expertPMF improvement plan

Action Plan Based on Score#

PMF Score < 25%: Major Pivot Needed#

Actions:

  1. Talk to 20+ users immediately
  2. Identify who (if anyone) is getting value
  3. Consider pivoting or major product changes
  4. Don't scale - you'll accelerate failure

PMF Score 25-40%: Getting Close#

Actions:

  1. Double down on what's working
  2. Identify friction points
  3. Improve onboarding to core value
  4. Test changes with specific segments

PMF Score 40%+: Ready to Scale#

Actions:

  1. Document what makes you successful
  2. Build repeatable acquisition channels
  3. Start scaling team and marketing
  4. Continue monitoring PMF as you grow

Deliverables#

DeliverableDescription
PMF surveyImplemented and deployed
PMF dashboardScore, retention, trends
User researchInterview findings
Segment analysisPMF by user type
Action planImprovement roadmap

Best Practices#

  1. Survey regularly - PMF can change over time
  2. Segment your data - PMF in one segment first
  3. Combine methods - Survey + retention + qualitative
  4. Don't fake it - Honest assessment is crucial
  5. Act on feedback - Use "somewhat disappointed" insights
  6. 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