A common early mistake in IoT platform design is assuming a mobile app or dashboard can just query a device directly for its current state. In practice, most devices are asleep, offline, or on an intermittent connection most of the time — especially battery-powered ones — so a direct query usually times out or returns stale information. A device shadow (also called a digital twin, though that term is used more broadly elsewhere) solves this by maintaining a cloud-side representation of device state that apps interact with instead of the device itself.
What a device shadow actually is
A device shadow is a cloud-stored JSON-like document per device, holding the last known reported state (what the device last told the cloud about itself) and, often, a desired state (what the app or system wants the device to become). Apps read and write to the shadow, not to the device directly. The device, whenever it connects, syncs the shadow — pushing its current reported state and pulling any pending desired-state changes to act on. This decouples app interactions from device connectivity: an app can "set" a device's configuration at any time, and it takes effect the next time the device actually connects, without the app needing to know or care whether the device is online right now.
Reported state vs. desired state
- Reported state — the device's last known actual state, updated by the device itself whenever it connects or its state changes
- Desired state — the state an app or automation wants the device to reach, which the device picks up and acts on when it next connects
- Delta — the difference between desired and reported state, which is what the device actually needs to process on its next sync, rather than the device having to compare full state documents itself
This reported/desired split is what makes the pattern useful for offline devices specifically — a change requested while a device is offline isn't lost or immediately failed, it's queued as part of the desired state and applied on next connection.
Sync conflict handling
Because a device and its cloud shadow can diverge — the device may have changed state locally (a button press, a sensor threshold trigger) between syncs — the sync protocol needs a clear conflict resolution rule. Common approaches include version numbers or timestamps on state changes, with a defined precedence rule (device-reported state usually wins for anything the device controls directly, cloud desired-state wins for remote configuration) applied consistently rather than left ambiguous.
Where this pattern earns its complexity
- Apps and dashboards that need to show device state instantly without waiting on a live device connection
- Configuration changes queued for offline or low-power devices, applied automatically whenever the device next connects
- Fleet-wide queries and automations ("show me all devices with firmware below version X") that need to run against cloud-side state rather than polling every device live
- Multi-client consistency — several apps or users interacting with the same device see a consistent view via the shared shadow, rather than racing to query the device directly
For very simple products with a single companion app and no fleet-wide management needs, a full shadow architecture can be more infrastructure than the product needs — it's a pattern that earns its complexity at fleet scale or with multi-client access requirements, not a default for every connected product.
How we approach this
We size the shadow/twin architecture to the product's actual connectivity pattern and fleet-management needs, rather than defaulting to it regardless of scale. See our cloud & device platform work for how this fits into a full fleet-management build.
