Timestamps
The broker stamps what it stores with createdAt and modifiedAt, on every entity and every attribute. You stamp a reading with observedAt, the time you observed it. expiresAt says when an entity or one of its attributes should go away. They are easy to muddle, because all of them read as date-times.
This guide covers what each instant means, when it moves, and how to read and query them.
createdAt and modifiedAt
Every entity and every attribute carries the pair. The broker sets both to the instant of the write that created the thing, so a fresh attribute reads them equal. A later write that changes the attribute moves its modifiedAt and leaves its createdAt alone.
- http
- curl
- rust
- python
PATCH /ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567/attrs/name
Content-Type: application/json
{
"type": "Property",
"value": "second"
}
curl -i -X PATCH http://localhost:1056/ngsi-ld/v1/entities/urn:ngsi-ld:Vehicle:A4567/attrs/name \
-H 'Content-Type: application/json' \
-d '{
"type": "Property",
"value": "second"
}'
let vehicle = EntityId::from(iri!("urn:ngsi-ld:Vehicle:A4567"));
let name: AttributeName = "name".parse()?;
let attribute = Attribute::from(Property::builder("second").build()?).into_patch();
let outcome = client
.entities()
.update_attribute(&vehicle, &name, attribute)
.send()
.await?;
response = requests.patch(
f"{BASE}/entities/urn:ngsi-ld:Vehicle:A4567/attrs/name",
json={
"type": "Property",
"value": "second",
},
)
assert response.status_code == 204
HTTP/1.1 204 No Content
After this, name holds its original createdAt and a newer modifiedAt. The clocks run at every depth. Sub-attributes have their own pair, and so does each instance of a multi-datasetId attribute. Updating the sensor-a instance moves that instance's modifiedAt, and the sensor-b instance beside it keeps the pair it was created with.
A write that changes nothing moves nothing. Restating the value that is already there answers 204, and every clock stays where it was. Writes explains what counts as a change.
The pair is hidden unless you ask for it. Read with options=sysAttrs and the answer carries createdAt and modifiedAt on the entity, on every attribute, and on every sub-attribute.
- http
- curl
- rust
- python
GET /ngsi-ld/v1/entities/urn:ngsi-ld:Building:1?options=sysAttrs
Accept: application/json
curl -i 'http://localhost:1056/ngsi-ld/v1/entities/urn:ngsi-ld:Building:1?options=sysAttrs' \
-H 'Accept: application/json'
let building = EntityId::from(iri!("urn:ngsi-ld:Building:1"));
let retrieved = client
.entities()
.retrieve(&building)
.system_attributes(SystemAttributes::Include)
.send()
.await?;
response = requests.get(
f"{BASE}/entities/urn:ngsi-ld:Building:1",
params={"options": "sysAttrs"},
headers={"Accept": "application/json"},
)
assert response.status_code == 200
{
"id": "urn:ngsi-ld:Building:1",
"type": "Building",
"temperature": {
"type": "Property",
"value": 21,
"createdAt": "2026-10-26T08:00:00.000Z",
"modifiedAt": "2026-10-26T08:00:00.000Z",
"accuracy": {
"type": "Property",
"value": 0.5,
"createdAt": "2026-10-26T08:00:00.000Z",
"modifiedAt": "2026-10-26T08:00:00.000Z"
}
}
}
The same read without options=sysAttrs answers neither member. Replacing the entity keeps its createdAt, because the identity is the same entity and only the content is new.
You cannot set them
A createdAt or modifiedAt in a request body is not an error, and it is not kept either. The broker replaces whatever you claim with its own stamps, and the sysAttrs read shows its instants, never yours. observedAt exists precisely because there has to be a member for the time the client does own.
observedAt
observedAt is when the value was observed in the world, as opposed to when it reached the broker. A sensor that writes last night's reading this morning sets observedAt to last night. The broker keeps the instant you wrote, served in one spelling however you wrote it, to the microsecond.
It is an attribute member like any other, so q reaches it. The time is compared as an instant when you write the operand bare.
- http
- curl
- rust
- python
GET /ngsi-ld/v1/entities?type=Building&q=temperature.observedAt>2024-03-01T00:00:00Z
curl -i 'http://localhost:1056/ngsi-ld/v1/entities?type=Building&q=temperature.observedAt>2024-03-01T00:00:00Z'
let since: DateTime = "2024-03-01T00:00:00Z".parse()?;
let page = client
.entities()
.query()
.entity_type("Building")
.q(q::attr("temperature").sub_attr("observedAt").gt(since))
.send()
.await?;
response = requests.get(
f"{BASE}/entities",
params={
"type": "Building",
"q": "temperature.observedAt>2024-03-01T00:00:00Z",
},
)
assert response.status_code == 200
The same text in quotes compares as a string and matches a declared DateTime never. Time values in q has that rule in full.
expiresAt
expiresAt is a request, not an observation. Store this until then, then let it go. It exists at two levels that mean different things. An entity-level expiresAt retires the whole entity, and an attribute-level one retires that attribute. An instance of a multi-datasetId attribute carries its own, so the GPS reading can outlive the speedometer's.
{
"id": "urn:ngsi-ld:Vehicle:A4567",
"type": "Vehicle",
"expiresAt": "2099-01-01T00:00:00Z",
"speed": {
"type": "Property",
"value": 60,
"expiresAt": "2098-06-01T00:00:00Z"
}
}
The two stand beside each other, neither taking the other's instant. An attribute past its expiresAt disappears from reads while the entity and the other attributes stand, and the entity past its own is gone entirely. The member reads back under options=sysAttrs, and q can select on it, as in q=speed.expiresAt>2150-01-01T00:00:00Z.
deletedAt
A deletion is stamped too, but there is nothing left to stamp on the current state, so deletedAt shows up in two places. In the temporal history a deleted attribute reads back as the NGSI-LD Null beside its deletedAt, and in change notifications each deleted instance carries the instant it was removed. The temporal guide shows the history shapes.
Querying and ordering by time
q compares the timestamps you can name, with observedAt and any user-defined time Property compared as instants when written bare, and expiresAt as an ordinary member. The temporal API adds its own window parameters, timerel with timeAt, which select history by observedAt rather than by the broker's clocks, and a temporal q on modifiedAt also admits the instances that were in effect when the window opened, even though they were modified before it (see Temporal queries).
Where else clocks appear
Notifications carry notifiedAt, the instant the notification was sent, beside the entity's own stamps. A subscription's timeInterval and throttling are durations, not instants. They measure gaps between notifications rather than pointing at a moment. A snapshot reports lastUsedAt, when something last read or wrote it, which decides eviction order (see Snapshots).