Skip to main content

Command Palette

Search for a command to run...

Stop Reporting Percent Complete. Ship the Unknowns First.

Why dev tasks stall at 90% done, a 25-line simulation that shows where the delay hides, and a status format that keeps your dates honest.

Updated
•11 min read•View as Markdown
Stop Reporting Percent Complete. Ship the Unknowns First.
J
I'm a software engineer who spends most days building systems that solve real problems. When I'm not shipping code, I'm either untangling a tricky problem or writing about what I learned doing it. Currently exploring AI on the side.

Most of us run our task lists through an invisible sort before we start working:

plan = sorted(ticket.subtasks, key=how_well_i_understand_it, reverse=True)

Nobody writes that line. Everybody executes it. And it's the reason a task can sit at "90% done" for two weeks without anyone lying about anything.

There's an old rule for this, attributed to Tom Cargill at Bell Labs and popularized by Jon Bentley's Programming Pearls column in 1985: the first 90% of the code takes the first 90% of the time, and the last 10% takes the other 90%. It usually gets quoted as a joke. It's more useful read as a bug report.

TL;DR

  • The last 10% isn't slow because it's harder typing. It's slow because sorting by comfort pushes all the doubt to the end.

  • Percent complete is computed against a list that grows when unknowns get opened. It can drop, and it drops late.

  • In a toy simulation below, one unknown task out of six produces about 91% of the expected overrun.

  • Padding, finer task-splitting and "eat the frog" don't help, because none of them move the unknowns earlier.

  • Fix the order (doubt first) and change the report (proven / unproven / next / when you'll know).

The bug: your queue is sorted by comfort

Doing the understood parts first is a reasonable instinct. They're estimable, they show visible progress, and they feel productive. A jigsaw puzzle works the same way: edges and distinctive pieces go first, and the plain blue sky gets left for last. "80% of the pieces are in" is true, and it tells you almost nothing about the time left.

Sorted that way, whatever remains on your list has been filtered for one property: it's the part you didn't understand. So "90% done" really means "90% of the understood work is done." The rest hasn't been estimated in any meaningful sense, because you didn't know what it was.

A ticket that does exactly this

A hypothetical, but a realistic one. Here's a CSV export ticket on day two:

## CSV export for the reports page
- [x] Export endpoint
- [x] Query for the report data
- [x] CSV formatting
- [x] Export button
- [x] Permission check
- [ ] Handle large exports
Progress: 5/6 (83%)

Then you open the last item. Test accounts have a few hundred rows; the largest customer has 200,000, and building that file inside one web request blows past the request timeout. Real data has a shape your local data never had, which is the same reason apps fly in dev and crawl in production.

Day four:

## CSV export for the reports page
- [x] Export endpoint
- [x] Query for the report data
- [x] CSV formatting
- [x] Export button
- [x] Permission check
- [ ] Background job to build the file
- [ ] File storage
- [ ] Expiring download link
- [ ] Permission check on the download link
- [ ] Cleanup of old export files
- [ ] "Your export is ready" notification
Progress: 5/11 (45%)

More work done, lower real progress. Standup still heard "almost done," because nobody announces a falling percentage.

Simulate it: where the delay actually hides

Unknown tasks miss in one direction. A well-understood task lands a bit early or a bit late. An unknown can't finish much faster than estimated, but it can take several times longer. Erik Bernhardsson made this case in a widely shared 2019 post that modeled overruns as log-normal: developers estimate the median reasonably well, the mean is much larger, and the highest-uncertainty tasks dominate the expected total.

You can see the shape with a toy model. Six tasks, each estimated at one day. Five are well understood; one is an unknown.

import random, statistics

random.seed(42)

# Six tasks, each estimated at 1 day.
# Five are well understood (small spread). One is an unknown (wide spread).
# Actual time = estimate * lognormal blowup, median blowup = 1x (the estimate).
KNOWN_SIGMA, UNKNOWN_SIGMA = 0.2, 1.2

def one_project():
    known = sum(random.lognormvariate(0, KNOWN_SIGMA) for _ in range(5))
    unknown = random.lognormvariate(0, UNKNOWN_SIGMA)
    return known, unknown

runs = [one_project() for _ in range(100_000)]
totals = sorted(k + u for k, u in runs)

mean_known = statistics.mean(k for k, _ in runs)
mean_unknown = statistics.mean(u for _, u in runs)

print(f"median actual:   {statistics.median(totals):.1f} days")
print(f"mean actual:     {statistics.mean(totals):.1f} days")
print(f"90th percentile: {totals[int(0.9 * len(totals))]:.1f} days")
print(f"unknown task's share of the expected overrun: "
      f"{(mean_unknown - 1) / (mean_known + mean_unknown - 6):.0%}")

Output:

median actual:   6.2 days
mean actual:     7.1 days
90th percentile: 9.8 days
unknown task's share of the expected overrun: 91%

The sigma values are made up for illustration, so don't treat these as data about real teams. The point is structural: the "typical" outcome looks close to the plan, while the expected outcome and the bad-case outcome are driven almost entirely by the one task nobody understood. Now remember the sort order. That task is the one you scheduled last.

Percent complete measures the work you've finished. Your deadline is decided by the doubt you haven't touched.

Why the usual fixes miss

Common fix What it changes Why it misses
Pad the estimate by 20% Adds time evenly Doubt isn't spread evenly; one task can overrun far more than 20%
Split into smaller tasks Precision on known work You can't decompose what you don't understand yet
"Eat the frog" Does the dreaded task first The dreaded task and the unknown task are often different
Push harder at the end More hours The last stretch is discovery, and hours don't turn questions into answers

To be fair to small tasks: Karl Wiegers argues for small, done-or-not-done tasks to kill partial-credit reporting, and that genuinely helps with known work. It just can't help with the item that looked small on day one.

The doubt-first workflow

Same total work, different order. The point isn't finishing sooner. It's finding out sooner.

1. Write the doubt list before the task list. One question: what could make this three times bigger? Common answers are production-scale data, a third-party API you haven't called, permissions and multi-tenancy, a migration on a large table, and old code nobody has touched in years. That last one is sneaky, because old code only charges interest when you change it, and your ticket might be the change. A ticket template helps it stick:

## Doubts (resolve these first)
- [ ] What could make this 3x bigger?
- [ ] Which part have I never done in this codebase?
- [ ] What does production data look like here (size, nulls, tenants)?
- [ ] Which external system's real behavior am I assuming?

2. Walk a thin skeleton end to end. One rough path through every layer (UI, API, job, storage) with real-shaped data, before polishing any layer. Alistair Cockburn calls this a walking skeleton; Hunt and Thomas describe tracer bullets in The Pragmatic Programmer. In the CSV ticket, a skeleton that exported 200,000 rows badly on day one finds the timeout before the button is pretty.

3. Timebox every unknown. Half a day or a day per doubt, aimed at one question: is this the size I think it is? A spike that comes back with "bigger than we thought" did its job early.

4. Report proven and unproven. See the format below.

5. Move the date the day you learn. Not at sprint review. That day.

A status format that can't hide a 90% plateau

Task:      CSV export
Proven:    works end to end for normal accounts (tested on a staging copy)
Unproven:  very large accounts; a 200k-row export times out
Next:      1-day spike on a background job + expiring download link
Date:      Thursday if the spike holds. I'll know by tomorrow noon.

"Proven" means demonstrated in production-like conditions: real-shaped data, real permissions, the real integration. Merged isn't proven. Works locally isn't proven. The last line is the one a manager actually needs, because it says when the date will be confirmed or moved.

When someone asks "how far along are you?", the honest version sounds like: "The parts I understood are done. The part I didn't is the large-account export. I'll know how big it is by tomorrow."

The trade-offs (read before you sell this to your team)

  • It looks slower early. A skeleton demos worse than one polished screen. Say so before you start, not after someone asks.

  • Spikes can become rabbit holes. The timebox is the guardrail. When it expires, report and decide together.

  • Some doubt needs real traffic. Scale and vendor behavior sometimes only show up in production. A feature flag and a small slice of users is a legitimate way to spend that doubt early.

  • Pure known work doesn't need it. If a ticket has no question marks, order barely matters.

Why this matters past the ticket

Managers plan around dates, not code. A slip known on day two lets them cut scope or move a launch. A slip discovered on day nine breaks everything stacked on it. That's why the most trusted engineers aren't always the fastest; they're the ones whose bad news arrives early. And a proven/unproven trail is evidence of judgment, which matters because credit follows evidence, not effort.

Key Takeaways

  • Sorting work by comfort pushes uncertainty to the end, so "90% done" usually means "90% of the understood part."

  • Percent complete is calculated against a growing list, so it can fall, and usually falls late.

  • In skewed task-duration models, the highest-uncertainty task drives most of the expected overrun.

  • Padding and finer splitting don't help because they don't change when you meet the unknowns.

  • Do doubt first: doubt list, walking skeleton, timeboxed spikes.

  • Report proven, unproven, next step, and when you'll know.

FAQ

What is the 90-90 rule in software development?

The 90-90 rule, attributed to Tom Cargill of Bell Labs and popularized by Jon Bentley in 1985, says the first 90% of the code takes the first 90% of the time and the remaining 10% takes another 90%. In practice it describes how uncertainty collects at the end of a task when the understood parts are done first.

Why do software tasks get stuck at 90% done?

Because people naturally complete the parts they understand first, which leaves the unknowns for last. Those unknowns often split into several new tasks when opened, so real progress can drop even while the reported percentage stays at 90%.

How do I estimate tasks that contain unknowns?

Estimate the known work normally, list the unknowns separately, and timebox a short spike on each unknown before committing to a firm date. Give a conditional date ("Thursday if the spike holds") and say when the condition will be checked.

What is a walking skeleton in software engineering?

A walking skeleton is a minimal end-to-end implementation that connects every main layer of a system, a term associated with Alistair Cockburn. Building one first exposes integration and scale problems before time is spent polishing individual layers.

How should a software engineer give a status update?

Report what is proven (demonstrated in production-like conditions), what is unproven, what you are testing next, and the date along with when you'll know if it holds. This is more useful than a completion percentage because it shows where the risk is.

Does padding estimates fix late software projects?

Usually not. A flat buffer spreads extra time evenly, but schedule risk tends to sit in one or two uncertain tasks that can overrun by far more than the padding. Resolving those unknowns early is more effective than adding a uniform margin.

The bottom line

The last 10% was never the small part. It was the doubt you scheduled last. Move it to the front, report it plainly, and your dates will still move, just early enough to help.

A date that moves early is information. A date that moves late is a surprise.


New here? Start Here Get one of these in your inbox each week: subscribe to the Simply Explained newsletter

Adam Jaber is a software engineer who writes Simply Explained: complex topics, made simple. No jargon, no hype.