DEV Community

Alan Matthew
Alan Matthew

Posted on

Population Growth Sandbox: 6 Experiments That Show How Carrying Capacity Really Works

In 1911, a small herd of reindeer was released on St. Paul Island, Alaska, a place with plenty of lichen and no predators. By most accounts the herd grew into the low thousands within a few decades, ate the island's slow-growing pasture down to almost nothing, and then collapsed to a tiny remnant. (The exact figures vary by source, so treat these as a rough shape rather than a dataset.)

That story is the whole idea behind carrying capacity in one picture: growth that feels unstoppable, a ceiling nobody can see until they hit it, and a crash when a population overshoots.

Textbooks compress this into one equation. Equations don't build intuition, though. Turning the knobs does. So this post is a lab notebook. We'll take one small population, run six experiments on it, change one variable at a time, and watch what breaks.

You can follow along with the free Carrying Capacity Calculator, which models logistic growth over time and shows the curve, a time-series table, and the step-by-step derivation. I'll also include short Python snippets for the developers reading this.

The equipment: four numbers and two equations

Everything below uses four parameters:

Symbol Meaning
N₀ Starting population size
r Intrinsic growth rate, per time step
K Carrying capacity, the maximum the environment can sustain
t Time

And two models.

Exponential growth (unlimited resources):

dN/dt = rN          →     N(t) = N₀ · e^(rt)
Enter fullscreen mode Exit fullscreen mode

Logistic growth (limited resources), introduced by Pierre François Verhulst in 1838:

dN/dt = rN · (K − N) / K

N(t) = K / (1 + ((K − N₀) / N₀) · e^(−rt))
Enter fullscreen mode Exit fullscreen mode

The only difference is the braking term (K − N) / K. It's close to 1 when the population is tiny (so growth looks exponential), it shrinks toward 0 as N approaches K (so growth stops), and it turns negative if N is above K (so the population shrinks).

The second line is the "integrated" or closed-form solution. It gives the population at any time directly, with no step-by-step simulation. That detail matters a lot in Experiment 6.

Three derived quantities will come up repeatedly:

  • Inflection point: growth is fastest when N = K/2.
  • Maximum growth rate: (dN/dt)ₘₐₓ = rK/4.
  • Time to reach the inflection point: t = (1/r) · ln((K − N₀) / N₀).

Our test population for every experiment unless stated otherwise: N₀ = 10, K = 1,000, r = 0.5 per time step.


Experiment 0: the baseline run

Hypothesis: The population will start slowly, speed up, then flatten near 1,000.

Here's the full run, with the exponential prediction alongside for comparison:

t Logistic N N / K dN/dt Exponential N
0 10.00 0.010 4.95 10.0
2 26.72 0.027 13.00 27.2
4 69.45 0.069 32.31 73.9
6 168.66 0.169 70.11 200.9
8 355.46 0.355 114.55 546.0
9.19 500.00 0.500 125.00 990.0
10 599.86 0.600 120.01 1,484.1
12 802.96 0.803 79.11 4,034.3
14 917.20 0.917 37.97 10,966.3
16 967.86 0.968 15.56 29,809.6
20 995.53 0.996 2.23 220,264.7

Result: Confirmed, and the details are more interesting than the hypothesis.

  1. The two models are almost identical early on. At t = 4, logistic gives 69.5 and exponential gives 73.9. If you only had data up to that point, you could not tell which model was right. This is the central difficulty of estimating carrying capacity from early data (more on that later).
  2. Peak growth is at t ≈ 9.19, when N = 500 exactly. That's K/2, and the growth rate there is 125, which equals rK/4 = 0.5 × 1000 / 4.
  3. The population is 99.6% of K by t = 20, but it never quite reaches 1,000. It approaches K asymptotically. The growth rate drops toward zero because the braking term is nearly zero.
  4. Exponential growth is absurd by t = 20. It predicts over 220,000 individuals in an environment that can support 1,000.

This is the classic S-shaped (sigmoid) curve, as opposed to the J-shaped exponential one.

Try it: enter N₀ = 10, r = 0.5, K = 1000 in the calculator and check the summary metrics. You should see an inflection point of 500, a max growth rate of 125, and an inflection time of 9.1902.


Experiment 1: turn up the growth rate (r)

Hypothesis: A higher r means a higher carrying capacity.

Let's test it. Keep N₀ = 10 and K = 1000, and vary r:

