《独立开发者的商业指南·收入与财务工程》8.3 runway 与最低现金水位

runway 是现金除以净烧钱率,最低现金水位是不可取消支出加已发生税费。本节给出三案例的水位公式、零收入 runway、冲击后的 runway,以及绿黄红三级触发线;Nord Studio 的 25,740 现金低于自身 38,580 的水位,是全书最直接的一次红灯。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

13 周表回答「哪一周最紧」,情景模拟回答「如果发生会怎样」。两个问题都指向同一个决策:现在这笔现金还能撑多久,撑到哪一天必须动作。回答这个问题的两个数字是 runway 和最低现金水位。runway 是一个时间,水位是一条线;时间告诉你还剩多少个月,线告诉你低于多少就不能再等。

这两个数字最容易被误用。runway 被当成一个越大越好的指标,水位被当成一个固定金额。实际上 runway 取决于你假设收入怎么变,水位取决于你承诺了哪些不可取消的支出。同一笔现金,按不同的假设可以算出 2.6 个月,也可以算出 65 个月;差别不在现金,在假设。

runway 的三个口径

runway 的定义很简单:现金除以月净烧钱率。难的是分子和分母各取什么。

口径分子分母回答的问题适用场景
零收入 runway可用现金每月不可取消支出如果明天收入归零,还能撑多久平台依赖、单一客户
当前 runway可用现金当前月净烧钱按现在的速度还能撑多久稳定亏损期
情景 runway情景后期末现金情景后的月净烧钱冲击发生后还能撑多久压力测试之后

零收入 runway 是最保守的口径,也是独立开发者最该先算的一个。它的分母只放不可取消的支出:工资、房租、云服务、工具订阅、已经发生的税。营销、外包、新项目都可以停,所以不进分母。LedgerLite 的月固定支出是 11,000,其中创始人薪酬 5,000、运营费用 6,000;零收入 runway 是 95,000 ÷ 11,000 = 8.6 个月。如果把已发生的所得税 11,941 先留出来,可用现金是 83,059,runway 是 7.6 个月。

当前 runway 用月净烧钱做分母。LedgerLite 的月净现金是 4,858:MRR 20,020 减变动成本 3,167、运营费用 6,000、薪酬 5,000,再减月均税负 995。它是正数,所以当前 runway 不存在——不烧钱的公司没有 runway,只有缓冲。这一点常被误写成「runway 无限」,两者不是一回事:没有 runway 意味着时间不是约束,缓冲才是。

情景 runway 只在冲击之后使用。Nord Studio 招聘第三人后,13 周现金从 25,740 降到 16,218,净减少 9,522,折月净烧钱 4,395,情景 runway 是 16,218 ÷ 4,395 = 3.7 个月。同一个数字,零收入口径下是 25,740 ÷ 10,000 = 2.6 个月,招聘口径下是 3.7 个月——招聘反而让 runway 变长,因为它把现金分母换成了更小的期末数,同时把烧钱率也放大了。这说明 runway 只有在同一口径下才能比较。

Tidecast 的 runway 需要单独说明。它的月固定支出是 17,000 ÷ 12 = 1,417,现金 92,090,零收入 runway 是 65.0 个月。这个数字没有意义,因为案例事实表没有给出创始人的生活支出;1,417 只覆盖营销、服务器和工具,不覆盖一个人吃饭住房。买断制生意的 runway 必须把生活支出加进分母,否则会得到一个「可以撑五年」的假象。案例没有给这个数,本节就不编一个,只标注它是上限。

最低现金水位:不可取消支出加已发生税费

水位是一条线:低于它,公司就不能再按原计划花钱。计算分三层。

第一层是月固定支出:把年固定成本除以 12。第二层是覆盖月数:收入越不可预测,覆盖月数越长。第三层是已发生的一次性义务:已经产生的税、已经承诺的押金、已经签署的最低采购。三者相加就是水位。

月固定支出 = 年固定成本 ÷ 12
最低现金水位 = 月固定支出 × 覆盖月数 + 已发生的一次性义务
案例年固定成本月固定支出覆盖月数一次性义务水位
LedgerLite$132,000$11,0003所得税 $11,941$44,941
Tidecast$17,000$1,41712所得税 $29,081$46,081
Nord Studio$120,000$10,0003所得税 $8,580$38,580

覆盖月数不是拍脑袋,它跟收入的可预测性成反比。LedgerLite 有 260 家客户、月流失 1.5%、年付占 30%,收入的可预测性最高,3 个月够用。Tidecast 的收入是脉冲式的,Q4 占全年销量 38%,平台单一,一次封禁就可能让收入归零,所以用 12 个月。Nord Studio 夹在中间:咨询续约率 70%、前三大客户占 48%,项目制回款,3 个月是最低要求。

