cpuos

Tutorials · 5 min read · updated Oct 7, 2026

Asynchronous Python jobs: submit, poll, cancel and retry

Design asynchronous Python execution with cpuOS: distinguish HTTP acceptance from success, poll job status, recover idempotent requests and cancel work.

The current pilot runs trusted Python and Node jobs on your Docker worker. Containers share its kernel. Browser and repository workflows need capabilities beyond this pilot.

On this page

HTTP acceptance and Python completion are separate events

An asynchronous Python execution API returns a job record before the script necessarily finishes. In cpuOS, POST /v1/jobs returns HTTP 202 with an accepted or replayed job. Store its ID, request parameters and application intent before letting another workflow depend on its answer. A successful HTTP response proves that the request was handled, not that a calculation produced a correct result.

Use the Python execution tutorial for the complete standard-library HTTP client and known arithmetic fixture. This guide focuses on lifecycle decisions that surround that client: waiting budgets, ambiguous submission, cancellation races and deliberate new attempts. The current pilot runs trusted Python 3.13 or Node 24 code on your Docker worker. Code and outputs pass through the hosted control plane.

Persist one submission intent before making the POST

Keep the workspace API key in your submitting application. An intent record should contain a stable Idempotency-Key, the exact source, resource settings and eventually the returned job ID. Replaying unchanged normalized parameters with the same key recovers the same workspace job. Changed parameters with that key produce HTTP 409 idempotency_conflict. A fresh key requests another execution.

Canonical JSON body for POST /v1/jobs; source from the Python tutorial
{  "name": "Synthetic order totals",  "template": "python",  "code": "import json\n\n# Synthetic orders, with prices represented as integer cents.\norders = [\n    {\"sku\": \"A\", \"unit_cents\": 1200, \"quantity\": 2},\n    {\"sku\": \"B\", \"unit_cents\": 500, \"quantity\": 3},\n    {\"sku\": \"A\", \"unit_cents\": 1200, \"quantity\": 1},\n]\nby_sku = {}\nfor order in orders:\n    revenue = order[\"unit_cents\"] * order[\"quantity\"]\n    by_sku[order[\"sku\"]] = by_sku.get(order[\"sku\"], 0) + revenue\nprint(json.dumps({\n    \"rows\": len(orders),\n    \"units\": sum(order[\"quantity\"] for order in orders),\n    \"revenue_cents\": sum(by_sku.values()),\n    \"by_sku\": by_sku,\n}, sort_keys=True))",  "vcpu": 1,  "memoryMb": 512,  "timeoutSeconds": 30}

Send the intent key in the Idempotency-Key header, rather than adding it as a JSON field. The jobs contract rejects extra body fields. Retain the key when a connection fails before a response arrives: losing the response does not prove the server rejected the submission. Do not create a new key merely because an HTTP client timed out.

Poll status with an application waiting budget

Scroll horizontally to see every column.

Job stateApplication decision
queuedKeep the job ID and wait for eligible worker capacity.
runningContinue bounded polling. Output is returned after execution.
completedCheck exitCode, error, outputTruncated and the result contract.
failedInspect the reason; decide whether a new attempt is appropriate.
cancelledRecord an unsuccessful attempt and stop normal result processing.

Read GET /v1/jobs/:id until a terminal state arrives. Set a separate client waiting budget that includes requests, queue delay and retry waits. The job's execution timeout cannot stand in for that budget. The published Python client uses time.monotonic for elapsed time, so wall-clock adjustments do not alter its waiting calculation.

The workspace rate limit is shared across API keys and clients. Polling one job every two seconds already uses roughly 30 requests per minute, before submissions or other operations. Coordinate polling centrally for several jobs and respect Retry-After after HTTP 429. A longer interval increases result discovery latency; it does not slow the script itself.

Cancellation can race with normal completion

