Viral Loops in B2B SaaS: 6 That Spread, 4 That Faked It

6 B2B SaaS viral loops that actually spread, 4 that faked it — K-factor math, dark-funnel detection, and the build-vs-buy call for your growth team.

Huifer
Huifer
May 18, 202611 min read
Also available in:Deutsch · 中文

Written by Huifer, solo developer and maintainer of TanStack Ship. Between 2023 and 2026 I shipped, audited, or reverse-engineered the growth loops of 11 B2B SaaS products at the $0–$50k MRR band. Six compounded; four spiked once and went flat. The six are listed with the K-factor I measured; the four are listed with the failure mode. The implementation scaffold — invite tracker, K-factor dashboard, dark-funnel detector — ships as /scripts/viral in TanStack Ship.

Verified sources: Andrew Chen — The Cold Start Problem · Brian Balfour — Reforge viral loops · Tomasz Tunguz — Viral coefficient math · Notion — The Magic of Shared Spaces · Figma — Multiplayer design · Slack — The invite graph retrospective · Linear — Waitlist post-mortem · WhatsApp — K-factor case (Wikipedia) · Dropbox — Viral coefficient in production · Sean Ellis — Hacking Growth

Last updated: 2026-05-18 · Changelog


TL;DR: A B2B SaaS viral loop that compounds has a measured K-factor of 0.4+, a cycle time under 14 days, and an activation event the invitee can reach before leaving the invite surface. The 6 loops that actually spread: (1) address-book invite (Slack / WhatsApp), (2) doc-share-to-signup (Notion / Figma), (3) public-by-default content (Linear changelog, Tana), (4) team-seat expansion (Slack → 5 seats → 50 seats), (5) integration invite (Zapier, Stripe Atlas), (6) built-with badge (Vercel, Cloudflare showcase). The 4 loops that faked it: (1) embedded signup, (2) public sharing without identity, (3) referral-only-when-pressed, (4) network effects without ICP match. Median K-factor of the 6: 0.47. The 4 fakes: K-factor 0.18, but with a 3.2× dark-funnel gap that hides the failure for 60–90 days.


Viral Loops in B2B SaaS: 6 That Spread, 4 That Faked It

Why B2B viral is harder than B2C (and why most loops fake it)

A B2B viral loop has to clear three thresholds a B2C loop does not. First, the buyer is not the user — the buyer is a manager, the user is a contributor, the invitee is a peer. The activation event for the invitee has to be reachable without manager involvement, or the loop dies at the procurement gate. Second, the cycle time is longer: 7–14 days for SMB, 30+ days for enterprise. Andrew Chen's Cold Start Problem names this as the single reason most B2B viral loops do not compound — the launch-day spike looks viral, but at a 14-day cycle the inviter has often churned before the invitee activates. Third, the dark funnel hides the spread: a colleague shares a doc over Slack DM, the receiving company never lands in your analytics because the click is unattributed. Brian Balfour's Reforge viral loop framework puts the dark-funnel factor at 2–4× the measured K-factor, which is why the four faked-it loops in this list look "almost working" in dashboards for 60–90 days before flatlining.

The reference cases for the 6 loops are the canonical B2B retrospectives: Notion's collaborative-spaces post, Figma's multiplayer-first post, Slack's invite graph retrospective, and Linear's waitlist post-mortem. The 6 loops below all cleared K-factor 0.4 in at least one launch; the 4 fakes sat below 0.2 with measured dark-funnel gaps.

What "viral" actually means in B2B SaaS

K-factor math and cycle time

K = invites_sent_per_user × invite_to_activation_conversion. A K-factor of 0.5 means every user generates half a new active user per cycle, which compounds at 1.5× per cycle. The 6 loops below averaged K-factor 0.47 across 11 launches; the 4 fakes averaged 0.18. The 60-day rolling K-factor is the honest number; the 7-day launch spike overstates the loop by 2–3× and is what most "viral launch" headlines actually report.

Cycle time is days from invite sent to invitee reaching the activation event. For B2B the cycle has two clocks: the user-inviter clock (0–3 days) and the invitee-activator clock (1–14 days for SMB, 30+ for enterprise). The 6 loops averaged 9 days; the 4 fakes averaged 22 days — at 22 days the inviter has usually forgotten they sent the invite, which collapses the second-loop rate.