Nord 的 3 个月还有一个更宽松的算法:把外包 18,000 当成随项目变动的成本,只把两人工资 96,000 算固定,月固定支出是 8,000,水位是 8,000 × 3 + 8,580 = 32,580。两种口径都高于它账上的 25,740。也就是说,无论怎么算,Nord 的现金都低于自己的水位线。

这正是水位比 runway 更早报警的地方。Nord 的当前月净现金是正的:月收入 12,860 减直接成本 2,000、工资 8,000、月均税负 715,等于 2,145。它不烧钱,runway 按当前口径不存在;但它的现金 25,740 低于水位 38,580,缺口 12,840。一个不烧钱的公司,也可能处在红灯状态。

补回缺口需要时间。按每月 2,145 的净现金,12,840 ÷ 2,145 = 6.0 个月。这六个月里不能有招聘、不能有设备采购、不能有坏账;任何一项都会把 6 个月拉长。如果把覆盖月数从 3 提到 6,水位变成 68,580,缺口 42,840,补回需要 20.0 个月——这就是覆盖月数选择的代价。

三案例的对照:水位、runway 与触发线

把水位、现金和 runway 放在一起,三案例的状态就清楚了。

案例现金水位差额1.5 倍水位零收入 runway判定
LedgerLite$95,000$44,941+$50,059$67,4128.6 个月绿
Tidecast$92,090$46,081+$46,009$69,12265.0 个月(不含生活支出)绿
Nord Studio$25,740$38,580−$12,840$57,8702.6 个月红

LedgerLite 的现金是水位的 2.1 倍,缓冲 50,059;即使前五大客户全部流失,现金 81,985 仍然高于 1.5 倍水位 67,412。它的 runway 在零收入口径下是 8.6 个月,足够完成一次转型或一次出售谈判。

Tidecast 的现金是水位的 2.0 倍,缓冲 46,009,判定为绿。但这个绿要打折扣:65.0 个月的 runway 不含生活支出,也不含续作预算 120,000。如果启动续作,92,090 的现金连预算都不够,缺口 27,910,必须靠 IP 授权的保底 60,000/年补。水位只覆盖「停下来不死」,不覆盖「继续做下一款」。

Nord Studio 是唯一红灯:现金 25,740,水位 38,580,缺口 12,840。它的 runway 在零收入口径下只有 2.6 个月,因为月固定支出 10,000 几乎等于全部收入。红灯不意味着马上关门,它意味着不能再按原计划花钱:招聘要停、外包要压、税要提前留。

触发线分三级,判定和动作一一对应。

级别判定条件含义动作
绿现金 ≥ 1.5 × 水位,且 runway ≥ 12 个月有时间做选择维持计划,按季度复核
黄水位 ≤ 现金 < 1.5 × 水位,或 6 ≤ runway < 12 个月缓冲变薄冻结招聘与非必要支出,加快回款
红现金 < 水位,或 runway < 6 个月时间不够启动收缩:延期付款、降薪、融资或退出

红灯的动作不是「想办法多卖」,因为销售有周期,而现金没有。红灯时能立刻改变现金的只有支出端和回款端:推迟非承诺的采购、和供应商谈账期、催逾期应收、把年付折扣拿出来换现金。销售动作要放到黄灯做,红灯做已经来不及。

用这张表回看情景模拟的结论:LedgerLite 在前五大流失后仍是绿,因为它的水位低、缓冲厚;Tidecast 在封禁后现金 120,291 仍高于水位,但它的收入已经不足以支撑续作,判定要从「现金」切换到「业务」;Nord 在招聘后现金 16,218,远低于水位,runway 3.7 个月,直接进红。

预警的具体阈值和每周检查节奏属于操作层,见 runway 与创始人薪酬 和 现金预警 。如果红灯持续且没有融资或出售路径,退出决策属于卷三《资本与退出》中「有序关停」一节的内容。

把水位写成代码

下面的 runway.ts 把三层水位、runway、补回缺口所需月数和三级判定固化成函数。金额用整数美分,覆盖月数限制在 1 到 24,避免出现「用 60 个月覆盖」这种把水位算成天文数字的用法。

// file: src/cash/runway.ts
// tier: L1
// verify: node --test test/cash/runway.test.ts

import { type Cents } from "../core/money.ts";
import { type Result, fail, ok } from "../core/result.ts";

