Your device loses connection, keeps measuring, and sends everything once it is back — with the times the readings actually happened, not the time they arrived.
By default, Virtuino IoT stamps a reading with the moment it arrives. That is the right answer almost always — but not when a device has been offline. A datalogger that kept measuring through a four-hour power cut has four hours of real history in its memory, and if it publishes it on reconnection, all of it lands on the chart at the same instant: one vertical wall of points where a curve should be.
The app can place each reading at its own time instead. There are two ways to do it, and the first one works no matter how your device talks to the app.
Your device sends its buffer however it likes, and a short script places each reading at its own time. Because the script does the work, the protocol does not matter — WebSocket, MQTT, HTTP, Modbus, anything.
On MQTT connections the app can read the time straight out of the message. No script, no parsing — one setting on the variable.
A script can return many readings at once, each with its own time. That is the whole mechanism, and it is why the connection type is irrelevant: your device only has to get the buffered data into the app somehow, in whatever shape suits your firmware. The script turns that into properly timed readings.
Put the backlog into a variable as text. The format is entirely yours — the script is what reads it. A compact and easy one is value and time in pairs:
21.4,1757600000000;21.6,1757600060000;21.9,1757600120000Send it the way your connection already sends anything else. On a WebSocket connection that is one entry of the JSON message; on MQTT it is the payload of a topic; on HTTP it is the value the app reads. None of that changes what follows.
The script's returned value may be a single object or an array of them. Each object names a variable, its value, and the moment it was measured:
result = '[{"tag":"temp","value":21.4,"timestamp":1757600000000},' + ' {"tag":"temp","value":21.6,"timestamp":1757600060000},' + ' {"tag":"temp","value":21.9,"timestamp":1757600120000}]';| Field | What to put in it |
|---|---|
| tag | One of the script's own variable tags — the variable the reading belongs to, and the one your data bucket records. |
| value | The reading. Numbers and text are both fine. |
| timestamp | The moment it was measured, as Unix time in milliseconds. A plain number — text dates are not accepted here. |
The time only works on the returned value, not on Virtuino.writeValue.
Writing through Virtuino.writeValue always stamps the reading with the
current moment; there is no way to pass a time with it. To place a reading in the past,
it has to go through what the script returns.
This script reads the buffer from one variable, splits it, and returns every reading with its own time:
var buffer = Virtuino.readValue("bufferTag"); // "21.4,1757600000000;21.6,1757600060000" var out = []; if (buffer != null && buffer.length > 0) { var samples = buffer.split(";"); for (var i = 0; i < samples.length; i++) { var part = samples[i].split(","); if (part.length == 2) { out.push('{"tag":"temp","value":' + part[0] + ',"timestamp":' + part[1] + '}'); } } } result = out.length > 0 ? '[' + out.join(',') + ']' : null;Set the script to run when bufferTag changes, so it
fires the moment the backlog arrives. A ready-made script that copies a value together with
its time is in the script examples as
copy_value_and_timestamp.js.
On an MQTT connection you can skip the script entirely. The app will take the time out of the message itself, configured per variable in the variable's settings.
Send the reading and its time as two keys, name them whatever you like:
{"value": 21.4, "t": 1757600000000}Then fill in Date Json Path with the name of the
time key — t here. Leaving that field empty keeps the default behaviour, so
existing projects are unaffected until you fill it in. It is a path rather than a plain
key, so nested values and array elements are reachable too:
| Setting | Send | Example |
|---|---|---|
| Timestamp | Unix time. Milliseconds, or plain seconds — a 10-digit value is recognised as seconds and converted for you. | 1757600000000 1757600000 |
| Iso8601 | Standard ISO 8601. The easiest option if your firmware already formats dates as text. | 2026-09-11T14:03:00Z |
| Custom date format | Anything else, described with a pattern you supply. | dd/MM/yyyy HH:mm:ss |
If you send Unix time, turn “Convert UTC to local time” off.
That option is on by default, and it exists for text dates that carry no zone of their own. Unix time already refers to one exact instant worldwide, so converting it shifts every reading by your time-zone offset — three hours in Greece, eight in California. The symptom is history that looks correct in shape but sits at the wrong hour.
Leave it on only when you send a text date written in UTC and you want it displayed in local time.
One message carries one reading on this route, so a device with five hundred buffered samples publishes five hundred messages. If you would rather send the backlog in a few large messages, use the script route above instead — that is exactly what it is for.
Whichever route you take, one rule matters more than the rest:
Send the backlog oldest first, and finish it before you resume live values.
Charts are built on the assumption that readings arrive in chronological order. While a chart is open on screen, it draws only what is newer than the newest point it already holds — so a reading older than that is stored correctly, but does not appear until the chart is opened again. Drain the buffer first, then start sending live values, and everything lands in order the first time.
| Limit | What it means |
|---|---|
| Chronological order | Older readings mixed into a live stream are stored but not drawn until the chart is reopened. See the rule above. |
| Milliseconds only | On the script route the timestamp must be a number in milliseconds. The MQTT route is the one that also accepts text dates. |
| Unparseable time | If a time is missing or cannot be read, the app falls back to the arrival time rather than rejecting the reading. It does this silently, so test with a handful of samples before trusting a long replay. |
| Chunk size | A buffer arrives as the value of a variable, so send it in reasonable pieces rather than one very long string. |
Automatic clearing of a data bucket is off unless you switch it on, and when you do, the period is whatever you set. Replayed history is kept under exactly the same rule as live data.
Using Virtuino Cloud instead of the app?
The platform has its own store-and-forward mechanism, and the rules are different — there the timestamp travels over HTTP, and retention is tied to your plan. It is documented separately in Store and Forward for Virtuino Cloud.