r Time to inflection Max growth rate (rK/4) Time to reach 90% of K
0.25 18.38 62.5 27.17
0.50 9.19 125.0 13.58
1.00 4.60 250.0 6.79
2.00 2.30 500.0 3.40

Result: Hypothesis rejected. K never moved. All four populations end up at 1,000. What r controls is how fast you get there.

Notice the pattern: doubling r halves every time value (18.38 → 9.19 → 4.60 → 2.30) and doubles the peak growth rate. This follows from the equations, since r only appears as e^(−rt) and as a multiplier. Time scales as 1/r.

The lesson is about biotic potential. A fast-breeding species (bacteria, insects, rodents) reaches its ceiling quickly, but the ceiling is set by resources, not by how fast the species can reproduce. A slow-breeding species (elephants, whales) gets to its K more slowly but has the same fundamental limit.


Experiment 2: change the starting population (N₀)

Hypothesis: Starting with more individuals raises where the population ends up.

Same setup (K = 1000, r = 0.5), different starting sizes:

N₀ Time to inflection Time to 90% of K
1 13.81 18.21
10 9.19 13.58
100 4.39 8.79
400 0.81 5.21
500 0 (already at K/2) 4.39
800 n/a (already past K/2) 1.62

Result: Rejected again. N₀ shifts the timeline, not the destination. Every row ends at 1,000.

Two details are worth catching:

  • Once N₀ ≥ K/2, there's no inflection point in the future. The population is already at or beyond its fastest-growth moment, so it starts in the decelerating phase. A well-built calculator should handle this case explicitly, and this tool's page says it notes when the inflection point doesn't apply.
  • A tiny founder population (N₀ = 1) is slow, but not weak. It takes about 18 time steps to reach 90% of K, versus 13.6 for N₀ = 10. The destination is the same.

This also explains post-disaster recovery. After a fire or storm knocks a population down, it climbs back along the same S-curve, and recovery is slowest when numbers are lowest.


Experiment 3: raise the ceiling (K)

Hypothesis: Doubling K doubles everything.

Keep N₀ = 10 and r = 0.5, and vary K:

K Time to inflection Max growth rate (rK/4)
500 7.78 62.5
1,000 9.19 125.0
2,000 10.59 250.0

Result: Half right. Peak growth rate doubles each time, because it's proportional to K. But the time to inflection only goes up by about 1.39 each time you double K.

That number isn't random. It's ln(2) / r = 0.693 / 0.5 ≈ 1.386. A population living in an environment twice as large doesn't take twice as long to hit its peak growth. It takes a fixed extra amount, because early growth is exponential and doubling a target on an exponential curve costs a constant amount of time.

The ecological reading: a richer environment gives you a bigger ceiling and a higher peak growth rate, but it doesn't slow the early rise much.


Experiment 4: start above the ceiling (overshoot)

Hypothesis: If N₀ is greater than K, the population will decline smoothly to K.

Set N₀ = 1000, K = 500, r = 0.2:

t N dN/dt
0 1,000.00 −200.00
5 612.70 −27.62
10 536.29 −7.78
15 512.76 −2.62
20 504.62 −0.93
30 500.62 −0.12
40 500.08 −0.02

Result: Confirmed. The braking term (K − N) / K is negative when N > K, so dN/dt is negative. The population shrinks fast at first (−200 per step) and slows as it approaches K from above. This is the "dieback" phase.

Now the limitation, which is important. This model shows a smooth return to K. It never undershoots, never oscillates, never crashes below the ceiling. Real populations often do, as the St. Paul Island reindeer story shows. Why?

Because real populations have time lags. Resources get consumed now, but starvation shows up later. By the time the population "notices" it's over capacity, it's already far over, and the correction is brutal. The basic logistic equation assumes the brake responds instantly to current density. Delayed-response models are needed for the boom-and-bust cycles seen in the wild, and that's a good reminder of what a simple model leaves out.


Experiment 5: how fast is the fastest?

Before the last experiment, let's check three homework-style problems, since these are exactly the questions a calculator helps you verify.

Problem A: instantaneous growth rate. Field mice have r = 0.15 per year, K = 800, and the current population is N = 200. What is dN/dt?

dN/dt = 0.15 × 200 × (800 − 200)/800 = 30 × 0.75 = 22.5 mice per year
Enter fullscreen mode Exit fullscreen mode