/** Amber above the floor multiplier, red below the floor itself. */
export const AMBER_MULTIPLE = 1.5;
/** A runway shorter than half a year is treated as a warning. */
export const RUNWAY_WARNING_MONTHS = 6;

export type AlertLevel = "green" | "amber" | "red";

export type FloorInput = {
  annualFixedCosts: Cents;
  monthsOfCover: number;
  oneOffObligations: Cents;
};

export type Floor = {
  monthlyFixedCosts: Cents;
  monthsOfCover: number;
  oneOffObligations: Cents;
  floor: Cents;
};

/**
 * The floor is what cannot be cancelled in the cover window: fixed payroll,
 * tools, hosting and the tax already incurred. Months of cover should track
 * revenue predictability, not optimism: a pulsing one-off business needs more
 * cover than a subscription book with 1.5% monthly churn.
 */
export function cashFloor(input: FloorInput): Result<Floor> {
  if (!Number.isInteger(input.annualFixedCosts) || input.annualFixedCosts < 0) {
    return fail("annual fixed costs must be a non-negative integer");
  }
  if (
    !Number.isInteger(input.monthsOfCover) ||
    input.monthsOfCover <= 0 ||
    input.monthsOfCover > 24
  ) {
    return fail("months of cover must be a whole number between 1 and 24");
  }
  if (
    !Number.isInteger(input.oneOffObligations) ||
    input.oneOffObligations < 0
  ) {
    return fail("one-off obligations must be a non-negative integer");
  }
  const monthlyFixedCosts = Math.round(input.annualFixedCosts / 12);
  return ok({
    monthlyFixedCosts,
    monthsOfCover: input.monthsOfCover,
    oneOffObligations: input.oneOffObligations,
    floor:
      monthlyFixedCosts * input.monthsOfCover + input.oneOffObligations,
  });
}

/**
 * Months until cash reaches zero at a constant net burn. A non-positive burn
 * returns null: the business is not consuming cash and runway is undefined,
 * which is different from an infinite runway.
 */
export function runwayMonths(
  cash: Cents,
  monthlyNetBurn: Cents,
): Result<number | null> {
  if (!Number.isInteger(cash)) {
    return fail("cash must be an integer number of cents");
  }
  if (!Number.isInteger(monthlyNetBurn)) {
    return fail("monthly net burn must be an integer number of cents");
  }
  if (monthlyNetBurn <= 0) {
    return ok(null);
  }
  if (cash <= 0) {
    return ok(0);
  }
  return ok(Math.round((cash / monthlyNetBurn) * 10) / 10);
}

/**
 * Months of surplus needed to climb from today's cash to the floor. Returns 0
 * when the floor is already met, and null when the surplus is not positive.
 */
export function monthsToFloor(
  cash: Cents,
  floor: Cents,
  monthlySurplus: Cents,
): Result<number | null> {
  if (!Number.isInteger(cash) || !Number.isInteger(floor)) {
    return fail("cash and floor must be integers");
  }
  if (!Number.isInteger(monthlySurplus)) {
    return fail("monthly surplus must be an integer");
  }
  if (cash >= floor) {
    return ok(0);
  }
  if (monthlySurplus <= 0) {
    return ok(null);
  }
  return ok(Math.round(((floor - cash) / monthlySurplus) * 10) / 10);
}

/**
 * Three-level trigger. The floor is the red line; 1.5x the floor is amber.
 * Both are compared on rounded cents so the boundary is reproducible.
 */
export function classifyCash(
  cash: Cents,
  floor: Cents,
  amberMultiple: number = AMBER_MULTIPLE,
): Result<AlertLevel> {
  if (!Number.isInteger(cash) || !Number.isInteger(floor)) {
    return fail("cash and floor must be integers");
  }
  if (!Number.isFinite(amberMultiple) || amberMultiple < 1) {
    return fail("amber multiple must be at least 1");
  }
  if (cash < floor) {
    return ok("red");
  }
  if (cash < Math.round(floor * amberMultiple)) {
    return ok("amber");
  }
  return ok("green");
}

/** Red when either the floor test or the runway test fails. */
export function alertWithRunway(
  cash: Cents,
  floor: Cents,
  monthlyNetBurn: Cents,
): Result<AlertLevel> {
  const level = classifyCash(cash, floor);
  if (!level.ok) {
    return level;
  }
  const runway = runwayMonths(cash, monthlyNetBurn);
  if (!runway.ok) {
    return runway;
  }
  if (level.value === "red") {
    return ok("red");
  }
  if (runway.value !== null && runway.value < RUNWAY_WARNING_MONTHS) {
    return ok("red");
  }
  return ok(level.value);
}

