PromptABCD
FeaturesLearnHow it worksUse casesFAQGuideBlogContext Blocks
Sign inGet started free
Sign inSign up
PromptABCD

A calm home for your best AI prompts. Save them once, find them in seconds, reuse them forever.

Product

  • Features
  • Chrome Extension
  • Free Courses
  • How it works
  • Use cases
  • Blog
  • Context Blocks
  • Export Anywhere
  • FAQ

Resources

  • User guide
  • Learn prompting
  • Sign in
  • Get started free

© 2026 PromptABCD. All rights reserved.

Privacy PolicyTerms and Conditions
Home/Blog/AI Harness/Concurrency in an Agent Harness: A Case Study
AI Harness

Concurrency in an Agent Harness: A Case Study

Agent harness concurrency breaks on shared mutable state, not on parallelism. A case study on the one-line class-attribute bug that leaked context between runs.

September 1, 2026·8 min read
ShareShare
⚡Featured Prompt— copy and use right now
class Agent:
    messages = []          # class-level — SHARED across all instances

    def __init__(self, model, tools):
        self.model = model
        self.tools = tools

    def run(self, task):
        self.messages.append({"role": "user", "content": task})
        for _ in range(10):
            reply = self.model.complete(self.messages, self.tools)
            # ... appends more to self.messages
        return extract(self.messages)

Most advice about running agents concurrently focuses on the wrong problem. It tells you how to spin up parallel workers and bound them with a semaphore — the easy part — while barely mentioning the failure that actually bites: shared mutable state leaking between concurrent runs. Agent harness concurrency goes wrong not because parallelism is hard to start, but because a harness that worked perfectly for one agent at a time quietly corrupts itself when ten run at once. This case study is about a team that learned that the expensive way, and the one-line design flaw at the center of it — a flaw that passed every test they had right up until they turned on parallelism.

The Problem a Team Faced

A team built a batch-processing feature: an agent that classified support tickets, run across thousands of tickets a night. Sequentially it worked flawlessly. To hit their nightly window, they made it concurrent — a pool of workers, each running the same agent on a different ticket.

That's when the results went strange. Occasionally a ticket would get a classification that clearly belonged to a different ticket. Ticket A's agent would reference details from ticket B. The outputs were being cross-contaminated, but only sometimes, and never reproducibly. Sequential runs were clean; concurrent runs leaked, and the difference between the two was the whole clue nobody could read yet.

The lead engineer's first assumption was a race condition in the database. It wasn't. The bug was in the harness itself, and it was the kind of thing that's invisible until concurrency exposes it. "We wrote the harness assuming one run at a time," she said, "and nothing in it said so out loud." That silent assumption is the heart of most agent concurrency bugs.

The Wrong Approach

Here's the shape of the harness that leaked. Read the class carefully:

hljs python
[object Object], ,[object Object],:
    messages = []          ,[object Object],

    ,[object Object], ,[object Object],(,[object Object],):
        ,[object Object],.model = model
        ,[object Object],.tools = tools

    ,[object Object], ,[object Object],(,[object Object],):
        ,[object Object],.messages.append({,[object Object],: ,[object Object],, ,[object Object],: task})
        ,[object Object], _ ,[object Object], ,[object Object],(,[object Object],):
            reply = ,[object Object],.model.complete(,[object Object],.messages, ,[object Object],.tools)
            ,[object Object],
        ,[object Object], extract(,[object Object],.messages)

What this does — and this is the bug — is declare

messages
as a class attribute, not an instance attribute. Every
Agent
object shares the same list. Run one agent at a time and it looks fine because runs don't overlap. Run ten concurrently and they all append to the same shared
messages
, so ticket A's conversation and ticket B's conversation interleave into one corrupted history. The cross-contamination was ten agents scribbling in one shared notebook.

The insidious part is that this passes every sequential test. The bug is dormant until concurrency wakes it, which is exactly when it's hardest to reproduce — intermittent, timing-dependent, and gone the moment you slow things down to debug. A print statement or a debugger, by serializing execution, makes the very interleaving that causes the bug stop happening, so the tools you'd reach for to find it are the tools that hide it. That's what made this take days: every attempt to observe it made it disappear.

⚠️ Common mistake: Storing per-run state anywhere shared across runs — class attributes, module-level globals, a single reused client object with mutable state. A harness written and tested one-run-at-a-time can hide this indefinitely, because the bug only appears when runs overlap. Before you make anything concurrent, audit every piece of state your harness touches and ask: is this per-run, and is it actually isolated per run? Shared mutable state is the single most common source of concurrency corruption in agent harnesses.

The Correct Approach: Isolated State for Agent Harness Concurrency

The fix is to make every run own its state completely — nothing shared, nothing mutable across runs.

hljs python
[object Object], ,[object Object],:
    ,[object Object], ,[object Object],(,[object Object],):
        ,[object Object],.model = model
        ,[object Object],.tools = tools

    ,[object Object], ,[object Object],(,[object Object],):
        messages = [{,[object Object],: ,[object Object],, ,[object Object],: task}]   ,[object Object],
        ,[object Object], _ ,[object Object], ,[object Object],(,[object Object],):
            reply = ,[object Object],.model.complete(messages, ,[object Object],.tools)
            ,[object Object],
        ,[object Object], extract(messages)

What this does: it moves

messages
into the
run
method as a local variable, so every call gets its own fresh, isolated conversation. Nothing is shared between concurrent runs because the state lives on the call stack, not on the object or the class. Ten agents now scribble in ten separate notebooks. The fix is small — one variable moved — but it's the difference between correct and corrupt under load.

With state isolated, safe concurrency is straightforward:

hljs python
[object Object], asyncio