Problem B: population at a given time. Bacteria start at N₀ = 10 with K = 1000 and r = 0.5 per hour. What's N after 8 hours?

(K − N₀)/N₀ = 99
N(8) = 1000 / (1 + 99 · e^(−4)) = 1000 / 2.8132 ≈ 355.46
Enter fullscreen mode Exit fullscreen mode

(Check this against the Experiment 0 table: it matches.)

Problem C: time to a target. How long until the same bacteria reach 90% of K? Solving N = 0.9K gives a general formula for reaching a fraction p of the ceiling:

t = (1/r) · ln( p/(1−p) · (K − N₀)/N₀ )
Enter fullscreen mode Exit fullscreen mode

For p = 0.9: t = 2 · ln(9 × 99) = 2 · ln(891) ≈ 13.58 hours. That's the value in the r = 0.5 row of Experiment 1.


Experiment 6: break the simulator

This one is for anyone who's ever written a simulation loop.

You can model logistic growth two ways:

  1. Closed form: plug t into N(t) = K / (1 + A·e^(−rt)). Exact.
  2. Euler stepping: start at N₀ and repeatedly apply N ← N + r·N·(1 − N/K)·Δt. Approximate.

Hypothesis: Both give essentially the same curve.

import math

def exact(N0, K, r, t):
    return K / (1 + ((K - N0) / N0) * math.exp(-r * t))

def euler(N0, K, r, steps, dt=1):
    N, out = N0, [N0]
    for _ in range(steps):
        N += dt * r * N * (1 - N / K)
        out.append(N)
    return out

for r in (0.5, 1.5, 2.2):
    print(f"r={r}")
    print("  euler:", [round(x) for x in euler(10, 1000, r, 9)])
    print("  exact:", [round(exact(10, 1000, r, t)) for t in range(10)])
Enter fullscreen mode Exit fullscreen mode

Output:

r=0.5
  euler: [10, 15, 22, 33, 49, 73, 106, 154, 219, 305]
  exact: [10, 16, 27, 43, 69, 110, 169, 251, 355, 476]
r=1.5
  euler: [10, 25, 61, 147, 336, 670, 1002, 999, 1000, 1000]
  exact: [10, 43, 169, 476, 803, 948, 988, 997, 999, 1000]
r=2.2
  euler: [10, 32, 99, 297, 755, 1162, 748, 1163, 747, 1163]
  exact: [10, 84, 451, 881, 985, 998, 1000, 1000, 1000, 1000]
Enter fullscreen mode Exit fullscreen mode

Result: Hypothesis rejected, and it gets worse as r increases.

  • At r = 0.5, Euler with Δt = 1 is visibly slower than the truth (305 vs 476 at t = 9). The step is too coarse.
  • At r = 1.5, Euler overshoots K (1002) before settling.
  • At r = 2.2, Euler never settles. It bounces between about 1,163 and 747 forever, while the true curve flattens smoothly at 1,000.

