DEV Community

Aneesh Hari
Aneesh Hari

Posted on

Why your progress bar's ETA lies, and the survey-sampling trick that fixes it

We've all trusted a progress bar that said "5 minutes left," walked away, and come back an hour later to find it barely moved. It's not a bug. It's built into how progress bars estimate.

Here's the core problem, and it's more interesting than it looks.

The lie every progress bar tells

A normal progress bar computes ETA like this: "I've done 2,000 of 10,000 items in 10 minutes, so the remaining 8,000 will take about 40 more." That assumes the rest of the job looks like the part you already ran.

That assumption breaks exactly when it matters most, which is when the expensive work is at the end:

for tile in satellite_tiles:   # first 8,000 are small; last 2,000 are huge
    process(tile)
Enter fullscreen mode Exit fullscreen mode

At 80% done, a plain bar happily reports "3 minutes left," then the heavy tail hits and it's actually 3 hours. The bar never saw it coming, because everything it learned came from the cheap early items.

I tested this across a range of workload shapes. A plain count-based ETA is routinely 60 to 280 percent off on uneven jobs:

shape           actual   plain bar   off
heavy-tail        3.9s        1.6s   60%
heavy-middle      4.3s        1.3s   70%
heavy-front       4.3s       16.4s  278%
random-spikes     3.6s        6.0s   64%
Enter fullscreen mode Exit fullscreen mode

The fix: stop extrapolating the future, sample it

Here's the idea I couldn't shake. For a loop over a known set of items, the future work already exists. You just haven't run it yet. So instead of guessing what the remaining 8,000 items look like from the 2,000 you saw, why not measure a tiny representative sample of the ones you haven't run?

This is design-based survey sampling, the same statistical machinery ecologists use to estimate a population (fish, trees, sharks) from a small, carefully chosen sample. You draw a stratified or systematic sample of the remaining items, measure their true cost, and form a Horvitz-Thompson estimate of the total. Because the sample is chosen by design, the estimate is unbiased and comes with a real margin of error rather than a blind guess.

I actually first used this exact approach years ago in an ecology paper on adaptive-cluster sampling. The insight transfers cleanly: when the thing you care about is concentrated in places you haven't looked yet, a well-designed sample beats extrapolating from what you happened to see first.

The result: sampling about 2 percent of the remaining work cuts the error to near zero on the same hard cases:

shape           plain bar   RunScope
heavy-tail          60%        0%
heavy-middle        70%        0%
heavy-front        278%        1%
random-spikes       64%        3%
uniform              0%        0%   (does no harm on easy jobs)
Enter fullscreen mode Exit fullscreen mode

Meet RunScope

I packaged this into a small, dependency-free library called RunScope. It's a drop-in that gives you an honest range instead of a fake exact number.

pip install runscope
Enter fullscreen mode Exit fullscreen mode
import runscope

for item in runscope.track(items, key="my_job"):
    process(item)
Enter fullscreen mode Exit fullscreen mode
my_job |########------------| 38% 17492/48000 | 1h20m left (1h12m-1h31m) | high | [Padawan]
Enter fullscreen mode Exit fullscreen mode

It has three modes, and you never pick. It uses the best it can:

  • Padawan: a smarter current-run estimate that reacts to slowdowns instead of hanging.
  • Master: after you run the same job a few times, it learns that job's behavior and calibrates automatically.
  • Jedi: for uneven or back-loaded jobs, it samples a little of the future work to predict the heavy tail before it hits.

There's a tqdm drop-in too:

from runscope import trange
for i in trange(10000, key="my_job"):
    ...
Enter fullscreen mode Exit fullscreen mode

The honest caveat

RunScope is for enumerable work: loops over files, records, images, tiles, simulations, parameter grids. It does not try to predict the runtime of an arbitrary opaque function. And when there genuinely isn't enough information to estimate honestly, it says so instead of inventing a number. That restraint is the whole point.

Try it, or roast it

This is my first open-source release, and I'd genuinely love your feedback. Issues and PRs very welcome.

If you've ever been burned by a lying ETA, give it a spin and tell me where it holds up and where it doesn't.

Top comments (0)