, , ,

Asynchronous Tasks and ETags: Making Safe Writes with the Nutanix v4 API

5 min read

Reading from the Nutanix v4 APIs is easy. Writing is where people get caught out, because two behaviors differ from what most REST clients assume: an accepted write is not a completed write, and modifying an existing entity requires proof that you are working from its current state.

Get these two things right, and v4 writes become predictable. Ignore them, and you get intermittent failures that are miserable to reproduce.

Applies to: Prism Central v4 GA APIs. If you are new to the v4 request structure, start with Getting Started with the Nutanix v4 REST APIs.

Five-step diagram of a Nutanix v4 write: GET the entity for its ETag, send a PUT with If-Match and a new request ID, receive HTTP 202 with a task ID, poll the task, then re-GET the entity. Below, three common failure causes: stale ETag, reused request ID, and treating 202 as completion.

The ETag protects against mid-air collisions: Consider two administrators updating the same VM within seconds of each other. Both read the current configuration, both make a change, both write. Without protection, the second write silently discards the first, a mid-air collision.

The v4 APIs prevent this using standard HTTP ETags. A GET returns the entity along with an ETag header representing its current version. When you modify that entity, you send the ETag back in an If-Match header. If the entity has changed since your read, the ETag no longer matches, and the write is rejected.

The ETag is mandatory for operations on existing entities. This is not something you can skip when it is inconvenient.

The detail that catches people: the ETag changes after every successful update. If your script updates an entity twice, the ETag from the first read is already stale by the second write. You must GET the entity again to obtain its new ETag.

The request identifier makes retries safe: Most v4 create, update, and delete requests also require an Ntnx-Request-Id header containing an opaque value, conventionally a UUID.

This is an idempotency token. If a network failure leaves you unsure whether a request arrived, you can safely retry with the same identifier and the operation will not be applied twice. That is genuinely useful; the alternative is a script that creates two VMs because a response was lost in transit.

The corollary matters just as much: generate a new identifier for each genuinely new request. Reusing one across distinct operations is how you end up with a change that appears to succeed but never takes effect.

If you use the official SDKs, this header is generated for you, and you should not set it manually. Working with raw HTTP means managing it yourself.

Putting both headers together

# 1. Read the entity and capture its ETag
curl -i -X GET \
  "https://pc.example.com:9440/api/vmm/v4.0/ahv/config/vms/$VM_EXT_ID" \
  -H 'Accept: application/json' \
  -H "Authorization: Basic $AUTH"

# 2. Send the update with If-Match and a fresh request ID
curl -X PUT \
  "https://pc.example.com:9440/api/vmm/v4.0/ahv/config/vms/$VM_EXT_ID" \
  -H 'Content-Type: application/json' \
  -H "Authorization: Basic $AUTH" \
  -H "If-Match: $ETAG" \
  -H "Ntnx-Request-Id: $(uuidgen)" \
  -d @payload.json

Note the -i flag on the GET. The ETag arrives in the response headers, not the body, and it is easy to spend an afternoon looking for it in the JSON.

HTTP 202 means accepted, not finished: A successful v4 write typically returns HTTP 202 Accepted along with a task identifier. The response confirms that your request was well-formed, authorized, and queued. It does not confirm that the work succeeded.

The actual outcome lives in the task, which you retrieve through the prism namespace task management APIs. A script that treats 202 as success will happily report that it reconfigured forty VMs when several of those tasks failed.

There is a subtlety here worth knowing. For some entity types, including VMs, ETag validation itself is processed asynchronously. That means an incorrect ETag may not produce an immediate error response; the failure surfaces in the task instead. This is precisely why polling the task is not optional.

A workable polling pattern

  1. Submit the write and capture the task identifier from the response.
  2. Poll the task at a sensible interval rather than in a tight loop.
  3. Apply a timeout, so a stuck task does not hang the job indefinitely.
  4. Treat a failed task as a failed operation and surface the error detail.
  5. Re-read the entity afterward if you need its new state or a fresh ETag.

Back off between polls. Rate limit enforcement was introduced in Prism Central pc.2024.1 and AOS 6.8, and a tight polling loop across many concurrent operations is an efficient way to discover it.

Failure patterns to recognize

SymptomLikely cause
Write rejected despite a valid payloadStale ETag from an earlier read
Second update in a loop failsETag not refreshed after the first write
Retry appears to succeed, but nothing changesReused request identifier
Script reports success, config unchanged202 treated as completion; task never checked
Errors appear only under loadConcurrent writers colliding, or rate limiting

Almost every one of these is intermittent, which is what makes them expensive. They pass in a quiet lab and fail during a change window.

Summary: Read the entity, capture its ETag, send it back in If-Match, and generate a fresh Ntnx-Request-Id for each new request. Re-read the entity whenever you need to write again, because the ETag changes with every update.

Then treat HTTP 202 as the beginning of the operation rather than the end. The task holds the real result, and for some entity types it holds the ETag validation result too.

Series Links:

Official Resources

Nutanix v4 API IntroductionNutanix API User GuideNutanix Developer Portal


What Do You Think? Has an unchecked task ever hidden a failure in your automation, or do you poll every write as a matter of course?