That bouncing is a numerical artifact, not biology. The stepping scheme is only stable when r·Δt < 2. Past that, the discrete update flips the sign of the error each step. (Researchers actually study discrete logistic maps for this very behavior, but that's a different model than the continuous one here.)

The takeaway for anyone building or choosing a growth simulator: use the closed-form solution when you have one. According to its documentation, the carrying capacity tool evaluates the integrated formula at every time step rather than stepping iteratively, so you don't get these step-size artifacts, whatever r you enter.


Fitting K from real(ish) data

So far we've chosen K. In real research you have to estimate it from counts. Here's a quick demonstration with synthetic data. I generated noisy measurements (about 5% noise) from a known population with N₀ = 15, K = 800, r = 0.45, then tried to recover those values:

import numpy as np
from scipy.optimize import curve_fit

def logistic(t, N0, K, r):
    return K / (1 + ((K - N0) / N0) * np.exp(-r * t))

rng = np.random.default_rng(42)
t = np.arange(0, 21, 2.0)
obs = logistic(t, 15, 800, 0.45) * (1 + rng.normal(0, 0.05, len(t)))

# Fit using ALL the data
params, _ = curve_fit(logistic, t, obs, p0=[obs[0], obs.max(), 0.3])
print(np.round(params, 3))        # [N0, K, r]  ->  [ 16.216 808.509   0.428]

# Fit using only the early points (t <= 6)
early = t <= 6
params2, _ = curve_fit(logistic, t[early], obs[early],
                       p0=[obs[0], obs.max() * 2, 0.3], maxfev=20000)
print(np.round(params2, 3))       # ->  [ 13.876 646.679   0.485]
Enter fullscreen mode Exit fullscreen mode

With all the data, the fit recovers K ≈ 809 (true value 800), which is close. With only the first few points, it estimates K ≈ 647, about 19% too low, and also gets r wrong.

That's Experiment 0's early-data problem in action. Exponential and logistic curves look almost the same before the population nears half of K. You can only pin down K well once the data bends toward the plateau. If you ever read a paper or a homework answer claiming a confident K from the exponential phase alone, be skeptical.


What the model assumes (and where it breaks)

Every result above depends on the logistic model's assumptions:

  • K and r are constants. In reality, K shifts with seasons, climate, and habitat change. A drought lowers K for a grazing population.
  • Instant feedback. The brake responds to current density with no delay (Experiment 4's limitation).
  • All individuals are identical. No age structure, no sex ratio, no differences in competitive ability.
  • A closed population. No immigration or emigration.
  • Smooth, continuous growth. Real populations are made of whole individuals, and small populations face random chance.

None of this makes the model useless. It makes it a baseline, the thing you compare real data against to see how a population deviates and why.

What actually sets K?

The ceiling comes from limiting factors. Ecologists split them into two groups:

  • Density-dependent factors get worse as the population grows. Competition for food, water, space, and light; predation; disease spreading in crowded groups; and in a closed system like a yeast flask, the build-up of toxic waste such as ethanol. These are what produce the braking effect in the equation.
  • Density-independent factors hit regardless of population size: wildfires, hurricanes, severe weather. They knock N down temporarily, after which the population regrows along the S-curve. They don't define K itself.

Five misconceptions this sandbox cleans up

  1. "Growth is fastest at K." No: growth is zero at K and fastest at K/2.
  2. "A higher growth rate means a higher carrying capacity." No: r controls speed, K controls the ceiling (Experiment 1).
  3. "A bigger starting population means a bigger final population." No: N₀ shifts timing only (Experiment 2).
  4. "At K, nothing is happening." Births still equal deaths. The population is in dynamic balance, not frozen.
  5. "Populations always settle smoothly at K." Only in the basic model. Real populations overshoot, crash, and oscillate, especially with time lags.

The one-page cheat sheet

Quantity Formula
Exponential growth N(t) = N₀·e^(rt)
Logistic growth rate dN/dt = rN(K − N)/K
Population at time t N(t) = K / (1 + ((K−N₀)/N₀)·e^(−rt))
Inflection point N = K/2
Max growth rate rK/4
Time to inflection (1/r)·ln((K − N₀)/N₀)
Time to reach fraction p of K (1/r)·ln( p/(1−p) · (K−N₀)/N₀ )
Equilibrium dN/dt = 0 at N = K (births = deaths)
If N > K dN/dt < 0 (dieback)

How to use the calculator effectively

I'd suggest using the Carrying Capacity Calculator the same way these experiments do: as a sandbox, not an answer machine.

What the page says it provides:

  • Inputs for N₀, r, K, and the number of time steps, with rounding to 2, 4, or 6 decimals.
  • Summary metrics: the environmental limit, the inflection point (K/2), the time to inflection, and the maximum growth rate.
  • A simulation curve with the carrying capacity and inflection point marked.
  • Step-by-step derivation of the current model.
  • A time-series table with population size, per-capita capacity usage (N/K), growth rate, and the growth phase at each step, plus CSV export.
  • It runs in your browser.

A good workflow for a homework problem:

  1. Solve it by hand with the formulas above.
  2. Enter the same parameters and compare your answer to the table row.
  3. If they don't match, look at the derivation steps to find where your math diverged.
  4. Then change one parameter and predict what will happen before you look. That's how you build intuition instead of just checking answers.

It's a modeling aid and a checking tool. It doesn't know whether your K and r are realistic for your species, and a model is only as good as its parameters.


Your turn

Here's a challenge. Predict the answers first, then verify them in the calculator.

A population starts at N₀ = 25 in an environment with K = 2,000 and r = 0.3 per month.

  1. At what population size is growth fastest?
  2. What is the maximum growth rate?
  3. At what time does the population hit that point?
  4. Bonus: how long until it reaches 90% of K?

Post your answers in the comments. And if you've ever built a simulation that oscillated when it shouldn't have, I'd like to hear what step size got you.

Top comments (0)