DEV Community

Jeff
Jeff

Posted on Originally published at powerduck.com

When the Agent Fires Parallel API Calls: Out-of-Order Writes and Rate Limits

Quick answer: Agents love to fire several API calls in parallel because it feels fast, and for independent reads it is, but parallel writes against the same resource are a race: two requests can act on stale reads, create duplicate records, or hammer a rate limit and then all retry together. The fix is to be deliberate about which calls are genuinely independent and therefore safe to parallelize, and to serialize anything that touches shared state, with bounded retries and jitter so a throttled batch does not stampede. The performance win from parallel calls is real, but it only applies when the operations have no ordering dependency; once they do, concurrency is how a clean-looking agent run produces conflicting data and a cascade of 429s.

The model does not naturally distinguish "these happen to be separate calls" from "these calls are independent." Both look like a list of requests it can issue together, and tooling that encourages Promise.all makes the parallel path the default. The trouble arrives when the operations share a resource: two updates that each read the current record, then write a computed value, will both overwrite each other, and a burst of ten requests can trip a per-second limit that ten sequential requests would never hit.

Where parallel calls go wrong

The failure modes separate cleanly into data races and rate-limit effects, and conflating them is part of the problem.

Pattern What happens Correct treatment
Parallel writes to one record Lost updates, last write wins Serialize or use optimistic concurrency
Parallel creates Duplicate records Idempotency keys
Burst of reads Shared rate limit tripped Cap concurrency
Synchronized retries Thundering herd after 429 Backoff with jitter
Independent reads Safe and faster Parallelize freely

Independent reads are the one row where parallelism is strictly good. The discipline is to prove independence before choosing that path, rather than assume it, and to treat any write or shared counter as a reason to limit concurrency.

Separate independent reads from dependent writes

Structure the flow so reads that do not affect each other run together, and then run any writes that depend on those results in a controlled sequence. Making the dependency explicit is what prevents the agent from flattening everything into one batch.

The reads share no state and can be capped with a simple pool if there are many, while the writes happen one at a time after a single reconciliation. That keeps the speed win without exposing shared resources to concurrent mutation.

Use idempotency and optimistic concurrency

When a write can be retried or duplicated, attach an idempotency key so the server collapses repeated attempts into one result, and use a version field so a write against a stale read is rejected rather than silently overwriting a newer value.

A 409 on a version mismatch is the system telling the agent its read was stale, which is the correct signal; blindly retrying the same write would reintroduce the race. The safe response is to re-read, recompute, and write again.

Bound retries with jitter and a concurrency limit

Against a 429, every request in a failed batch backing off the same fixed amount will retry together and fail again. Use exponential backoff with randomized jitter and keep the number of in-flight requests below a limit that respects the API's documented rate.

A spec-driven client that knows the rate limit from the contract can apply these defaults consistently rather than leaving concurrency and retry to whatever the agent improvises in the moment. This is the larger point: parallelism and throttling are contract properties, and they should be handled once at the client layer rather than re-decided on every run.

Frequently asked questions

Are parallel API calls always bad?

No. Independent reads are both safe and faster in parallel. The risk appears with writes to shared resources, creates that can duplicate, and large bursts that exceed a rate limit.

How do I know if two calls are independent?

They are independent if neither writes a resource the other reads or writes, and neither's inputs depend on the other's output. If the answer is unclear, treat them as dependent and serialize until the contract proves otherwise.

What is the right response to a 429 in a batch?

Back off exponentially with jitter so the requests do not retry in lockstep, reduce concurrency, and respect the documented limit. A fixed shared delay recreates the same burst.

How do I prevent duplicate records on retries?

Send an idempotency key on creates so the server recognizes a repeated attempt and returns the original result, rather than creating a second record.

How does the API contract help here?

It documents rate limits, idempotency, and version fields, letting a generated client enforce concurrency caps, jittered retries, and optimistic concurrency consistently instead of relying on ad hoc agent behavior.

This is part of the Powerduck vs Cursor comparison — start there for the full picture of keeping agent-driven API work reliable.

Top comments (2)

Collapse
 
alexcodebytes profile image
Oleksandr •

The 'prove independence before choosing that path' is the key discipline. Most agents default to parallel because it feels faster, but they don't stop to ask whether the calls actually share state. The table makes the distinction very clear.

Collapse
 
freedailypro profile image
FreeDailyPro •

Good breakdown. The point about not retrying a 409 blindly is one I see missed a lot.

One thing I'd add from running an API that agents hit in bursts: if the server sends rate-limit headers on every response (remaining, reset, and Retry-After on a 429), the client can pace itself before it ever gets throttled. A small token bucket on the client side that reads those headers works much better than a fixed concurrency number guessed from the docs, because the real limit is often shared across keys or changes by plan.

On idempotency keys, it's worth deciding what happens when the same key arrives with a different body. Returning an error there (instead of replaying the first response) catches the case where the agent reused a key for what is really a new write.

Do you scope the concurrency cap per resource or per API key? I've gone back and forth on that for agents that touch several resources in one run.