DEV Community

Sungwoo Lee
Sungwoo Lee

Posted on Originally published at my-blog.org

Why Growth Curves Bend: Exponential vs. Logistic, With Code

In early 2020 a lot of people learned the phrase "doubling every three days." A month later the same commentators were saying "growth is slowing." The math didn't change. What changed is that the quantity being measured ran into a ceiling — and every unbounded growth curve you'll ever plot a metric against does the same thing, whether it's infections, DAU, or a tweet's retweet count.

This post walks through why, with formulas you can verify on a calculator and a script short enough to paste into a REPL.

Exponential growth is a multiplication rule, not a speed

The discrete form is:

y = a * b**t
Enter fullscreen mode Exit fullscreen mode

a is the starting value, b is the growth factor per period, t is the number of periods. If b = 2, the quantity doubles every period. The continuous form uses e:

y = a * e**(r*t)
Enter fullscreen mode Exit fullscreen mode

where r is the continuous growth rate and b = e**r. Both describe the same property: the rate of change is proportional to the current size. That's the entire definition — nothing in it says "fast." A process growing at r = 0.001 per year is exponential in structure but you'd never notice it happening. Media shorthand for "exponential" as "extremely fast" is a category error; exponential means "multiplicative," full stop.

Doubling time, computed, not looked up

Doubling time is how many periods it takes for y to hit 2a. Solve a * e**(r*t) = 2a for t and you get:

import math

def doubling_time(r):
    return math.log(2) / r

for r in [0.01, 0.05, 0.10, 0.25, 0.50, 1.00]:
    print(f"r={r:.2f} -> {doubling_time(r):.2f} periods to double")
Enter fullscreen mode Exit fullscreen mode

Running that gives:

Growth rate (r) Doubling time
1% 69.31 periods
5% 13.86 periods
10% 6.93 periods
25% 2.77 periods
50% 1.39 periods
100% 0.69 periods

The "Rule of 70" shortcut (divide 70 by the percentage rate) is just ln(2) ≈ 0.693 ≈ 0.70 rounded for mental math — at 7% growth, 70/7 = 10 periods, which matches ln(2)/0.07 = 9.9. Nothing mystical, just a rounding convenience.

Why the curve has to bend

The exponential model has a hidden assumption: unlimited room to grow. An infected person always finds a susceptible one, a shared post always finds a new viewer. That assumption is false in every bounded system, which is every real system. Once you add a ceiling — call it K, the carrying capacity — the growth rate has to fall as you approach it. That's the logistic model:

def logistic(n0, r, k, t):
    return k / (1 + ((k - n0) / n0) * math.exp(-r * t))
Enter fullscreen mode Exit fullscreen mode

dN/dt = r * N * (1 - N/K) is the differential-equation form. The (1 - N/K) term is a brake: near zero it's close to 1 (so growth looks purely exponential), and as N approaches K it drops toward 0 (so growth stops). This is exactly why early-stage growth of almost anything — a pandemic, a viral post, a new product — looks exponential: locally, before N is a meaningful fraction of K, the brake term is doing nothing yet.

Watching the two models diverge

Same starting conditions, run both models out over 70 periods:

n0, k, r = 100, 1_000_000, 0.3

def exponential(a, r, t):
    return a * math.exp(r * t)

for day in [0, 10, 20, 30, 40, 50, 60, 70]:
    e = exponential(n0, r, day)
    l = logistic(n0, r, k, day)
    print(f"day {day:2d}: exponential={e:,.0f}  logistic={l:,.0f}")
Enter fullscreen mode Exit fullscreen mode

By day 30 the two curves are still close — the exponential model hasn't broken anything yet. By day 70, the exponential model predicts roughly 1.3 * 10**11 — about 130,000 times the entire population you defined as K = 1,000,000. The logistic model, run with the identical r, is sitting at roughly 999,992: essentially saturated. Nothing about the underlying process changed between the two models except the brake term. The exponential model isn't wrong because the math is bad; it's wrong because it was never told there's a ceiling.

You can solve for exactly when the curve bends — the inflection point, where the growth rate peaks — by setting N = K/2 and solving for t:

t* = ln((K - N0) / N0) / r
Enter fullscreen mode Exit fullscreen mode

For the numbers above (N0 = 100, K = 1,000,000, r = 0.3), that's ln(9999) / 0.3 ≈ 30.7 days. Before that day, each new period's absolute growth is still accelerating. After it, growth keeps happening but the rate of growth is falling — even though the raw numbers can still look large for a while.

Exponential vs. logistic, side by side

Property Exponential Logistic
Formula y = a·e^(rt) dN/dt = rN(1 − N/K)
Growth rate Constant r Falls as N → K
Long-run shape Unbounded (J-curve) Saturates at K (S-curve)
Inflection point None — always accelerating At N = K/2
Where it's a good model Early phase only Any bounded process, full lifecycle

Why this matters for anything you're tracking

If you've ever fit a trendline to a metric — signups, API calls, weekly active users — and extrapolated it forward, you did the same thing the early-2020 commentators did with case counts. The fit isn't wrong on the data you have; it's wrong about what the data will keep doing, because it silently assumes there's no K. Two fixes: either fit a logistic curve directly once you have enough data to estimate an inflection, or treat any exponential fit as valid only for forecasting a few periods past your last data point — the part of the S-curve where the brake term genuinely hasn't kicked in yet.

I worked through the full derivation — plus the R₀/epidemiology framing and the technology-adoption S-curve examples — over at the original post, if you want the longer version with more worked tables.

Top comments (0)