代码里有三个判断值得说明。第一,runwayMonths 在净烧钱率为负或零时返回 null,而不是返回一个很大的数;null 表示「时间不是约束」,大数会诱导错误比较。第二,monthsToFloor 在现金已经高于水位时返回 0,在净现金为负时返回 null——后者意味着永远补不回来,只能靠降支出或融资。第三,alertWithRunway 把水位和 runway 两个条件取更严的一个:水位显示黄,但 runway 只有 3 个月,仍然判红。

代入三案例:LedgerLite 的水位是 4,494,100 美分,现金 9,500,000,判定绿;Tidecast 水位 4,608,104,现金 9,209,000,判定绿;Nord 水位 3,858,000,现金 2,574,000,判定红。Nord 的补回月数是 (3,858,000 − 2,574,000) ÷ 214,500 = 6.0 个月。

水位不是一次算完就固定的。收入结构变了,覆盖月数要改;税负变了,一次性义务要改;招聘了,固定成本要改。唯一不变的是公式的形状:不可取消支出乘以覆盖月数,加上已经发生但还没付的钱。

小结

  • runway 有三个口径:零收入、当前、情景;只有同一口径下的 runway 才能互相比较,不烧钱的公司没有 runway,只有缓冲。
  • 水位 = 月固定支出 × 覆盖月数 + 已发生的一次性义务;覆盖月数与收入可预测性成反比,LedgerLite 用 3 个月,Tidecast 用 12 个月,Nord 用 3 个月。
  • LedgerLite 现金 95,000、水位 44,941、缓冲 50,059,零收入 runway 8.6 个月;即使前五大客户全部流失,仍在 1.5 倍水位之上。
  • Tidecast 现金 92,090、水位 46,081,零收入 runway 65.0 个月,但这个数不含生活支出和 120,000 的续作预算,只能当上限。
  • Nord Studio 现金 25,740 低于水位 38,580,缺口 12,840,零收入 runway 2.6 个月,是全书最直接的一次红灯;按月净现金 2,145 补回需要 6.0 个月。
  • 三级触发线:现金低于水位或 runway 低于 6 个月判红;红灯只改支出端和回款端,销售动作要放在黄灯做。

阅读导航:上一节:8.2 情景模拟:断供、退款潮、平台封禁 · 下一节:9.1 销售税/VAT/GST 的注册与代收 。

常见错误与边界

  1. 把 runway 当越大越好。 65.0 个月的 Tidecast 不是健康,是收入脉冲之后没有下一步;runway 长也可能意味着业务停滞。
  2. 用同一个分母算所有案例。 LedgerLite 的 11,000 含创始人薪酬,Tidecast 的 1,417 不含生活支出;分母不同,runway 不可比。
  3. 忘记已发生的税。 LedgerLite 的 11,941、Nord 的 8,580 是已经产生的义务,不因收入下降而消失;水位必须加上它们。
  4. 把水位当固定金额。 招聘、涨薪、签最低采购都会抬高水位;水位要随承诺更新,不是年初算一次。
  5. 覆盖月数拍脑袋。 3 个月和 6 个月对 Nord 意味着 38,580 和 68,580 两条线,补回时间从 6.0 个月变成 20.0 个月。
  6. 把不烧钱当成安全。 Nord 的月净现金是 +2,145,但现金低于水位,仍然是红灯;现金流为正不等于有缓冲。
  7. runway 用期末现金除以期初烧钱率。 分子分母要来自同一个情景;混用会把冲击后的 runway 算高。
  8. 忽略一次性收入。 Tidecast 的 Q4 脉冲会在 1 月到账,用它算月均收入会高估全年;runway 要用可持续收入。
  9. 红灯时才做销售。 销售有 30 到 90 天的周期,红灯时能动的只有支出和回款;销售动作属于黄灯。
  10. 把水位线当成破产线。 水位是「停止按原计划花钱」的线,不是「关门」的线;低于水位还有 2.6 到 8.6 个月的动作时间。

本节的边界是「单一口径、静态承诺」的水位模型:它假设固定成本在覆盖窗口内不变,不处理季节性收入、融资到账和坏账。它回答「现在该不该踩刹车」,不回答「刹车之后往哪开」。真正的连续性计划要把供应商、平台、客户和融资路径一起排,卷二《合同、应收与风险》第 12 章会展开业务连续性计划。下一章进入跨境税务结构,那里的一次性义务会比工资更集中、更不可协商。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查