Caching and conditional requests
This page handles client side caching through the usage of the ETag header. Other forms of caching exist, e.g. inside the internal databases, in a proxy between the user and the API etc., but are outside the scope of this article.
Other caching layers
Besides client-side ETag caching, two other cache-related aspects exist:
- The internal databases Ember Nexus depends on, e.g. Neo4j and Elasticsearch, have their own caches. They may show slower “cold start” performance until their own caches are warmed up.
- Ember Nexus itself stores/caches computed
ETagvalues. They can be cleared and recalculated with thecache:clear:etagcommand.
Collections larger than the configured cache.etagUpperLimitInCollectionEndpoints (see the configuration reference) do not receive an ETag at all, as computing one for very large collections would cause performance problems.
What is the ETag header?
The ETag header, short for “entity tag”, is an identifier for a version of an element. It is sent in HEAD and GET responses, and can be used in later requests to determine whether content has changed or stayed the same, i.e. whether data cached by the client is still valid.
Furthermore, the ETag can be used to craft conditional requests - do action x only if the element y has not changed.
See also:
How Ember Nexus calculates the ETag
The value of the Etag header is calculated as the XXH3 hash from the identifiers and updatedAt timestamps of all involved elements, as well as the instance’s ETag seed (config value cache.etagSeed, see documentation).
We use the XXH3 hash due to its performance benefits.
As the updatedAt timestamp uses microsecond precision, the resulting Etag will be different as long as updates happen at least one microsecond apart.
Using If-None-Match to avoid a second request
Initial request
As we do not yet know the ETag value associated with the first request, we simply execute the request normally:
curl \ -H 'Authorization: Bearer secret-token:M3WHIDj4q62EY0XiZFMLnv' \ https://api.example.com/35cd3b18-0d0c-4e98-876e-898b930797f2The response then contains the Etag-header.
Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-MethodAccess-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateContent-Type: application/json; charset=utf-8Date: Tue, 25 Aug 2026 11:53:42 GMTETag: "6JM8JahrCeu"Server: UnitTransfer-Encoding: chunkedX-Powered-By: Ember Nexus API{ "type": "Data", "id": "35cd3b18-0d0c-4e98-876e-898b930797f2", "data": { "created": "2020-01-01T00:00:00+00:00", "updated": "2020-01-01T00:00:00+00:00", "name": "Data", "scenario": "general.if-match" }}Note: The ETag value itself contains quotes; they are part of the ETag.
Cached request
As we now know the ETag value of the request, we can send a second request with the If-None-Match header and the ETag as its value.
If the element has stayed the same, the API will respond with a 304 not modified response, which does not have a response body.
Should the element have changed, the API returns the up-to-date data in its response, as well as its new ETag in the response headers.
curl \ -H 'Authorization: Bearer secret-token:M3WHIDj4q62EY0XiZFMLnv' \ -H 'If-None-Match: "6JM8JahrCeu"' \ https://api.example.com/35cd3b18-0d0c-4e98-876e-898b930797f2Response:
Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-MethodAccess-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateDate: Tue, 25 Aug 2026 11:53:50 GMTETag: "6JM8JahrCeu"Server: UnitX-Powered-By: Ember Nexus APIThe not modified response does not contain a response body.
Cached request with invalid Etag
We can lastly modify the Etag value to another string, effectively invalidating it. The API will then see the mismatch and return up-to-date data for the endpoint:
curl \ -H 'Authorization: Bearer secret-token:M3WHIDj4q62EY0XiZFMLnv' \ -H 'If-None-Match: "invalid"' \ https://api.example.com/35cd3b18-0d0c-4e98-876e-898b930797f2Response:
Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-MethodAccess-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateContent-Type: application/json; charset=utf-8Date: Tue, 25 Aug 2026 11:54:05 GMTETag: "6JM8JahrCeu"Server: UnitTransfer-Encoding: chunkedX-Powered-By: Ember Nexus API{ "type": "Data", "id": "35cd3b18-0d0c-4e98-876e-898b930797f2", "data": { "created": "2020-01-01T00:00:00+00:00", "updated": "2020-01-01T00:00:00+00:00", "name": "Data", "scenario": "general.if-match" }}Using If-Match to create conditional requests
Conditional requests are useful to avoid lost updates, i.e. when you only want a change to be executed when you know that no other person has changed it in the meantime.
Initial request
Similarly to the If-None-Match-header, we first need to find out the ETag of the element to be updated:
curl \ -H 'Authorization: Bearer secret-token:M3WHIDj4q62EY0XiZFMLnv' \ https://api.example.com/35cd3b18-0d0c-4e98-876e-898b930797f2The response then contains the Etag-header.
Access-Control-Allow-Headers: Authorization, Origin, X-Requested-With, Content-Type, Accept, Access-Control-Request-MethodAccess-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateContent-Type: application/json; charset=utf-8Date: Tue, 25 Aug 2026 11:54:15 GMTETag: "6JM8JahrCeu"Server: UnitTransfer-Encoding: chunkedX-Powered-By: Ember Nexus API{ "type": "Data", "id": "35cd3b18-0d0c-4e98-876e-898b930797f2", "data": { "created": "2020-01-01T00:00:00+00:00", "updated": "2020-01-01T00:00:00+00:00", "name": "Data", "scenario": "general.if-match" }}Sending a conditional successful request
If the element has not been changed in the meantime, a conditional request will be successful:
curl -X PATCH \ -H 'Authorization: Bearer secret-token:M3WHIDj4q62EY0XiZFMLnv' \ -H 'Content-Type: application/json' \ -H 'If-Match: "6JM8JahrCeu"' \ -d '{ "key": "some update to the element" }' \ https://api.example.com/35cd3b18-0d0c-4e98-876e-898b930797f2The condition matches, so the update is applied and the response is 204 No Content - the element’s ETag has now changed as a side effect, so this same If-Match value can not be reused for a second update.
Access-Control-Allow-Headers: *Access-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateDate: Tue, 25 Aug 2026 12:52:00 GMTX-Powered-By: Ember Nexus APISending a conditional invalid request
If we repeat the same request with the old ETag, which is no longer valid, as we have just updated the element, the request will fail:
curl -X PATCH \ -H 'Authorization: Bearer secret-token:M3WHIDj4q62EY0XiZFMLnv' \ -H 'Content-Type: application/json' \ -H 'If-Match: "6JM8JahrCeu"' \ -d '{ "key": "some update to the element" }' \ https://api.example.com/35cd3b18-0d0c-4e98-876e-898b930797f2Access-Control-Allow-Headers: *Access-Control-Allow-Methods: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKAccess-Control-Allow-Origin: *Access-Control-Expose-Headers: ETag, LocationAllow: GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE, PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCKCache-Control: no-cache, privateContent-Type: application/problem+json; charset=utf-8Date: Tue, 25 Aug 2026 12:52:05 GMTTransfer-Encoding: chunkedX-Powered-By: Ember Nexus API{ "type": "https://api.example.com/error/412/precondition-failed", "title": "Precondition Failed", "status": 412, "detail": "Precondition does not match."}