# Idempotency and Concurrency

<Lead>

Requests fail in ways the client cannot see. The connection drops after the
server committed, a gateway times out, or an agent retries a tool call. Terraform
re-applies whole plans. v4 is designed so that **reads, updates and deletes can be
retried without thinking about it**. Two things v4 does not support:
`Idempotency-Key`, so a retried create can make a second resource, and `ETag` and
`If-Match`, so when two people (or agents) edit the same resource, the last write
wins.

</Lead>

## <N n="1" />Everything but a create is retry-safe

<Rule id="V4-IDEM-1" review>

`GET`, `PUT`, `PATCH`, `DELETE` and the `POST` that adds an association can be
retried safely. `PATCH` sets absolute values, and `GET` has no side effects.

</Rule>

<Tiles cols={3}>
<Tile method="GET" title="Safe">

No side effects.

</Tile>
<Tile method="PUT" title="Idempotent">

The body is the full new state of a resource whose key the client chose.

</Tile>
<Tile method="PATCH" title="Idempotent">

Merge patch sets absolute values (see below).

</Tile>
<Tile method="DELETE" title="Idempotent">

A repeat returns `404`, which the client treats as success.

</Tile>
<Tile method="POST" title="Idempotent for an association">

The key is in the body, so a repeat replaces the same association.

</Tile>
<Tile method="POST" title="Not idempotent for a create" tone="warn">

A retry can create a second resource. There is
[no `Idempotency-Key`](#no-idempotency-key).

</Tile>
</Tiles>

The rules that keep this true:

<DoDont>
<Do title="PATCH sets an absolute value">

```json
{ "value": 600 }
```

</Do>
<Dont title="PATCH a relative change">

```json
{ "increaseBy": 100 }
```

Anything relative is a `POST` that creates something.

</Dont>
</DoDont>

<Rule id="V4-IDEM-4" review>

When the client knows the identity of what it creates (a person's role on a
pool, a person's budget), the request carries that key, and it replaces the
existing one instead of adding a second. The key is in the body of the `POST`
that adds an association, or in the URL of the `PUT` that sets one, or a person's
singleton.

</Rule>

<DoDont>
<Do title="Send the natural key">

```http
POST /v4/buckets/{bucketId}/ai/pools/{poolId}/members
Content-Type: application/json

{ "userId": "user_Jm5tR8wQz2KcX7vB4nLp9HdS", "role": "admin" }
```

```http
PUT /v4/buckets/{bucketId}/ai/user-apps/{userAppId}/users/user_Jm5tR8wQz2KcX7vB4nLp9HdS/budget
Content-Type: application/json

{ "limits": […] }
```

Repeating either request changes nothing.

</Do>
<Dont title="Give the association its own ID">

```http
HTTP/1.1 201 Created
Location: /v4/buckets/{bucketId}/ai/pools/{poolId}/members/mbr_…
```

A retry creates a second one.

</Dont>
</DoDont>

**No side effects on `GET`.** Nothing is created, sent or counted by a read.

## <N n="2" />No Idempotency-Key

<Rule id="V4-IDEM-2" lint="v4-idempotency" shared="ROB-1">

No operation declares an `Idempotency-Key` header. v4 does not store and replay
the response of a `POST`, and a header that the server ignores promises a retry
safety that does not exist.

</Rule>

The shared guidelines (ROB-1) require the header, as in the
[IETF draft](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/),
for creating or charging `POST`s. v4 does not apply that rule
([decided on 2026-11-05](/guidelines/decisions#open-questions), question 9).

<DoDont>
<Do title="A create without the header">

```yaml
post:
  operationId: create-ai-pool
  requestBody: …
```

</Do>
<Dont title="A header the server ignores">

```yaml
post:
  operationId: create-ai-pool
  parameters:
    - $ref: "#/components/parameters/IdempotencyKey"
```

A client that trusts it still creates duplicates.

</Dont>
</DoDont>

A `POST` whose response was lost may or may not have been applied, and may
still be running. Where the client knows the identity of what it creates,
the request carries that key and replaces an existing association (V4-IDEM-4), so
this case does not come up.
Otherwise a retried create can always duplicate. Look the resource up before
retrying: by its `name` or `label` where that is unique (a pool, a provider assignment, a consumer), or
as the newest item of the list. This catches a create that already completed,
but not one that is still in flight, so it makes a duplicate less likely, not
impossible:

<DoDont>
<Do title="Check before retrying">

```text
create pool "Engineering"  → connection lost
list pools                 → not there, so create it again
```

</Do>
<Dont title="Retry blindly">

```text
create pool "Engineering"  → connection lost
create pool "Engineering"  → a second "Engineering" pool
```

</Dont>
</DoDont>

## <N n="3" />No ETag or If-Match

<Rule id="V4-IDEM-3" lint="v4-conditional-requests" shared="ROB-2">

No operation declares an `If-Match` header, an `ETag` response header or a `412`
response. v4 does not check a write against the version the client read, and a
header that the server ignores promises a protection from lost updates that does
not exist.

</Rule>

The shared guidelines (ROB-2) require conditional requests for "config-like"
resources, and most v4 resources, such as an app or a pool, are rich
configuration. v4 does not apply that rule: the last write wins
([decided on 2026-11-05](/guidelines/decisions#open-questions), question 14).

<DoDont>
<Do title="A write without the header">

```yaml
patch:
  operationId: update-ai-pool
  requestBody: …
```

</Do>
<Dont title="A header the server ignores">

```yaml
patch:
  operationId: update-ai-pool
  parameters:
    - $ref: "#/components/parameters/IfMatch"
```

A client that trusts it still overwrites someone else's change.

</Dont>
</DoDont>

A write replaces whatever someone else changed since you read the resource. `PATCH` leaves the fields you leave out alone, so send only the ones
you change. An array is replaced as a whole, so read it right before you write
it:

<DoDont>
<Do title="Send only what you change">

```http
PATCH /v4/buckets/{bucketId}/ai/pools/{poolId}
Content-Type: application/json

{ "name": "Platform" }
```

</Do>
<Dont title="Send back everything you read">

```http
PATCH /v4/buckets/{bucketId}/ai/pools/{poolId}
Content-Type: application/json

{ "name": "Platform", "budgetRules": […], "policyTemplate": […] }
```

This undoes any change to the budget rules or the policy template made since
your read.

</Dont>
</DoDont>

<EnforcedBy
  lint="v4-idempotency v4-conditional-requests"
  checks="No operation declares Idempotency-Key, If-Match, ETag or 412."
>

That `PATCH` sets absolute values and `GET` has no side effects (V4-IDEM-1), and
that an association's natural key is in the body of its `POST` or the URL of its
`PUT` (V4-IDEM-4).

</EnforcedBy>
