The Serial-Default Trap
Articles
2026-08-278 min read

The Serial-Default Trap

A folder of documents is still crawling through its queue hours after the result stopped needing human attention. Nothing has crashed. The outputs are good. The machine is mostly idle and the service is answering promptly. The delay comes from a quieter decision: each item waits for the previous item to finish, although the items do not depend on one another.

Is that the serial-default trap? Mostly, and unevenly, in the sample we have. A small blind study found one-at-a-time structure in 14 of 18 generated Python programs in each of two matched arms. Yet one of the three coding agents produced concurrent structures for four of its six assignments in both arms. The default is not universal, and it may already be shifting. It is still common enough to deserve examination.

Each item waits for the previous item to finish, although the items do not depend on one another.

The revealing part is not that a serial loop is primitive. It is that the smallest correct program carries a sensible local safety property into places where that property has no jurisdiction.

What was actually measured

The controlled study was run on 16 September 2026. Claude, Gork and Koda each received six independent bulk assignments: calling an API over documents, retrieving URL titles, looking up postcodes, making thumbnails, calculating checksums and transcoding media. The first three were network-bound and the latter three ordinary compute-bound assignments. Here, compute-bound means familiar local work such as extraction, parsing and image processing—not running a machine-learning model on a CPU.

There were two 18-cell arms, one program for every agent-and-assignment pairing in each. Subjects saw no item counts, performance framing or implementation hints. One arm ran in the operator's normal environment. The other ran in a clean room with hooks, MCP connections, automatic memory and discovered project guidance disabled.

A static syntax-tree audit counted a program as concurrent only when it constructed an executor and dispatched independent units through it. It recorded batching separately. Fourteen of 18 programs were serial in each arm, 78% when rounded, and all 18 corresponding cells agreed between arms on the serial-or-concurrent classification. That agreement makes the tested environmental differences an unlikely explanation for the pattern in this sample; it does not eliminate every environmental or prompting effect.

The unevenness matters. In each arm, Claude emitted concurrent structure for four of six assignments and serial structure for checksums and postcodes. Gork and Koda emitted serial structure for all six. Claude used thread pools for network or subprocess work and a process pool for thumbnails. These are structural observations, not measured speed claims.

This is not a benchmark or a league table. The programs were not run against workloads. There was no timing, quality score, success-rate comparison or general test of vendor capability. Codex was not a subject. The sample contains 36 programs, is small, non-random, Python-only and fixed in time; 78% is a description of these arms, not a market rate. The operator says he has noticed the same pattern in Swift, but that is testimony, not evidence supplied by this study.

The eight concurrent programs—the same four cells repeated across two arms—are therefore not noise to sweep away. They show that generated code can discriminate among kinds of work and can selectively pay the coordination cost. The measured picture is safety-first simplicity persisting strongly, with a real but uneven departure from it.

Safety that leaks out of scope

A plain loop is attractive for good reasons. It has one control path. Exceptions arrive directly. It retains little state. It does not need to account for several failures, cancellations or retries at once. Because it contains no pool, executor, dispatcher or outstanding-task state, it silently sets both active work and submitted work to one. The standard library's own executor interface makes those additional mechanisms explicit.

That simplicity explains the mechanism. It does not certify the decision.

For independent units, a global bound of one is justified only if some relevant constraint actually requires it. Otherwise a safety property that belongs to one operation has spread over the whole queue. We can see the error most clearly in a two-step item:

For item i, produce A, then use A to produce B.

The order Aᵢ → Bᵢ is real. It says nothing about item j. A worker can preserve both steps as the atomic body it claims while other workers process j and k. This is fan-out: several independent units assigned across a bounded group, a pattern illustrated concretely in the Go pipelines account of fixed worker groups. It is not permission to launch the entire queue at once.

Two panels. Left: A points to B. Right: input feeds a pool, a scheduler fans out to three A-then-B units.
Inside one item, A then B is a real chain. Across items it is fan-out: a pool, a scheduler, and N copies of that same two-step body.

The same scoping applies at the destination. A sink may genuinely admit only one writer. SQLite, for example, serializes writes while still allowing multiple readers. That can earn a narrow sequence: prepare independent results with overlap, then funnel them through one writer. Per-key order can likewise coexist with work across other keys; systems that preserve order within partitions show why local order need not become global serialization.

