5 · Scale and cost¶
The first four chapters worked on samples. This one is about the 100,000-row night run: capping spend, surviving failures, and rerunning for free.
Belts before the run¶
smartpipe map "Extract {label}" --max-calls 500 < big.jsonl # hard spend ceiling
smartpipe map "Extract {label}" --dry-run < big.jsonl # see the request, spend nothing
SMARTPIPE_CACHE=1 smartpipe map "Extract {label}" < big.jsonl # identical calls are free
--max-calls stops intake when the billable-unit budget is spent and drains what's in
flight; ordinary model requests use one unit, while dedicated OCR reserves one
unit per PDF page before upload. A capped run never exits 0, so scripts notice. The cache keys on
model + request, so a rerun after a crash (or a prompt that didn't change)
replays answers without paying twice.
Batching: small items share one call¶
Small items don't each deserve their own HTTP call. By default, map,
extend, and filter collect items for a moment (about 75 ms, or until 12
are waiting) and send them as ONE request - each item in its own labeled
<input id="r1"> block, answered by one JSON object keyed per item. You do
nothing; XML-looking text inside an item is escaped as data, the run costs
less, and one stderr note discloses attempts and recovery:
note: batching: 500 items in 42 packed calls · 3 solo recoveries
What never batches: items carrying media (images, audio, video), oversized items that need chunking, repair retries, and every other verb - those take the same solo path as before. If an answer comes back missing or invalid for some item, that item alone is retried as a normal solo call (with the usual repair ladder); the rest of the batch keeps its answers.
Accounting stays honest: --max-calls counts real calls, so a batch of 12
items is 1 call - the cap stretches further. The cache still works per item:
cached items never enter a batch, and batched answers are cached individually
for later runs.
Turn it off with smartpipe config batching off (or per run:
SMARTPIPE_BATCH=off). SMARTPIPE_BATCH_SIZE accepts 2..12 and
SMARTPIPE_BATCH_WINDOW_MS tunes the wait.
Failures are rows, not mysteries¶
One bad item skips with a warning; the run continues; exit 1 says "partial". For dataset work, keep the failures instead:
smartpipe map "Extract {v}" --keep-invalid < rows.jsonl > out.jsonl
smartpipe where '__invalid == true' < out.jsonl > failures.jsonl
Each failure becomes {"__invalid": true, "__error": …, "__raw": …} - a
machine-readable set you can inspect, count, or rerun through a different
model explicitly.
When the provider goes down¶
Five consecutive availability failures at the actual-call boundary (an exhausted
rate-limit retry ladder, timeout, or 5xx) trip the circuit breaker:
the run stops early with a "provider looks down" screen instead of failing
the rest one by one. Work already done is safe, and rerunning is cheap with
the cache on. Tune with SMARTPIPE_BREAKER (0 disables).
Configure a fallback and the run doesn't stop at all:
smartpipe map "Extract {v}" --fallback-model gpt-5.4-mini < big.jsonl
At the threshold, smartpipe switches models wholesale, re-runs the failed window on the fallback, and the end receipt shows how many answers came from each model.
Throughput¶
--concurrency N (default 4) caps simultaneous outbound API calls, not the
number of items waiting to fill batches. With batch size 6, concurrency 1,
and 18 items, three calls run sequentially. Order is preserved regardless -
outcomes emit in input order, always.
Next: 6 · Pipelines that last - saving, wiring, and shipping what you built.