,[object Object], ,[object Object], ,[object Object],(,[object Object],):
    sem = asyncio.Semaphore(max_concurrent)
    ,[object Object], ,[object Object], ,[object Object],(,[object Object],):
        ,[object Object], ,[object Object], sem:
            ,[object Object], ,[object Object], make_agent().run_async(task)   ,[object Object],
    ,[object Object], ,[object Object], asyncio.gather(*(one(t) ,[object Object], t ,[object Object], tasks))

What this does: it runs tasks concurrently but caps how many run at once with a semaphore, and it builds a fresh agent per task so no state is shared. The semaphore bounds concurrency to respect rate limits; the fresh-agent-per-task guarantees isolation. Both matter, and the isolation is the one people forget.

Notice the

make_agent()
call inside the loop rather than a single agent passed in. That's deliberate and load-bearing: constructing a new agent per task is what guarantees each run starts from clean state. Passing one shared agent into every task — the natural, tidy-looking thing to do — reintroduces exactly the sharing you're trying to eliminate. The slightly less elegant "build a fresh one each time" is the version that's actually correct under concurrency, and the small cost of constructing an agent per task is nothing next to the cost of debugging cross-contamination.

Results and What Changed

Once state was isolated, the cross-contamination vanished completely. The team reran the batch that had produced mysterious mismatches and got clean, correct results every time. The fix was a few characters; finding it had taken days, because the symptom pointed everywhere except the actual cause.

The deeper lesson reshaped how they built harnesses. They adopted a rule: a harness must be safe to run concurrently by default, even when today's use is sequential, because "we'll never run this in parallel" has a way of becoming false. Writing state as per-run from the start costs nothing and removes an entire class of future bug. Their new harnesses had no shared mutable state anywhere, by policy.

The rule paid off in an unexpected place: testing. A harness with no shared mutable state is also a harness whose runs are independent, which means their eval suite could run its tasks in parallel and trust the results — the same isolation that fixed the production bug made their tests faster. Correctness under concurrency and speed under concurrency turned out to be the same property, bought once. That's the quiet payoff of designing for isolation early: it's not just insurance against a future bug, it's a capability you get to use immediately.

⚡ Pro tip: Choose async over threads for agent concurrency. Agent work is overwhelmingly IO-bound — waiting on model and tool API calls — which is exactly where async shines, letting one process juggle hundreds of in-flight agents cheaply. Threads work too but carry more overhead and more room for subtle sharing bugs. Async's explicit

await
points also make the "where does this pause" question easy to answer.

⚡ Pro tip: Bound concurrency to your rate limit, not your CPU. The constraint on parallel agents is almost never compute — it's the provider's requests-and-tokens-per-minute cap. Size your semaphore to fit under that shared limit, and coordinate it with your rate limiter, or you'll launch a hundred agents that all get throttled at once.

How to Apply This to Your Situation

The isolate-state principle holds across every concurrent agent workload:

  • A data engineering team running thousands of extraction agents nightly makes every run fully state-isolated and bounds concurrency to their token limit, finishing in their window without cross-contamination.
  • A customer-support platform handling many simultaneous live conversations gives each conversation a fully independent agent instance, so no two users' contexts can ever touch.
  • A research group running large parallel evaluations isolates every eval run's state, so a batch of a thousand concurrent evaluations produces the same results it would one at a time — which is the entire point of an eval.

Across all three, the payoff of isolation is the same: concurrency stops being a source of correctness risk and becomes pure speed. You get the throughput of running everything at once with the reliability of running everything alone, which is the outcome that made parallelism worth reaching for in the first place.

The move is always the same: no shared mutable state, fresh state per run, and a semaphore sized to your rate limit rather than your cores.## Next Steps

Audit your harness today for state that outlives a single run — class attributes, module globals, reused mutable clients. Ask of each one: what happens if two runs touch this at the same time? If the answer is "corruption," move it to per-run scope before you ever go concurrent. It's a cheap change now and an expensive, intermittent bug later.

⚡ Pro tip: Test concurrency deliberately, don't wait for production to find the bug. Write a test that runs the same agent on ten different inputs concurrently and asserts each output matches what that input produces alone. Shared-state leaks show up immediately under this test and are invisible under sequential ones — so the test is the difference between finding the bug on your laptop and finding it in a customer's cross-contaminated results.

Safe agent harness concurrency comes down to isolation: every run owns its state, and nothing mutable is shared. The concurrency patterns and the semaphore-sizing rules you settle on are reusable across every batch and parallel workload you build. Keeping those patterns — and the prompts your concurrent agents run — organized in a library like PromptABCD means your next parallel workload starts isolated and rate-aware by default, instead of hiding a shared-state bug that waits for load to reveal itself.

agent harness concurrencyconcurrencyasyncai agentsshared statecase study

Continue Reading

Managing the System Prompts Your Harness Injects
AI Harness

Managing the System Prompts Your Harness Injects

Harness system prompt management stops the one-word edit that silently breaks every task. Learn to version prompts, gate changes on evals, and review every diff.

September 1, 2026·8 min read
Cost Tracking in an AI Agent Harness
AI Harness

Cost Tracking in an AI Agent Harness

Agent harness cost tracking shows why the bill tripled — usually context re-sending, not run count. Learn per-step tracking, ceilings, and cache-friendly prompts.

September 1, 2026·8 min read
Rate Limiting Inside the Agent Harness
AI Harness

Rate Limiting Inside the Agent Harness

Agent harness rate limiting done reactively makes overload worse. Learn proactive, token-aware limiting that stops the 429 retry storm before it starts.

September 1, 2026·9 min read

Save the prompts from this post

PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.

Start free →
← PreviousCost Tracking in an AI Agent HarnessNext →Managing the System Prompts Your Harness Injects
Share this post:
ShareShare