Seriality is earned when there is a true global order, a one-writer boundary, a tested safe capacity of one, an effect that cannot safely be retried, or a deliberate limit on resident state, failure exposure or committed spend. Those are serious reasons. But each constraint should govern the boundary it owns, not unrelated fetching, extraction or preparation upstream.

A batch is not an overlap

The postcode assignment exposed a second scope mistake. All six postcode programs found a bulk facility and grouped lookups into batches. Claude and Gork also deduplicated inputs in both arms; Koda did not. Every program then issued those batches one after another.

That is a competent optimisation and a serial program.

Serial, batching and concurrent work answer different questions. Serial means one unit finishes before the next begins. Batching combines several units into one operation, reducing the number of operations or round trips; it does not by itself make two operations overlap. Concurrency means more than one activity is in progress. Strict parallelism means simultaneous execution, so when physical simultaneity is the point we should say CPU parallelism. Concurrency can be useful even when much of each activity is waiting.

These axes can be combined. A hundred batches issued one by one are batched and serial. Several batches may be in flight concurrently. Individual units can also be concurrent without being batched. Even database documentation warns, in a different setting, that pipelining outstanding operations does not imply parallel server execution. “We batched it” is not yet an answer to “was more than one independent operation in flight?”

Derive the boundary; do not guess a number

The alternative to an accidental one is not infinity. It is bounded scheduling: place each constraint where it belongs, then admit only as much work as the tightest constraint permits.

We need at least two numbers before we begin. The active-worker bound limits how many units can execute at once. The outstanding-work bound limits how many can be submitted or retained, including work waiting for a worker. Conflating them can produce a modest worker pool backed by an enormous memory commitment. Executor APIs expose both worker and, in current Python, submission-buffer controls for precisely this separation; their defaults are implementation choices, not evidence about our workload.

Then we derive candidate ceilings separately:

  • correctness: what can overlap without violating order, ownership or effects?
  • capability: can this substrate perform the operation at all?
  • capacity: what sustained load has representative testing shown to be safe?
  • rate and downstream limits: which request, token, queue, writer or dependency ceiling is exhausted first?
  • exposure: how many retries, irreversible effects, resident objects or units of spend can we tolerate at once?
  • priority: what capacity must remain for other work?

A rate-limit response cannot do this reasoning for us. HTTP 429 means the client has sent too many requests in a given time and may include Retry-After, but the standard does not define the provider's accounting scope. Metered services may enforce request, token, queue and spend limits independently, as the separate ceilings in the OpenAI and Anthropic documentation make plain. The first applicable ceiling binds.

Take the limiting candidate, reserve headroom for shared capacity, and ramp under representative load. Stop where useful throughput or safety worsens. This is why load-tested capacity and maximum concurrency belong in the same reliability discussion. A published maximum is permission, not a safe target; a default of one is a starting behaviour, not a workload finding.

Ownership is not the axis

We also need to stop treating owned capacity as naturally serial and metered capacity as naturally concurrent. Either can rationally settle at one or at many. What differs is how its ceiling is derived.

For capacity we own, the bound comes from capability, availability, contention, priority and urgency. The operator may be away while the machine is available; those are not the same condition. Machine speed affects how quickly each unit completes, while operator availability affects when intervention is possible. The capital expenditure is already spent, but scarce capacity may still be reserved for more important work.

For capacity we meter, the bound comes from terms, request and token rates, queue limits, spend caps and per-call expenditure. It may be perfectly sensible to serialize costly or irreversible calls even when the service permits overlap. Conversely, an owned machine may process many independent units at once. Ownership describes the ledger, not the scheduling shape.

Latency, throughput and cost must stay separate too. More in-flight work can hide waiting and improve throughput without making any individual request faster. It can also increase memory, contention, tail latency, retries and committed spend. Batching may lower operation count without changing overlap. Asynchronous batch products can trade a longer completion window for a different price and rate pool—current examples from OpenAI, Anthropic and Google are provider-specific terms retrieved on 16 September 2026, not timeless process laws.

The useful question is therefore not “Why didn't the agent make this parallel?” It is: what invariant set the in-flight bound, at what scope, and from what evidence?

Sometimes the answer is one. Often it will be a cautious number above one. What makes the serial default a trap is not caution itself, but caution escaping its proper boundary and becoming an unexplained global wait.

The fresh study gives us a narrower and more interesting conclusion. Generated serial code was the majority result, but not the only result. The next step is neither to celebrate concurrency nor to condemn loops. It is to make every bound answer for its jurisdiction.

1,910 words · 8 min read