Skip to content

Jobs, shards and tasks

Three words that are easy to confuse, and the tuning knob that matters most.

  • A job is what you submit: a list of items, a type, and a model.
  • A shard is a contiguous slice of those items, sized by shard_size.
  • A task is a shard's row in the queue — the unit that gets leased, retried, and reassigned.

One job of 500 items with shard_size=32 becomes 16 tasks.

Job to result500 items → 16 tasks → flattened results
1 · items500 items · 1 cell ≈ 5shard_size= 322 · shards16 tasks, one row in `tasks`lease3 · workerspull up to lease_limitworker 1 embed shard locally worker 2 embed shard locally worker 3 embed shard locally complete_batch4 · resultsshards gathered, then flattened results[] — one entry per shard task_id, worker_id, duration_ms items[] — flat, 500 embeddings this is what consumers read GET /v1/jobs/{'{'}job_id{'}'}/results

Submitting

json
{
  "type": "embed",
  "model": "nomic-embed-text",
  "shard_size": 32,
  "items": [
    { "id": "doc-1", "text": "first chunk" },
    { "id": "doc-2", "text": "second chunk" }
  ],
  "meta": { "anything": "you want echoed back" }
}

shard_size accepts 1–2048 and defaults to 32. items must be non-empty.

The response tells you how the job was split:

json
{ "job_id": "…", "n_items": 500, "n_tasks": 16, "n_shards": 16, "shard_size": 32, "status": "running" }

Choosing shard_size

This is the highest-leverage number in the system, and the two forces pull opposite ways.

Small shardsLarge shards
One HTTP round trip per few itemsRound-trip cost amortised across many items
A dead worker loses almost nothingA dead worker loses the whole shard's work
More rows in tasks, more SQLite writesFewer, larger rows
Fine-grained reassignmentCoarse reassignment

The measured effect of getting this wrong is large. The same 500-item job on the same machine:

Effect of shard_sizeidentical hardware, identical corpus
050k100k150k200k250k1 Mac · unsharded20.158s wall · 0 failed 89,2962 Macs · unsharded17.11s wall · 0 failed 105,2001 Mac · shard_size=327.123s wall · 0 failed 252,686items / hour →
Unsharded (shard_size=1) spends most of its wall time on HTTP, not inference. Packing 32 items per task cut the job from 20.2s to 7.1s — a 2.8× speedup from batching alone.

A practical starting point: pick a shard that takes each worker 30 seconds to a few minutes. Long enough that HTTP overhead disappears, short enough that a closed laptop lid does not cost much.

Results

GET /v1/jobs/{job_id}/results returns both views:

json
{
  "results": [ { "task_id": "…", "status": "done", "worker_id": "…", "payload": {}, "result": {} } ],
  "items":   [ { "task_id": "…", "worker_id": "…", "id": "doc-1", "embedding": [...], "dim": 768 } ],
  "n_shards": 16,
  "n_items": 500
}

Read items. It is the reduce step — every shard's inner items flattened into one list, each tagged with the task and worker that produced it. The results array is the per-shard view, useful for debugging distribution and errors.

Job status

Derived on read, not maintained by a background process:

StatusMeaning
runningTasks are still pending or leased
completedNo open tasks, nothing failed
partialNo open tasks, at least one failed permanently

GET /v1/jobs/{id}/stats adds a throughput block. Note that items_done_est is an estimate — it scales n_items by the fraction of shards completed, so it is only exact when every shard is the same size.

Item shapes by job type

TypeItem fieldsRuns
embedid, textPOST /api/embeddings per item
ocrid, image_b64 or image_path, optional promptPOST /api/generate with images
classify, generateid, prompt or textPOST /api/generate

embed also still accepts a legacy single-item payload ({id, text} with no items array) from before sharding existed.

MIT licensed. Every benchmark on this site is reproducible with macbatch bench.