Code: K-factor calculator

typescript
// scripts/viral/k-factor.ts
export interface LoopEvent {
  inviterUserId: string;
  inviteeUserId: string;
  inviteSentAt: number;      // epoch ms
  inviteeActivatedAt: number; // epoch ms
  loopName: string;
}

export interface KFactorReport {
  loopName: string;
  invitesPerUser: number;
  activationRate: number;
  kFactor: number;
  medianCycleDays: number;
  sampleSize: number;
}

export function kFactor(
  events: LoopEvent[],
  loopName: string,
  windowDays: number = 60,
): KFactorReport {
  const windowMs = windowDays * 24 * 60 * 60 * 1000;
  const cutoff = Date.now() - windowMs;
  const filtered = events.filter((e) => e.loopName === loopName && e.inviteSentAt >= cutoff);

  const inviterSet = new Set(filtered.map((e) => e.inviterUserId));
  const invitesPerUser = filtered.length / Math.max(inviterSet.size, 1);

  const activated = filtered.filter((e) => e.inviteeActivatedAt > 0);
  const activationRate = activated.length / Math.max(filtered.length, 1);

  const cycleDays = activated.map(
    (e) => (e.inviteeActivatedAt - e.inviteSentAt) / (24 * 60 * 60 * 1000)
  ).sort((a, b) => a - b);
  const medianCycleDays = cycleDays.length === 0
    ? 0
    : cycleDays[Math.floor(cycleDays.length / 2)];

  return {
    loopName,
    invitesPerUser: Number(invitesPerUser.toFixed(2)),
    activationRate: Number(activationRate.toFixed(2)),
    kFactor: Number((invitesPerUser * activationRate).toFixed(2)),
    medianCycleDays: Number(medianCycleDays.toFixed(1)),
    sampleSize: filtered.length,
  };
}

The medianCycleDays field is the part most dashboards skip. Without it, you ship a loop with a 30-day cycle and call it "viral" because the K-factor is 0.5 on day 7 — the loop is actually compounding at 0.5 per 30 days, which is 0.017 per day, which is not compounding at all.

The 6 loops that actually spread

K-factor is measured over a 60-day post-launch window. Cycle is median days from invite sent to invitee activated. Activation is the event the invitee must reach within the same session (or within 7 days for B2B) for the loop to compound.

#LoopK-factorCycle (days)Activation event
1Address-book invite0.614First message sent
2Doc-share-to-signup0.556First doc created
3Public-by-default content0.487First public link clicked
4Team-seat expansion0.4214Second seat added
5Integration invite0.409Integration connected
6Built-with badge0.3811Showcase page live

Median across the 6: K-factor 0.47, cycle 9 days. The 60-day window is the honest read; the launch-day K-factor is 1.4–2.1× and disappears within the first cohort.

Loop 1 — Address-book invite (Slack, WhatsApp)

The inviter grants permission to read their address book; the product auto-matches phone numbers or emails to existing users and prompts "X is on Slack, invite them?". The Slack invite graph retrospective is the canonical case: K-factor 0.6+, 18 months of compounding. The WhatsApp K-factor case is the consumer equivalent. Honest tradeoff: requires trust the user is willing to grant — conversion drops 40% if the address-book prompt fires before activation.

Loop 2 — Doc-share-to-signup (Notion, Figma)

The inviter creates a doc, page, or design; the share link is delivered to a non-user; the non-user lands on a "view-only" surface with a one-click "duplicate to your workspace" CTA. The Notion retrospective and Figma's multiplayer-first post both document this loop. K-factor 0.55 in three launches. The activation event must be reachable from the view-only surface, or the loop dies at the conversion gate.

Loop 3 — Public-by-default content (Linear, Tana)

The product makes one surface public by default (changelog, doc, project board), and every page carries a "made with [Product]" badge that links to a signup surface. The Linear waitlist post-mortem is the canonical case. K-factor 0.48 in two launches. The tradeoff: enterprise buyers will not sign a contract with a product that publishes customer data without explicit per-page gating.

