← Back to Blog
Quantitative MarketingGamificationRetentionFinancial Engineering

Where Streaks Break: The Hazard Rate Behind Daily Engagement Mechanics

Day 1 saves the most users. It also costs 12.7 times more per user saved than day 29. Both are true, and your budget decides which one matters. The hazard-rate model for streaks, with the Python and the counterintuitive answer.

SPSantosh Paudel· July 25, 2026· 9 min read
Table of contents

A streak is a sequence of options with a rising exercise probability. A user on day 20 is a fundamentally different risk than a user on day 1, and the reward schedule should reflect that. Most do not, because most are designed around round numbers rather than around where users actually leave.

The short answer

Streak drop-off is governed by a hazard rate that starts high and falls as the surviving population self-selects. Placing a reward on day 1 produces the most additional long-run survivors in absolute terms. It also costs 12.7 times more per survivor than the same reward on day 29, because on day 1 you are paying every single user in the cohort. Under a fixed budget the answer inverts completely.

Modelling continuation, not retention

Retention curves tell you how many users are left. Hazard rates tell you the probability a user who is here today is still here tomorrow, and that is the quantity you can actually influence with a reward.

Streak hazards have a characteristic shape: continuation probability is lowest at the start and rises toward an asymptote. Someone who has kept a streak for three weeks has demonstrated something about themselves that someone on day 2 has not.

Model it as continuation probability p(d) rising exponentially toward a ceiling:

p(d) = p_inf - (p_inf - p_1) x exp(-k(d - 1))

With p_1 = 0.55, p_inf = 0.95 and k = 0.35, a 10,000-user cohort behaves like this:

Dayp(continue)Users activeUsers lost that day
10.550010,0004,500
20.66815,5001,825
30.75143,675914
50.85142,237332
70.90101,677166
140.945899554
210.949668635
300.9500432n/a

Forty five percent of the cohort never reaches day 2. By day 7 you have 16.8% left. The day-30 survivor count is 432 out of 10,000.

The question everyone asks wrong

"Where should we put the reward?" has no answer until you say what you are optimising and what you are spending.

Suppose a reward costs $2 per user who receives it, and lifts that day's continuation probability by 10 percentage points. Two obvious objectives give opposite answers.

Reward dayUsers paidSpendExtra day-30 survivorsCost per survivor
110,000$20,00078.5$254.87
25,500$11,00064.6$170.29
33,675$7,34957.4$127.95
52,237$4,47350.7$88.24
71,677$3,35347.9$70.00
14995$1,99145.6$43.63
21686$1,37145.4$30.17
29454$90945.4$20.00

Day 1 produces the most extra survivors: 78.5 on a baseline of 431.6, an 18.2% lift. Day 29 produces 45.4 extra survivors for $909 instead of $20,000.

Day 1 costs 12.7 times more per survivor than day 29. Both columns are correct. The tension is real, and it is not resolved by picking a favourite metric.

What a budget does to the answer

Give the same programme a fixed $2,000 and the ranking inverts.

Reward dayUsers you can afford to rewardExtra day-30 survivors
11,0007.8
31,00015.6
71,00028.6
1499545.6
2168645.4
2945445.4

At $2,000 you can only reach 1,000 users. Spending them on day 1 buys 7.8 survivors. Spending them on day 14 buys 45.6, nearly six times more, and costs less because there are fewer than 1,000 users left to pay.

The reason is that a day-1 reward is mostly wasted on users who were about to churn regardless, and a day-14 reward lands on a population that has already proven it will probably continue. You are buying a cheaper marginal probability from a more reliable customer.

The model

import math

def streak_analysis(cohort=10_000, p1=0.55, p_inf=0.95, k=0.35,
                    horizon=30, lift=0.10, reward_cost=2.00, budget=None):
    def p_cont(day):
        return p_inf - (p_inf - p1) * math.exp(-k * (day - 1))

    surv = [1.0]
    for d in range(1, horizon):
        surv.append(surv[-1] * p_cont(d))

    def tail(frm, to):
        q = 1.0
        for d in range(frm, to):
            q *= p_cont(d)
        return q

    rows = []
    for d in range(1, horizon):
        at_risk = cohort * surv[d - 1]
        reached = at_risk if budget is None else min(at_risk, budget / reward_cost)
        saved = reached * lift * tail(d + 1, horizon)
        spend = reached * reward_cost
        rows.append((d, reached, spend, saved, spend / saved if saved else float("inf")))
    return rows, cohort * surv[horizon - 1]


rows, baseline = streak_analysis()
print(f"baseline day-30 survivors: {baseline:,.1f}\n")
print(f"{'day':>4} {'paid':>9} {'spend':>10} {'extra':>8} {'$/survivor':>12}")
for d, paid, spend, saved, cps in rows:
    if d in (1, 3, 7, 14, 21, 29):
        print(f"{d:>4} {paid:>9,.0f} {spend:>10,.0f} {saved:>8.1f} {cps:>12,.2f}")

Output:

baseline day-30 survivors: 431.6

 day      paid      spend    extra   $/survivor
   1    10,000     20,000     78.5       254.87
   3     3,675      7,349     57.4       127.95
   7     1,677      3,353     47.9        70.00
  14       995      1,991     45.6        43.63
  21       686      1,371     45.4        30.17
  29       454        909     45.4        20.00

Pass a budget argument to see the inversion.

Fitting this to your product

The three parameters are estimable from data you already have. p_1 is your day-1 to day-2 continuation rate. p_inf is the continuation rate among long-streak users, which flattens out and is easy to read off. k controls how fast you get from one to the other, and one line of curve fitting or a manual sweep will find it.

Then the decision becomes concrete rather than aesthetic. If you are unconstrained and optimising raw retained users, reward early. If you have a budget, which you do, reward where the surviving population is dense enough to be worth paying and sparse enough to be affordable.

The mistake worth avoiding is the default: a reward at day 7 because a week is a familiar unit. Day 7 is neither the best total nor the best efficiency in this model. It is a round number, and round numbers are where unpriced mechanics go.

Leaderboards have the same problem in a different shape. The default design shows every player a ranked list, and for most of them that list is a daily notification that they cannot win.

Free resource

Get the Promo & Bonus Economics Workbook

Every formula for pricing a promo before you launch it: expected value, wagering cost, breakage, breakeven conversion. Worked examples with real executed numbers.

No spam. Unsubscribe anytime.

Browse all free guides →

Want to implement this with guidance?

Santosh helps founders turn insights like this into real systems.

Promo Economics Audit

External Resources

Further Reading & Tools

Related Posts

01
9 min
Risk ManagementPromo Economics
TodayQuantitative Marketing

Value at Risk and Kill Switches for Live Promotions

A spend cap set at expected payout shuts down 46% of perfectly healthy campaigns. The right multiple is 1.52x. Sizing exposure limits and automated kill switches against a real payout distribution.

Read article
02
8 min
Incentive DesignBehavioral Economics
TodayQuantitative Marketing

The Line Between Motivating and Manipulating: An Incentive Design Ethics Test

Every mechanic in this series works because of a cognitive bias. That makes them powerful and makes some of them indefensible. A six-question test for which side of the line your design sits on, and what regulators have already acted against.

Read article
03
8 min
Price DiscriminationPricing
TodayQuantitative Marketing

Why Promo Codes Beat Sales: Price Discrimination in Practice

A blanket 20% discount earns $20,000 less than no discount at all. The same discount behind a code that only bargain hunters bother to find earns $64,000 more. The friction is the entire mechanism.

Read article