Optimistic locking
Preventing lost updates with version-based concurrency control
The API uses the optional version field for optimistic concurrency control, preventing lost updates when multiple clients simultaneously edit the same entity.
How optimistic locking works
Every versioned entity includes a version field:
type Device {
id: ID!
version: Int!
title: String!
# ...
}The version starts at 1 when an entity is created and increments with each successful update.
Update and delete mutations accept the current version:
input DeviceUpdateInput {
id: ID!
version: Int # Optional. Include to enable conflict detection.
title: String
# ...
}
input DeviceDeleteInput {
id: ID!
version: Int # Optional. Include to enable conflict detection.
}Supported entities
Optimistic locking applies to:
GPS trackers, sensors, beacons
Vehicles, equipment, employees
Asset collections
Geofences, POIs, routes
Work hours, maintenance windows
Warehouse records
Tenants. Read-only in this API, so the version is informational
User accounts
Workspace memberships
External system integrations
All catalog items (device types, asset types, tags, etc.)
Operations by type
Create
Version not applicable. Returns version: 1
Update with version
Returns 409 Conflict if the entity was modified since your last fetch
Update without version
The API reads the current version and updates against it. A change that lands between that read and the update still returns CONFLICT, but a change that landed before your request is overwritten without warning.
Delete with version
Returns 409 Conflict if the entity was modified since your last fetch
Delete without version
Deletes unconditionally, regardless of any concurrent changes
Omitting version on a delete-type operation is particularly risky, as the operation proceeds unconditionally regardless of what happened to the entity since you last fetched it. Always include version when deleting unless you have a specific reason not to.
Workflow
Here's the typical flow for updating an entity:
Query returns version:
Mutation includes the version in its input:
Handling conflicts
If you provided version and the entity was modified since you fetched it, the API returns a 409 Conflict error. The HTTP status code will be 200 because the request was successfully received and processed. This is a business logic issue, not a transport failure. This follows the GraphQL-over-HTTP specification, which reserves HTTP error codes (4xx, 5xx) for transport-level problems like authentication failures or malformed requests.
The actual error details are in the response body:
Use extensions.code for programmatic error handling, not HTTP status codes. See Error handling for more details.
When you receive a conflict error:
Fetch the entity again to see what changed
Merge the other user's changes with yours if needed
Retry your update with the new version
Concurrent editing example
Here's what happens when two users edit the same device:

User A's update succeeds first. User B's update fails because the version changed. After refetching, User B can successfully update with the current version.
Idempotent commands
Mutations that manage relationships and assignments never require or check the version field, because the relationship row has none. Two of them are also idempotent commands: repeating the same call doesn't change the result.
Add asset to group
Remove asset from group
These operations behave as follows:
Repeating
addwith the same input returns success without making changesCalling
removewhen the relationship doesn't exist returns success without making changes
This design simplifies client code. You can safely retry these operations without worrying about conflicts or checking the current state first.
The device link, identifier, and relation mutations are not idempotent:
deviceInventoryLinkfails when the device is already assigned anywhere, anddeviceInventoryUnlinkfails when it has no active assignment. See Managing device inventory.deviceIdentifierAddreturns a 409 DUPLICATE error when the identifier already exists, on this or any other device, anddeviceIdentifierRemovereturns a 404 error for an unknown or already-removed identifier ID.deviceRelationCreatefails when the relation already exists, anddeviceRelationRemovefails for an unknown or already-removed relation.
Best practices
Always include
versionin your queries. When fetching entities you plan to modify, request theversionfield so you have it ready for mutations.Always include
versionin updates and deletes. The field is optional, but omitting it removes your protection against overwriting changes made by other users since your last fetch. Omit it only for bulk operations where overwriting concurrent changes is acceptable.Be especially careful when deleting without
version. A wrong update can usually be corrected with another update, but a delete withoutversioncan remove an entity that another user just changed, and a delete can't be reversed. See Deleting records.Handle conflicts gracefully. In collaborative applications, version conflicts are expected. Implement retry logic or prompt users to review changes.
Don't cache versions long-term. Versions can change at any time. Always use the version from your most recent fetch of the entity.
See also
Error handling: Understand error structure, codes, and common error scenarios
Managing device inventory: Assign devices to inventories and track assignment history
Last updated
Was this helpful?