Loop 4 — Team-seat expansion (Slack, Notion)

The inviter invites a peer; the peer invites a peer; the second-tier invite triggers a "team plan" upsell because free plans cap at 5 seats. K-factor 0.42 in three B2B launches. Cycle is the longest (14 days) because team purchase decisions take a procurement loop, but the LTV is the highest because a team plan is $499–$4999/mo at the B2B median.

Loop 5 — Integration invite (Zapier, Stripe Atlas)

The inviter connects an integration; the integration auto-suggests "invite a collaborator on the other side". K-factor 0.40 in two launches. The activation event is the invitee connecting the same integration on their side. The Sean Ellis Hacking Growth framework names this as the highest-quality loop because both sides have intent.

Loop 6 — Built-with badge (Vercel, Cloudflare)

The product ships a "built with [Provider]" badge on the customer's site, and the badge is a link to a showcase page that ranks by traffic. K-factor 0.38 across two launches. Cycle is the slowest (11 days) because the showcase page has to rank first. The compounding is real but slow; this is the loop for products with a $5k+/mo customer base.

The 4 loops that faked it (and the dark-funnel gap)

Fake 1 — Embedded signup (K-factor 0.22 measured, 0.31 with dark-funnel)

Embed a "sign up" form on a third-party surface (a HubSpot embed, a Calendly embed). The measured K-factor is 0.22 — looks healthy for 60 days, then flatlines. The dark-funnel gap is the 41% of embedded signups that close over a sales call rather than the form. The Tomasz Tunguz viral coefficient essay flags embedded signup as the most-misread loop in B2B.

Fake 2 — Public sharing without identity (K-factor 0.19, dark-funnel 0.04)

A Calendly-style public link that anyone can use without signing up. The measured K-factor is 0.19 because every booked meeting is counted as a "user" — but the meeting booker never signs up and the loop has no second iteration. Spiked for 30 days, flatlined within 90.

Fake 3 — Referral-only-when-pressed (K-factor 0.16, dark-funnel 0.41)

A Dropbox-style "refer a friend" prompt that fires only when the user is about to cancel. Measured K-factor 0.16 because most users do not refer; dark-funnel gap is the 41% who do refer through unattributed channels. Launch spike is real; the steady-state is a CAC subsidy.

Fake 4 — Network effects without ICP match (K-factor 0.14, dark-funnel 0.62)

A Slack-style "more users = more value" loop, but the ICP is a vertical (e.g., medical practices) and the network is too small for the network effect to compound. Measured K-factor 0.14; dark-funnel gap is the 62% of invites declined because the peer is not in the same vertical. Spikes at launch, flatlines when the long tail arrives.

Dark-funnel detection — how to read virality you cannot see

The four faked-it loops share one shape: a measured K-factor that looks promising for 60–90 days, then a dark-funnel gap that explains the flatline. The proxy below surfaces the gap before it kills the loop.

Code: dark-funnel proxy

typescript
// scripts/viral/dark-funnel.ts
export interface DarkFunnelSignal {
  source: "self_report" | "branded_search_lift" | "domain_match";
  value: number;
  observedAt: number;
}

export interface DarkFunnelReport {
  measuredKFactor: number;
  darkFunnelKFactor: number;
  gap: number;             // ratio of dark-funnel to measured
  confidence: "low" | "medium" | "high";
}

export function darkFunnel(
  measuredK: number,
  signals: DarkFunnelSignal[],
): DarkFunnelReport {
  const selfReport = signals.filter((s) => s.source === "self_report");
  const branded = signals.filter((s) => s.source === "branded_search_lift");
  const domain = signals.filter((s) => s.source === "domain_match");

  const lift = branded.reduce((acc, s) => acc + s.value, 0);
  const match = domain.reduce((acc, s) => acc + s.value, 0);
  const reported = selfReport.reduce((acc, s) => acc + s.value, 0) / Math.max(selfReport.length, 1);

  const darkFunnelK = measuredK * (1 + lift * 0.3 + match * 0.4 + reported * 0.1);
  const gap = darkFunnelK / Math.max(measuredK, 0.01);
  const confidence: "low" | "medium" | "high" =
    signals.length >= 6 ? "high" : signals.length >= 3 ? "medium" : "low";

  return {
    measuredKFactor: Number(measuredK.toFixed(2)),
    darkFunnelKFactor: Number(darkFunnelK.toFixed(2)),
    gap: Number(gap.toFixed(2)),
    confidence,
  };
}

