Skip to content

5. Device clock

Device clocks drift and can be set by hand. The backend needs the device’s idea of “now” at send time to correct every timestamp in a batch.

  • MUST send the device time at the moment of sending in the Apsio-Sent-At header, in Unix nanoseconds, as a decimal string.
  • A retry MUST update Apsio-Sent-At to the time of that attempt. It is the one value that changes between retries; the body and the batch id do not.
  • MUST NOT adjust record timestamps on the device.
  • An upload omits Apsio-Sent-At only when no SDK code runs at send time, because the operating system sends it on the SDK’s behalf (a background URLSession on Apple platforms). Such an upload leaves at a time the SDK cannot know, and MUST omit the header rather than send a stale value, which would make the backend shift every timestamp by the delay; the backend then stores the original times uncorrected.
  • Every other upload, including one made by SDK code in a background job (a WorkManager job on Android), MUST set Apsio-Sent-At at the time it sends.

For each request, the backend computes:

offset = received_at − sent_at

where received_at is the backend’s own clock when the request arrived and sent_at is Apsio-Sent-At. For every record in the request it then stores three times:

Time Value
Original The record’s timestamp, exactly as sent
Observed received_at
Corrected original + offset when `
  • A backend MUST store the original and observed times unchanged.
  • The 2-minute threshold exists because a small offset is mostly network latency; correcting it would add jitter between batches.
  • The same offset applies to every timestamp of a record: timeUnixNano and observedTimeUnixNano of logs, start and end of spans, start and end of metric points, and timestamp attributes such as apsio.session.start_time.
  • When Apsio-Sent-At is missing or not a valid integer, the corrected time equals the original and the backend records that no correction was possible.
  • A record whose corrected time is more than 1 hour after received_at is in the future even after correction; a backend MAY clamp it to received_at and MUST keep the original.