Verified against Proxmox VE 9.2.4 · Proxmox Backup Server 4.2.5 · Ceph Squid / Tentacle · Debian 13 (Trixie) · Linux kernel 7.0 (PVE 9.2 default) · 2026-08-12
○Not yet marked complete on this device.
Ask the Proxmox API to start a virtual machine and it does not tell you
whether the machine started. It tells you that it has begun trying:
Read-only / Safethe API's answer to 'start this VM'— Illustrative. This is the complete response body. It is not an error, it is not a status, and it is not a result — it is a receipt.
Every state-changing operation in Proxmox works this way. Starting a VM,
taking a backup, migrating a guest, creating a container, moving a disk:
each one forks a worker process, records it as a task, and returns
its identifier immediately. The API viewer confirms it — POST to
/nodes/{node}/qemu/{vmid}/status/start has a return type of string,
and that string is the UPID.
At an interactive shell this is easy to miss, because qm start usually
finishes so fast that the next thing you type happens after the VM is up
anyway. In a script it is the single most common source of races, and the
failures it produces are intermittent, timing-dependent, and worse on a
busy cluster — which is to say, worst in production.
What a UPID encodes
A UPID is a colon-delimited string with a trailing colon, and every field
is there for a reason.
UPID:pve-01:00006A2F:04C1B2E1:6A7C83E0:qmstart:100:root@pam: | | | | | | | | | | | | | +-- user@realm | | | | | +------ task id (usually VMID) | | | | +-------------- task type | | | +----------------------- start time, hex Unix epoch | | +-------------------------------- process start, hex jiffies | +----------------------------------------- worker PID, hexadecimal +------------------------------------------------ node that owns the task
Three of those are immediately useful.
The node. A UPID belongs to the node that ran the task. You cannot
query it from a different node’s /nodes/{other}/tasks/ path — the node
name in the UPID and the node in the API path have to match. Scripts that
migrate a guest and then poll the wrong node are a real category of bug.
The start time.6A7C83E0 in hexadecimal is 1786545120, a Unix
timestamp. That makes a UPID self-dating without any lookup, which is
useful when you find one in a log six months later.
The type and id.qmstart:100 says what was attempted and to what.
vzdump:141, qmigrate:102, imgcopy:, hastart:vm:100 — the type
vocabulary is small and readable, and it is what you filter on when
searching the task history.
Read-only / Safedecode a UPID's timestamp— Read-only. Field 5 is the start time in hexadecimal. This is worth knowing because a UPID in a six-month-old log tells you exactly when the operation ran without any lookup at all.
Two things have to be checked, and checking only one is the classic
mistake.
Read-only / Safethe two fields that matter— Read-only. 'status' says whether the worker is still running. 'exitstatus' says whether it succeeded, and it only exists once the task has stopped. A task can be stopped and failed at the same time.
NODE=pve-01
UPID='UPID:pve-01:00006A2F:04C1B2E1:6A7C83E0:qmstart:100:root@pam:'
pvesh get "/nodes/$NODE/tasks/$UPID/status" --output-format json | jq .
Read-only / Safea task that finished, and failed— Illustrative. status is 'stopped', which many scripts treat as success. exitstatus is the field that says otherwise, and 'OK' is the only value that means success.
# pvesh get /nodes/pve-01/tasks/UPID.../status --output-format json
{
"exitstatus": "unable to create VM 900 - VM 900 already exists on node 'pve-02'",
"id": "900",
"node": "pve-01",
"pid": 27183,
"pstart": 79822817,
"starttime": 1786545120,
"status": "stopped",
"type": "qmcreate",
"upid": "UPID:pve-01:00006A2F:04C1B2E1:6A7C83E0:qmcreate:900:root@pam:",
"user": "root@pam"
}
Illustrative output
Reading a task log
The task log is the same output the GUI shows in its task viewer, and it
survives the task by a long way — Proxmox keeps completed task logs on
disk, so a backup that failed last week can still be read in full.
Read-only / Safethe full log of a task, running or finished— Read-only. The log endpoint returns an array of objects with 'n' (line number) and 't' (text). Use --limit 0 to ask for all lines rather than the default page.
The pvenode wrapper is shorter for interactive use and does the same
thing:
Read-only / Safethe interactive route— Read-only. pvenode task list is the node's history; task log and task status take the UPID from it. This is the fastest path from 'something failed overnight' to the actual error message.
pvenode task list --limit 20 --errors 1
UPID='UPID:pve-01:00006C88:04C41A02:6A7BD3A3:vzdump:141:root@pam:'
pvenode task status "$UPID"
pvenode task log "$UPID"
Read-only / Safea backup task log with the real cause in it— Illustrative. The last line before the failure is the one that matters, and it is specific enough to act on. This detail exists only in the task log; the task list shows 'job errors' and nothing more.
INFO: starting new backup job: vzdump 141 --storage backup-pbs --mode snapshot
INFO: Starting Backup of VM 141 (qemu)
INFO: Backup started at 2026-08-12 02:00:03
INFO: status = running
INFO: creating Proxmox Backup Server archive 'vm/141/2026-08-12T02:00:03Z'
INFO: started backup task 'a9f21c44-2b1e-4c8d-9f10-REDACTED'
INFO: 0% (1.1 GiB of 200.0 GiB) in 3s, read: 383.4 MiB/s
ERROR: job failed with err -5 - Input/output error
INFO: aborting backup job
ERROR: Backup of VM 141 failed - job failed with err -5 - Input/output error
INFO: Failed at 2026-08-12 02:00:41
Illustrative output
Finding tasks you do not have the UPID for
This is the ordinary morning-after case: something failed, nobody
recorded the UPID, and you need to find it.
Read-only / Safefilter the node's task history— Read-only. The tasks endpoint accepts errors, typefilter, vmid, userfilter, statusfilter, since and until. Filtering server-side is much faster than pulling everything and grepping, especially on a node with months of history.
NODE=pve-01
# every failed task in the last 24 hours
SINCE=$(date -d '24 hours ago' +%s)
pvesh get "/nodes/$NODE/tasks" --errors 1 --since "$SINCE" \
--output-format json | jq -r '.[] | [.starttime, .type, .id, .status] | @tsv'
# everything that touched one guest
pvesh get "/nodes/$NODE/tasks" --vmid 141 --limit 20 \
--output-format json | jq -r '.[] | [.type, .status] | @tsv'
For a cluster-wide view there is /cluster/tasks, which aggregates
across nodes — useful precisely when you do not yet know which node the
work happened on.
Read-only / Saferecent tasks across the whole cluster— Read-only. This is the endpoint to reach for after an HA failover, when the guest has moved and you do not know which node's history to search.
pvesh get /cluster/tasks --output-format json \
| jq -r '.[] | [.node, .type, .id, .status] | @tsv' | head -20
Making it synchronous in a script
Almost every script wants “start this VM and tell me when it is running,
or fail”. That is a wrapper, and it is worth writing once properly.
Configuration changea task-waiting wrapper— Runs an API call, then polls until the task stops or the timeout expires. Returns non-zero on failure and prints the exitstatus, so the caller can react. Adjust the timeout per operation type: a VM start needs seconds, a 2 TB backup needs hours.
#!/usr/bin/env bash
set -euo pipefail
wait_for_task() {
local node="$1" upid="$2" timeout="${3:-600}"
local deadline=$(( $(date +%s) + timeout ))
local st exitstatus
while :; do
st=$(pvesh get "/nodes/$node/tasks/$upid/status" --output-format json)
if [ "$(echo "$st" | jq -r .status)" != "running" ]; then
exitstatus=$(echo "$st" | jq -r '.exitstatus // "no exitstatus"')
if [ "$exitstatus" = "OK" ]; then
return 0
fi
echo "task failed: $exitstatus" >&2
pvesh get "/nodes/$node/tasks/$upid/log" --limit 0 \
--output-format json | jq -r '.[].t' | tail -20 >&2
return 1
fi
if [ "$(date +%s)" -ge "$deadline" ]; then
echo "timed out after ${timeout}s waiting for $upid" >&2
return 2
fi
sleep 2
done
}
NODE=pve-01
VMID=100
UPID=$(pvesh create "/nodes/$NODE/qemu/$VMID/status/start")
wait_for_task "$NODE" "$UPID" 120
Four details in that wrapper are not decoration.
It polls status, then reads exitstatus. Both, in that order,
because exitstatus is absent while the task is running.
It has a timeout, and a distinct exit code for it. A caller needs
to distinguish “the operation failed” from “we stopped waiting”, since
the second means the operation may still be in progress.
It dumps the tail of the task log on failure. The exitstatus
string is often a summary; the log has the detail. Capturing it at the
moment of failure is far more reliable than expecting someone to go
looking later.
It sleeps two seconds rather than spinning. A tight poll loop
against pvedaemon across a fleet is a self-inflicted load problem.
Stopping a running task
Service impact possibleask a running task to stop— Sends a stop request to the worker. It is a request, not a kill: the worker unwinds at its next safe point, which for a backup means finishing the current chunk. Some tasks leave partial state behind, so check what the operation was before cancelling it.
A stopped task reports an exitstatus reflecting the interruption rather
than OK, which is correct — it did not do what it was asked. Scripts
that treat any completed task as done will read a cancelled backup as a
successful one.
Common mistakes
Treating the returned UPID as a result. It is a receipt for work
that has begun.
Polling status and ignoring exitstatus. A task can be
stopped and failed simultaneously. Only OK means success.
Querying a UPID against the wrong node. The node is in the UPID and
must match the API path. After a migration this bites.
Discarding the error string.exitstatus frequently names the
exact problem; the task log has more.
Polling every 100 ms. The load exceeds the work on a cluster of any
size. Two seconds for a start, ten to thirty for a backup.
No timeout, or a timeout that reports the same failure as an error.
A caller needs to distinguish “it failed” from “we stopped watching,
and it may still be running”.
Treating a cancelled task as complete. Stopping a task produces a
non-OKexitstatus for a reason.
Relying on task history for an audit trail. It rotates. Capture the
log if the record matters.
Key takeaways
Every state-changing Proxmox operation returns a UPID immediately and
does the work asynchronously.
A UPID encodes node, worker PID, process start, start time (hex Unix
epoch), task type, task id and user — and is globally unique without
any coordination.
Poll /nodes/{node}/tasks/{upid}/status; status says whether it is
running, exitstatus says whether it worked, and exitstatus exists
only once it has stopped.
exitstatus must be exactly OK. Anything else is the error.
/nodes/{node}/tasks/{upid}/log returns the full output and survives
the task; pvenode task list --errors 1 finds failures without a UPID.
/cluster/tasks aggregates across nodes — the right endpoint when you
do not know where the work ran.
DELETE /nodes/{node}/tasks/{upid} requests a stop; the worker unwinds
at a safe point and reports a non-OK status.
Write one task-waiting wrapper with a timeout and reuse it. Fixed
sleeps fail in both directions.
Knowledge check
Knowledge check · 5 questions
Q1. A script polls a task until status is "stopped", then proceeds. What class of bug does this introduce?
Q2. A UPID must be queried against the node named inside it; polling it against a different node in the /nodes/{node}/tasks path will not find it.
Q3. Which of these are legitimate reasons the task log is worth retrieving rather than relying on exitstatus alone? Select all that apply.
Q4. Why does the UPID include the worker process start time (the pstart field) in addition to the PID?
Q5. A provisioning script intermittently fails with configuration lock errors, but only on the production cluster and never in test. It creates a VM and immediately calls qm set to attach a disk. What is happening?
Passing score: 75%. Answers are checked in this browser.