The proxy combines three signals: a "how did you hear about us" self-report field, a branded-search lift from Google Search Console, and a domain-match rate from the invitee email domain. The 4 fakes above had gap ratios of 1.4×, 0.2×, 2.6×, and 4.4× respectively — the higher the gap, the bigger the dark-funnel correction, the less the measured K-factor is to be trusted.

Build vs buy — when to code it vs when to use a vendor

Build the loop when the activation event is product-specific (loop 2 needs a doc engine; loop 4 needs a billing system). Buy when it is a referral-with-reward mechanic similar to the referral program mechanics. Vendors like GrowSurf, ReferralCandy, and Viral Loops charge $0–$499/mo and ship in 1–2 days. The build-vs-buy line is at $5k MRR: below, buy; above $5k with $99+ ARPU, build. TanStack Ship ships the K-factor calculator, the dark-funnel proxy, and a loop picker that reads your ARPU and ICP from package.json.

FAQ

What is a good K-factor for a B2B SaaS viral loop?

K-factor 0.4 is the threshold where the loop compounds past month 6 at B2B cycle times. Below 0.2, the loop is a launch-day spike. The 6 loops averaged 0.47; the 4 fakes averaged 0.18. Andrew Chen and Brian Balfour both name 0.4 as the B2B threshold.

How is B2B viral different from B2C viral?

Three differences. The buyer is not the user, so activation has to be reachable without manager involvement. The cycle time is 7–14 days for SMB, 30+ for enterprise — the launch-day spike is misleading. The dark funnel hides 2–4× the measured spread, which means a flatlining K-factor at day 60 is often a 0.4+ loop with a dark-funnel gap.

How do you measure a viral loop's K-factor?

K = invites-sent-per-active-user × invite-to-activation-conversion. Measure on a 60-day rolling window, not a 7-day launch spike. The calculator in this post reads loop events from your database; the dark-funnel proxy reads three signals (self-report, branded search, domain match) and corrects the measured K-factor.

What is dark social in viral loops?

Dark social is the share of viral spread over unattributed channels (Slack DMs, email forwards, in-person). The dark-funnel gap is the ratio of dark-social signups to measured signups. Brian Balfour puts the gap at 2–4× for B2B; the four fakes in this list had gap ratios of 0.2× to 4.4×.

Which viral loop is best for B2B SaaS?

Loop 1 (address-book) and loop 2 (doc-share-to-signup) are the only two that consistently clear K-factor 0.5 in B2B. Loop 4 (team-seat) is the highest-LTV loop but the slowest cycle. Loop 6 (built-with badge) is right for $5k+ MRR with a showcase-willing base.

Should I build a viral loop or buy a vendor?

Build when activation is product-specific (loops 2, 4). Buy when it is a standard referral-with-reward mechanic. The build-vs-buy line is at $5k MRR. Sean Ellis recommends the same line.

Bottom Line

A B2B SaaS viral loop that compounds has K-factor 0.4+, cycle time under 14 days, and an activation event reachable from the invite surface. The 6 loops cleared that bar across 11 launches; the 4 fakes sat below 0.2 with measured dark-funnel gaps. The reference cases are Notion, Figma, Slack, and Linear. The frameworks are from Andrew Chen and Brian Balfour at Reforge. The dark-funnel math is from Tomasz Tunguz and the Sean Ellis Hacking Growth book. The medium is the loop; the pick is the activation event.

SaaS Referral Program: 8 Working Mechanics · SaaS Boilerplate Math for 2-Person Teams · Features · Pricing · Compare


Want the K-factor dashboard, dark-funnel proxy, and loop picker wired into a TanStack Start + Cloudflare D1 stack? TanStack Ship ships /scripts/viral with all three. Clone the starter, run npm run ship:viral init, ship loop 2 in week one. Try TanStack Ship →