Skip to content

Job Queue

Work that waits for a device instead of failing when none is free.

A fleet of personal machines is intermittent by nature — phones sleep, laptops close, iPads charge overnight. Through 0.7 every one of those was a failed request. The queue turns intermittency into a scheduling input: submit now, and the job runs when a node is free and fit to take it.

POST /v1/jobs queue a job, get an id and a position
GET /v1/jobs list jobs, counts, and why the queue is or isn't moving
GET /v1/jobs/{id} one job, with its position and waiting reason
DELETE /v1/jobs/{id} cancel, refunding the stake if it never ran
Terminal window
curl -X POST http://localhost:8420/v1/jobs \
-H "Authorization: Bearer $MYCELLM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user", "content": "Summarise this quarter."}],
"min_tier": "capable",
"stake": 10
}'
{
"job_id": "q_d5cfe664a012",
"state": "queued",
"position": 1,
"expires_at": 1787691541.9,
"stake": 10.0
}
FieldMeaning
messagesRequired. Same shape as a chat request.
modelA model name, or mycellm/swarm. Empty / auto lets the scheduler resolve.
min_tierQuality floor: frontier, capable, fast, tiny. Only valid when model is empty.
trustTrust ceiling: local, trusted, any.
token_budgetCeiling on generated tokens across the whole job.
fanoutProposer count, for mycellm/swarm.
stakeCredits staked for position. Staked, not spent.
ttl_sSeconds before the job gives up waiting. Default 24h; 0 never expires.

model and min_tier together are rejected with a 400, not silently reconciled. A floor constrains the node’s choice; once you name a model there is nothing left to constrain, so a request asking for both cannot mean what it says.

Every queued job carries a waiting_reason in plain language, and the list endpoint reports a device-level reason of its own:

{
"jobs": [{
"job_id": "q_d5cfe664a012",
"state": "queued",
"waiting_reason": "No frontier-tier model is reachable (3 model(s) online below that tier).",
"position": 1
}],
"counts": {"queued": 1},
"scheduler": {"running": 0, "reason": "Waiting — this device is in Low Power Mode or low on battery."}
}

This is not decoration. A queue that cannot say what it is waiting for is indistinguishable from a hang, and the two have completely different fixes: “go wake the Mac Studio” versus “take the phone off Low Power Mode”.

A node takes queued work when it is serving, not power-constrained and not thermally throttled — the same telemetry it already advertises to the rest of the fleet, so a node cannot decide it is fine while telling everyone else it is throttled.

Order is deliberately boring:

priority = staked_credits + waiting_time_bonus

…evaluated within an ownership class, because owner affinity is applied first and as a hard rule:

  • Your own jobs outrank a stranger’s on your hardware, at any bid. Not “usually”, not “weighted heavily” — always. That is the property a marketplace cannot offer, and it must not be expressible as a number someone can outbid.
  • Waiting time always accrues. Without it, any bidding converges on pay-to-play, unpaid work never runs, and the people donating hardware leave.
  • A stake is staked, not spent. Refunded when a job expires or is cancelled unrun, so bidding for position is never a gamble on the fleet’s availability.

A job is in SQLite before the caller is told it was accepted, and jobs left running by an unclean shutdown are returned to the queue on the next start. A queue that loses work on restart is worse than no queue, because the caller stopped holding the request.

SettingDefaultMeaning
MYCELLM_QUEUE_ENABLEDtrueOff makes /v1/jobs answer 503 queue_disabled.
MYCELLM_QUEUE_MAX_CONCURRENT1Jobs run at once. A personal device that starts three because three were waiting is a device someone force-quits.
MYCELLM_QUEUE_POLL_INTERVAL5.0Seconds between polls while work is pending.

/v1/jobs always requires authentication, including on a public bootstrap. Queuing work on someone’s hardware for up to a day is not something anonymous callers should be able to do.