A device that loses its connection does not have to lose its measurements. Keep them on the device while the link is down, then send them afterwards with the times they were actually taken. The history fills in as if nothing had happened.
Without buffering, an outage leaves a hole. Worse, the hole is invisible: the chart simply has no points between two timestamps, and nothing on the page says whether the sensor was quiet, the device was off, or the network was down.
With buffering, the same outage costs nothing. The readings arrive late but land in the right place, and the record is continuous — which matters when the data is not just a dashboard but a quality record, a billing input, or the evidence for what a machine did last Tuesday.
Every write to POST /api/data/write may carry an optional time field.
When it is present and valid, the reading is stored at that time. When it is absent, the reading
is stored at the moment it arrived.
That single field is the whole mechanism. A device that has been buffering simply sends its backlog one request at a time, each with the timestamp it recorded.
| Field | Meaning |
|---|---|
time |
When the reading was taken. Any format JavaScript's Date understands —
an ISO 8601 string such as 2026-09-07T14:32:05Z is the safe choice.
Use UTC. An unparseable value is not an error: the reading is stored
with the arrival time instead, silently. |
device_name |
The device id as created in the console. It must already exist — the API never
creates devices, and an unknown name returns 404. |
field + value |
A single reading. |
data |
An array of { field, value } — several fields at the same
timestamp. See the limitation below. |
time applies to the whole request. The data array sends many
fields at one moment — it does not send many moments. To replay a backlog of ten
readings you send ten requests, each with its own time.
A normal write, and the same write with a timestamp:
// Live reading — stored at the moment it arrives POST /api/data/write x-api-key: YOUR_API_KEY { "device_name": "gateway_1", "field": "temperature", "value": 21.4 } // Buffered reading — stored where it belongs in the history POST /api/data/write x-api-key: YOUR_API_KEY { "device_name": "gateway_1", "field": "temperature", "value": 21.4, "time": "2026-09-07T14:32:05Z" }
Several fields recorded at the same instant travel together:
{
"device_name": "gateway_1",
"time": "2026-09-07T14:32:05Z",
"data": [
{ "field": "temperature", "value": 21.4 },
{ "field": "humidity", "value": 63 }
]
}
The device needs three things: somewhere to keep readings, a rule for when to keep them, and a replay loop for when the link returns.
On an ESP32, a file in SPIFFS or LittleFS is usually enough — one line per reading, timestamp first. A file survives a reset, which RAM does not, and a reset is exactly what tends to happen during a power problem. Decide a maximum size and drop the oldest lines when you reach it: a device that fills its flash during a long outage is worse than one that loses the oldest hour.
Buffering is only useful if the device knows what time it is. Sync with NTP while the network is up and keep the value across the outage. A device with no clock should not buffer — it would replay readings with invented times, which is worse than a visible gap.
time and accept that they land
at arrival time — or discard them. A history that looks continuous but is wrong cannot be
detected later by anyone.
When the connection returns, send the oldest reading first and delete each line only after the
server has answered 200. If a request fails, stop and try again later — do not skip
ahead, or you will create a gap inside the replay itself.
while (buffer.hasLines() && wifiConnected) { Line line = buffer.peekOldest(); int code = postReading(line.field, line.value, line.timestamp); if (code == 200) buffer.removeOldest(); else break; // retry later, keep the order delay(150); // stay under the rate limit }
429 with a Retry-After header. A short delay between requests,
as above, keeps a long backlog well inside the limit. Treat 429 like a failure:
wait and resume from the same reading.
A field that is not marked as historical keeps only its latest value — each write replaces the previous one. Replaying a backlog into such a field does not build a history; it leaves you with whichever reading happened to be sent last, which during a replay is an old one. Open the field in the console and make sure history is enabled before you buffer anything for it.
Readings that arrive over MQTT are stored at the moment they arrive. There is no timestamp in
the topic or the payload, so a backlog cannot be replayed that way. A device that uses MQTT for
live values can still use POST /api/data/write for its backlog — the two paths
write to the same fields, and mixing them is fine.
This is true of the Virtuino Cloud platform, and it is the opposite of what the standalone Virtuino IoT app does. That app is a separate product with its own local storage, and there the timestamp travels in the MQTT payload. If that is what you are building against, follow Store and Forward for the Virtuino IoT app instead — the two pages describe different systems and the settings do not transfer.
Stored readings are removed once they pass the retention window of your plan. Replaying data from an outage of hours or days is unaffected. Importing history that is older than your retention window is not useful: it will be cleaned up on the next pass.
For the full write API — authentication, every field, and the error responses — see the HTTP API documentation. For alerting when a device stops reporting at all, see Device Online/Offline Detection.