Use DELETE /v1/jobs/:id when the application no longer needs unfinished work. cpuOS records cancellation for queued work and informs the worker of running cancellation through its polling loop. Do not interpret the API response as instantaneous process termination. A terminal job stays terminal: if completion or failure won the race, DELETE returns that existing result.

Stopping a web request, terminating your client or pressing Ctrl-C does not automatically cancel an accepted server job. The tutorial attempts cancellation when it knows the ID. If the cancellation request fails, keep that ID and inspect the dashboard or API. Recover an uncertain submission with its original intent key before deciding which job should be cancelled.

Separate request recovery from a new computation attempt

A request retry recovers the same intent after a transport problem. A computation retry requests a new execution after reviewing a failed or cancelled attempt. Idempotent replay returns the existing job, including terminal failure; it does not rerun Python. Keep a parent task identifier in your application so several approved attempts can be compared without treating them as distinct business tasks.

Local Node 24 policy fixture; no network requests
// Application policy, not a cpuOS SDK. Persist the intent key and payload.function nextAction({ transportUncertain = false, httpStatus, errorCode, status }) {  if (transportUncertain) return "recover-same-intent"  if (errorCode === "idempotency_conflict") return "investigate"  if (errorCode === "no_online_nodes" || errorCode === "queue_full"      || httpStatus === 429) return "wait-for-capacity"  if (status === "queued" || status === "running") return "poll-existing-job"  if (status === "completed") return "validate-result"  if (status === "failed" || status === "cancelled") return "review-before-new-attempt"  return "investigate"}const decisions = [  nextAction({ transportUncertain: true }),  nextAction({ httpStatus: 429, errorCode: "queue_full" }),  nextAction({ status: "completed" }),  nextAction({ status: "failed" }),]console.log(JSON.stringify(decisions))

This policy names the next decision without executing it. wait-for-capacity requires checking worker readiness or queue pressure, while review-before-new-attempt requires inspecting the failure. Add a maximum attempt count and a reason for each new key. Validate results separately with the structured stdout protocol.

Resolve queue and worker failures before resubmitting

Scroll horizontally to see every column.

EvidenceUseful response
503 no_online_nodesConnect an eligible recent-heartbeat Docker worker; verify CPU and memory.
429 queue_fullReduce application submissions or cancel unnecessary queued jobs.
failed / worker_lostInspect the interrupted attempt and worker before authorizing another run.
failed / queue_expiredReview worker availability and the application's waiting policy.
completed / invalid stdoutFix the output or domain contract; request acceptance did not establish correctness.

Jobs run with networking disabled; package installation, uploaded files and persistent sessions are unavailable in this pilot. Keep the submitted task small and self-contained. Follow worker operations for readiness and interrupted attempts, then use bounded batches when one application task needs several independent executions.

Return checked tool evidence to the model

When an open model requests a tool, your application authorizes the proposed arguments, submits the job and waits for a checked result. Return compact structured evidence with an explicit failure outcome when the task did not succeed. gpuOS tool calling explains the model side of that loop; LangChain and n8n show HTTP workflow wiring.

Questions

Does an asynchronous Python API return live stdout while the job runs?
cpuOS returns captured stdout and stderr after execution. Poll job status while queued or running; the current pilot does not stream Python logs live.
How do I recover a Python submission whose response was lost?
Replay the exact same job parameters with the original Idempotency-Key. cpuOS returns the same workspace job. Preserve that key before the POST; a new key requests another execution.
Does cancelling my HTTP request stop the Python job?
No. Use DELETE /v1/jobs/:id for unfinished work. Cancellation can race with completion, and a running worker observes cancellation through its polling loop. Inspect the returned terminal state.

Related

Connect a worker and run a job

Start with a small trusted Python or Node task, fixed limits and an expected result.

gpuOS · where models think

Need the model too? Run it on gpuOS

gpuOS serves open models on your own GPUs behind one OpenAI-compatible API. Your application can submit authorized actions to cpuOS jobs.