Foundations, lesson 18
Sync or batch
Blocking checks run sync; overnight work runs batch
The pre-merge check at Lumen Supply goes through the batch API, so developers may wait for hours with no promise of an answer. The nightly report runs sync at full price, so the bill is double for a wait nobody feels. This is the code at Lumen Supply, an online electronics shop. You will watch a plan send work down the wrong path, then fix it with one rule about who is waiting.
Who is waiting decides the path
Claude answers in one of two ways. The Messages API answers in the same call. The Batches API takes a list of requests, each with a custom_id, and answers later at half the price.
The plan decides the path, and the API runs it. A plan that picks wrongly fails in three ways.
A blocking check waits on a batch. A nightly job pays full price. A failed batch is rerun whole. The first two fail in a recorded run, and the third shows in the code and the fixed run.
A blocking check answers in the same call
The merge check is a job someone waits on, so the plan runs it sync. The request goes to the Messages API and the reply comes back in the same call. Open the card to read what Claude answered.
This is a real Claude Haiku 5.5 call, recorded once and replayed here. Nothing about it depends on how large the change is.
- JobThe pre-merge check on a change of 400 files. A developer waits for it before the merge…
Show the result
The pre-merge check on a change of 400 files. A developer waits for it before the merge button works.
- Planchose sync
- ClaudeFAIL: Skipping the customer lookup for tickets marked "verified" trusts an unchecked flag, so anyone who sets…
Show the result
FAIL: Skipping the customer lookup for tickets marked "verified" trusts an unchecked flag, so anyone who sets it could get refunds without identity confirmation.
The merge check answered in the same call. Nobody waited.
The starting plan sends the check to a batch
mode picks the path for a job, and resubmit picks what to send again. The starting mode has the rule backwards: it sends the blocking job to a batch and runs the overnight jobs sync.
The starting resubmit returns every custom_id, so one failed result sends the whole batch again.
The price is what makes this tempting. A batch costs half as much, so a check that runs on every change looks like the place to save. But a batch trades a wait for that price, and this check cannot wait.
How long will the merge check wait?
The starting plan has sent the merge check to the Batches API. A developer has pushed a change and is watching the merge button.
Predict: How long does the developer wait for the answer?
- AUp to 24 hours, with no promise of sooner.
- BA few seconds, because the change is small to Claude.
Show the answer
A. A batch trades a wait for half the price. It gives no latency promise, so a blocking check cannot rely on it.
The merge waits for a batch
The plan chose batch, so the check was submitted as a request with a custom_id and a batch to poll. The outcome line is the cost: the developer waits on a path with no latency promise. The last call sends the batch's one request again, because the starting resubmit returns every custom_id.
A batch may end sooner than its window, but nothing promises it. The window is up to 24 hours, and the API does not say when inside it.
- JobThe pre-merge check on a change of 400 files. A developer waits for it before the merge…
Show the result
The pre-merge check on a change of 400 files. A developer waits for it before the merge button works.
- Planchose batch
- Batches APIsubmitted 1 request: no latency promise, up to 24 hours
Show the result
[ "gate-1" ] - Batches APIsent again 1 request: gate-1
Show the result
[ "gate-1" ] - ClaudeFAIL: Skipping the customer lookup whenever a ticket claims the customer is verified lets anyone bypass…
Show the result
FAIL: Skipping the customer lookup whenever a ticket claims the customer is verified lets anyone bypass identity checks by simply asserting verification, which is a serious security and fraud risk.
The merge check went to a batch. It has no latency promise, so the developer may wait up to 24 hours.
Sync for blocking work, batch for the night
The fixed mode asks one question: is someone waiting? If so it runs sync. If not it runs batch, whatever the size of the job. The fixed resubmit reads each result's type and returns the custom_id of every result that did not succeed.
Follow the arrows. The expired result is matched to its request by custom_id, and only that request goes again. A batch whose results all succeeded sends nothing.
The same jobs, two plans
Switch between the starting plan and the fixed plan on the merge check. The card marked "This is where the runs split" is the plan's choice. One path answers now and the other answers later.
Then pick the nightly jobs. The starting plan runs them sync at full price, and the fixed plan runs them as a batch at half price.
This part is interactive in the lab: run ./workshop watch sync-or-batch.
Size does not decide the mode
Both nightly jobs are overnight work, 12 tickets and 10,000 orders. Both cards use the fixed plan, and the large one goes to a batch too. It is not special, because nobody is waiting on either.
Work out the window from the deadline. A batch ends within 24 hours, so it fits a report due in 30 hours and not one due in 6. Before a full run, tune the prompt on a small sample. A bad prompt on the whole job is paid for in full.
This part is interactive in the lab: run ./workshop watch sync-or-batch.
Try it: plans and jobs
Pick the starting plan on the merge check to see the wait, or on a nightly job to see the full price. Pick the fixed plan on the nightly tags to see one result sent again alone. The harness simulates that expired result, and only the resend is a recorded call.
Every combination is a recorded run, so this works with no internet.
This part is interactive in the lab: run ./workshop watch sync-or-batch.
The sync path in the Agent SDK
The Claude Agent SDK answers now, so it is the sync path. It has no batch. Each batch request is a single Messages call, so a multi-turn tool loop cannot run inside it.
For a CI job, claude -p is the same sync path from the command line. It runs once, prints its result, and lets the job read it. This is a real Claude Haiku 5.5 run.
- JobThe pre-merge check on a change of 400 files. A developer waits for it before the merge…
Show the result
The pre-merge check on a change of 400 files. A developer waits for it before the merge button works.
- Planchose sync
- ClaudeFAIL: skipping the customer lookup whenever a ticket merely claims verification lets anyone bypass identity…
Show the result
FAIL: skipping the customer lookup whenever a ticket merely claims verification lets anyone bypass identity checks and obtain refunds.
The merge check answered in the same call. Nobody waited.
You do: pick the path and resend
Copy the starting code and open work/sync-or-batch/plan.py. mode() sends overnight jobs down the sync path, and resubmit() sends every result again.
./workshop start sync-or-batch
./workshop check sync-or-batch
./workshop watch sync-or-batch
The checks want a blocking job on sync and a nightly job on batch, whatever its size. Only results that did not succeed are sent again, by custom_id.
Check your understanding
Answer each question, then read why every option is right or wrong. The wrong options are the mistakes engineers make on the job.
1. A pre-merge check must finish within 2 minutes or the developer stops waiting. It reviews a change of 400 files. Which way should it run?
- AAs a batch, because the batch price is half.
- BSync, so the answer comes back in the same call.
- CAs a batch, with the poll timeout set to 2 minutes.
- DAs several small batches, so each one ends sooner.
Show the answer
Answer: B
- A Half the price does not help a check that someone is waiting on. A batch trades a wait for the price.
- B Correct. A sync call answers now. A check that blocks a merge needs an answer now.
- C A poll timeout only stops your own loop. The batch still has no promise of finishing in 2 minutes.
- D Smaller batches are still batches. None of them carries a latency promise.
2. A report over 10,000 orders runs sync each night and is read at 8 the next morning. The bill is high. What is the root fix?
- AKeep it sync and shorten the prompt to cut the tokens.
- BKeep it sync, because 10,000 requests are too many for one batch.
- CKeep it sync, but start it at noon when the price is lower.
- DSubmit it as a batch, which costs half the price and can wait.
Show the answer
Answer: D
- A A shorter prompt trims a little from every request. The mode still pays full price for a wait nobody feels.
- B Size does not decide the mode. Nobody waits on the report, so it belongs in a batch at any size.
- C The price does not change with the hour. The half price belongs to the batch path.
- D Correct. Nobody waits on an overnight report, so the half price costs nothing you feel.
3. A nightly batch of 10,000 requests ends with 40 results that expired. How should the job recover?
- ASend again only the 40 requests, found by their custom_id.
- BSubmit all 10,000 again and keep the newer results.
- CFind the gaps by position in the results file and resend those lines.
- DDrop the 40, since 9,960 answered and the report is nearly whole.
Show the answer
Answer: A
- A Correct. Each result carries the custom_id of its request, so the failed ones can be found and sent again alone.
- B Sending everything again pays for 9,960 answers you already have.
- C Results can come back in any order, so a position says nothing. The custom_id is the link.
- D A silent gap in a report is a defect. The failed items still need an answer.
4. Select two facts about a Message Batches job.
- AIt has a 24 hour window and promises nothing faster.
- BIts results arrive in the order the requests were sent.
- CEach request is a single Messages call, so a multi-turn tool loop cannot run in it.
- DIt ends within the hour, which suits a check that blocks a merge.
Show the answer
Answer: A, C
- A Correct. The window is a limit, not a promise. A batch may finish sooner, and nobody can count on it.
- B Results can come in any order. A result is matched to its request by custom_id.
- C Correct. A batch request is a single call. A tool loop needs a call after each tool result.
- D There is no such promise. A merge-blocking check cannot depend on a batch.
5. A nightly code review of the whole repository is due in 30 hours. The window of a batch is 24 hours. Which path fits?
- ASync, because any deadline past 6 hours is too close to trust a batch.
- BHalf sync and half batch, so both halves end early.
- CBatch, submitted now, since its window ends before the deadline.
- DBatch, and tell the reader to expect it in 12 hours.
Show the answer
Answer: C
- A A 24-hour window fits inside 30 hours, so the batch is the cheaper path and still on time.
- B Splitting by size adds cost and complexity. The deadline already fits one batch.
- C Correct. Work out the window from the deadline. 24 hours is less than 30, so batch fits.
- D A batch makes no promise about 12 hours. Promise the reader only what the window guarantees.
6. The first trial of a batch prompt fails on 12 of 100 items. The full run is 50,000 items. What comes next?
- ASubmit all 50,000 and fix the failures from the results later.
- BFix the prompt, then test it on about 100 items.
- CRun all 50,000 sync so the failures show up right away, at full price.
- DResubmit the 12 failed items until they pass.
Show the answer
Answer: B
- A Every bad answer in the full run is paid for, and the prompt fault repeats across thousands of items.
- B Correct. A small sample shows the fault cheaply. The full run starts when the sample is clean.
- C A sync run at this size pays full price to find a fault a small sample would show.
- D The 12 failed because of the prompt. Sending them again fails the same way.
7. A CI job reviews a change and its result decides whether the deploy step runs. How should the job call Claude?
- ASubmit a batch, because the tool loop the review needs runs there.
- BSubmit a batch and poll it, since it usually ends within the hour.
- CSubmit a batch and deploy when it ends, since that halves the cost.
- DRun it sync with claude -p, and read the result in the job.
Show the answer
Answer: D
- A A batch request cannot run a multi-turn tool loop, so it is the wrong home for an agent review.
- B A batch has no latency promise. The deploy step could wait for up to a day.
- C The half price is real, but the deploy waits on this result. The saving is not worth the wait.
- D Correct. A blocking job needs its result now. claude -p runs once and returns it to the job.
8. Select two rules for choosing the path of a CI job.
- AWork that blocks a merge or a deploy runs sync.
- BA job of more than 1,000 items runs batch, blocking or not.
- CWork that is read the next morning runs batch.
- DEvery job runs batch, because the half price is the point.
Show the answer
Answer: A, C
- A Correct. Someone is waiting, and only a sync call answers when they need it.
- B Size does not decide the mode. A large blocking check still needs an answer now.
- C Correct. Nobody feels the wait, so the half price is free.
- D The half price comes with a wait and no latency promise. A blocking job cannot pay that.
Play: predict the output
This rule picks the path by how many requests a job sends.
def mode(job: Job) -> str:
return "sync" if job.size < 100 else "batch"
Predict: The nightly tagging job sends 12 requests, and the report is read the next morning. Which path does it take?
- ASync, at full price, though nobody reads the result until morning.
- BBatch, because it runs overnight.
Show the answer
A. Size says nothing about who is waiting. Send a job sync only when someone blocks on it, and batch everything that can wait, whatever its size.
References
- Batch processing: result types, the 24-hour window and how custom_id maps results back.
- Run Claude Code headless: the sync path a waiting person needs.
- Pricing: what the batch discount saves on a nightly job.
Conclusions
- A job someone waits on runs sync. A batch has no latency promise.
- Overnight work runs batch at half price, whatever its size.
- A failed result is found by its custom_id and sent again alone.
You can now choose sync or batch for a job from who is waiting, and recover a batch by custom_id.
Batch
A list of requests the API answers later, at half the price, with no latency promise
custom_id
The label on each batch request that matches its result back to it
Blocking job
Work someone waits on before they can go on
Resubmit
Sending again only the requests whose results failed