← writing

Work in Progress Creates Complexity

· 8 min

Work in progress creates complexity. This complexity does not come from how large a system is or how much it eventually ships. It comes from how much work is unfinished at any given moment. The best way to keep a system simple, whether it is software or a company, is to limit how much work is in progress. That means refusing to take on more work than you can finish.

Work is in progress as soon as a person or system takes responsibility for finishing it. This can be a request running against an integration, a webhook waiting to be processed, a PR waiting for review, an engineer juggling several topics, or a company running many initiatives at once.

Why work in progress is where complexity lives

Every unfinished item carries state: what has already happened, what still needs to happen, whether the original context is still valid, who owns the next step, what it depends on or blocks, and whether it is still worth finishing.

You have to store, order, protect, track, and reason about that state. That is the complexity. A system’s complexity depends on how much work is in progress, not how much work exists in total.

It is not always clear when work becomes work in progress. The moment you write down a ticket, it is already in progress to some degree. But before anyone commits to it, you hold almost no state. There is nothing to revisit, coordinate, or keep alive, so the complexity is low. It grows as you take on the work and it starts accumulating state.

Waiting makes this worse because context gets stale. Assumptions change, dependencies move, customer needs disappear, and later work invalidates earlier work. Even something you eventually throw away costs you while it waits. Someone has to remember the context, check its status, and decide whether it is still valid. Items also interact with each other, so complexity can grow faster than the number of items.

Three things matter:

  • Rate: how quickly new work enters.
  • Throughput: how quickly finished work leaves.
  • Work in progress: everything admitted but not yet finished.

Every system has a bottleneck. Adding work before the bottleneck does not give it more capacity. It just creates a queue. Little’s Law describes this: in a stable system, work in progress equals throughput multiplied by cycle time. Rearranged:

cycle time = work in progress / throughput

If throughput stays the same and work in progress doubles, the average time to finish something roughly doubles. Once the queue itself creates contention and coordination, throughput also drops, so it gets even worse.

Reject work instead of managing it

Most systems respond by trying to manage this state better. In software, you add queueing infrastructure, retries, deduplication, replay, and observability. In organizations, you add prioritization, more planning, more product managers, and a bigger backlog. You end up building an ever larger machine just to hold unfinished work.

But the work does not disappear. It waits somewhere, and someone has to hold it while it waits. That is the real choice: who holds the unfinished work, you or whoever produced it. Managing the state means you quietly volunteer to hold all of it. Backpressure means the producer feels the limit and holds the work instead, until you are ready for it.

How you apply it depends on whether you control the producer. When you do, you slow it down. A consumer that pulls new work only when it has capacity, or a fetch step that waits when the database is busy, keeps work from piling up faster than it can be finished. The excess stays at the source as work that has not started, which costs almost nothing.

When you do not control the producer, you refuse the excess. You return an overload signal and let the producer keep the work and retry later. It stays the owner, and you never take on state you cannot finish.

The shape of the work matters too. If you discover everything before you finish anything, you maximize how much is open at once. If you go depth-first, finishing and clearing one unit before you start the next, the amount in progress stays small on its own, without any explicit limit.

All of this keeps throughput at the capacity of the bottleneck without taking on the state that piles up when you accept work you cannot finish yet.

Software: the sync engine

At Scopas, our previous company, one scraping run expanded into millions of small jobs. The queue accepted work much faster than the APIs, workers, and Postgres could finish it. Slow runs overlapped with later runs, which increased the load and slowed everything down even more. Retries added more work to an already overloaded system. We had to reason about the age, order, and validity of millions of unfinished jobs. We built distributed queues, custom job tables, and partial-index tricks to keep the system alive. All of this complexity came from work we should never have created.

At Kombo, we do the opposite. One linked-account sync runs in a single process. It goes depth-first through the data, stores completed results quickly, and only keeps its current working set in memory. A scheduler prevents overlapping runs and limits global concurrency. When Postgres slows down, fetching slows down with it. There is no distributed queue because there is almost no work in progress to coordinate. We avoid the complexity by not creating the work in progress in the first place.

The same thinking is behind our limit on action concurrency. A customer might send 600 requests per minute to decline candidates, while the underlying tool only accepts around 50 every 10 seconds. Accepting all requests creates retry loops, ties up memory and connections, and can leave multi-step actions half completed. Once too many requests are in flight, Kombo returns an overload response and the customer retries later. The downstream tool sets the actual throughput either way. Rejecting early tells the customer immediately that we did not accept the request.

There is one important exception. Sometimes you cannot control the producer and cannot drop work. Examples are event streams and analytics, where events happen independently of your capacity and dropping a random slice would corrupt the result. In that case, you need a queue and intentionally take on storage, ordering, retries, poison messages, replay, and recovery. A queue does not remove the mismatch between rate and throughput. It turns the mismatch into a state-management problem you have chosen to own. When you control the producer, limiting work in progress is simpler.

The product backlog

This is not only about software. Software is just where it is easiest to see. The same thing happens in how an organization handles work.

A ticket is already work in progress. Writing one down turns a piece of evidence into an expectation that someone will get the work done. Nobody is coding it yet, but the company still owns it. Someone has to hold it, weigh it against everything else, check whether it is still relevant, and decide when it happens.

Most companies have a ticket backlog. Anyone can add to it, so it grows to tens of thousands of items. Eventually, you need a whole product organization whose main job is to manage the backlog: prioritize items, check whether they are still relevant, deduplicate them, and decide what comes next. This is the organizational version of investing in a bigger queue.

At Kombo, we do not keep a ticket backlog, but we still get the most important work done. Writing down an idea is cheap and useful, but it does not mean engineering has promised to finish it. Important work comes up again on its own. Most items can wait, be dropped, or come back later with stronger evidence. We do have an evidence backlog where team members can capture requests. They are there for AI to find if we decide to work on a problem, not as commitments for engineering to finish. By not committing to every request, we keep the number of things we actively hold small. Planning is when a few possibilities become real commitments. There will always be more good ideas than capacity, so every yes means saying no to something else.

People and teams

The same principle applies at the smallest scale. A PR waiting for review is unfinished work, and review is the bottleneck. If people keep opening changes while PRs wait, the review queue grows and less code reaches production. The fix is to limit work in progress: write small PRs that a reviewer can hold in their head, find a reviewer, and get them merged before starting the next change.

The same is true for initiatives. An engineer should work on one major thing at a time because working on several things in parallel creates stale assumptions and coordination costs. A leader who holds on to every responsibility becomes the bottleneck that the whole organization queues behind. The job is to give others ownership quickly and keep only a few things in your own hands.

What it comes down to

Complexity depends on how much work is in progress, not how much work exists or how much you finish over time. You do not reduce complexity by getting better at managing state because the machinery for managing state is the complexity. You reduce it by taking on less work, finishing things before starting new ones, and trusting that important work will come back.

I really like the videos in this blog post by John Cutler. They explain how additional work in progress creates organizational complexity.