CoAP Observe Notifications: Reject Older State Without Assuming Every Update Arrives
Maintain a current-state observation, not a presumed complete event stream. Check notification ordering before replacing the current value, track its freshness separately, and expose an explicit stale state when the usable age expires. A sequence gap does not by itself prove a lost application event.
This procedure targets RFC 7641 Observe over CoAP UDP. It includes a host-checked ordering predicate and proposed integration fixtures. It does not qualify a particular CoAP library, radio path, security implementation, or real-time control system. Blockwise notifications require additional representation-assembly checks outside this scope.
1. Keep one coherent observation record
For each active observation retain the resource identity, peer/security context, Token association, last accepted Observe value, client-local reception time, Max-Age, representation, and application quality state. Update that record coherently so a consumer cannot see an old payload paired with a new age.
Use a separate local observation generation to reject callbacks belonging to an earlier subscription instance. That generation is an application bookkeeping field; it does not appear in the protocol and cannot repair an incorrectly associated packet. Establish the supported library’s ownership and cancellation rules first.
RFC 7641 explicitly allows skipped intermediate resource states. The service aims to keep observed state current rather than deliver every transition. See the Observe freshness model. If the product must retain every alarm transition or pulse, define a separate event-history service instead of inferring completeness from Observe.

2. Use the actual serial-order rule
RFC 7641 section 3.4 defines a 24-bit Observe value. With old value V1, incoming value V2, and elapsed client-local time since the freshest accepted notification, the incoming notification is more recent when either forward difference is below 223, wraparound difference exceeds 223, or elapsed time is greater than 128 seconds. The inequalities matter. See the normative reordering rule.
A plain unsigned comparison fails at wraparound. Conversely, accepting every different value can let a delayed older notification overwrite newer state. Perform protocol association and security validation before applying this ordering test; a plausible sequence number is not authentication.
| Previous / incoming value | Elapsed time | Ordering predicate |
|---|---|---|
| 100 / 101 | 1 second | More recent. |
| 100 / 99 | 1 second | Not more recent. |
| 0xFFFFFE / 1 | 1 second | More recent across wrap. |
| 100 / 100 | 1 second | Not more recent. |
| 0 / 223 | 1 second | Neither strict serial inequality passes. |
| 100 / 99 | Exactly 128 seconds | The elapsed-time branch does not pass. |
| 100 / 99 | 128.001 seconds | The elapsed-time branch passes. |
An original Python predicate matching these conditions passed all seven cases on Python 3.12.14. That checks arithmetic and boundary expectations only. It does not establish correct packet parsing, monotonic-clock integration, or notification delivery on a device.
3. Do not confuse ordering with usable age
The 128-second condition is an ordering rule, not a promise that a measurement remains usable for 128 seconds. CoAP’s Max-Age freshness model governs cached response age, and the product can impose a stricter application limit.
Define states such as never received, usable, stale, and observation ended. If freshness expires, preserve the last value only with its stale label and permitted uses. Do not reset the age because a dashboard redraws or republishes the same cached value. An old notification rejected by ordering should not make the displayed current value look newly received.
Also distinguish protocol resource freshness from physical acquisition age. A server may represent a cached sensor sample. If the product needs source acquisition time, require it in the resource contract; do not derive it from the Observe value or packet arrival alone.
4. Re-establish interest without a retry storm
Document how the selected stack re-registers after stale state, a peer restart, or local observation loss. Follow RFC 7641’s re-registration and collision-avoidance guidance rather than issuing immediate repeated GETs whenever a timer fires. Keep recovery attempts bounded and visible.
Cancellation also needs a lifecycle. A locally closed view should not leave its callbacks able to update a reused UI object. At the same time, the protocol stack may need to reject or acknowledge incoming traffic according to the observation’s actual state. Do not implement a blanket packet drop that bypasses its required handling.
Secure request/response binding must remain valid when Tokens and observation instances are retired or replaced. Review the applicable RFC 9175 Token-processing requirements with the library instead of assuming that a new local object makes an old Token safe to reuse.
5. Qualify state and age together
- Deliver a newer notification followed by an older one; require the displayed value and freshness record to remain tied to the newer observation.
- Exercise wraparound, equality, half-range, and elapsed-time boundary cases through the actual parser and timer implementation.
- Skip several intermediate values and confirm the application does not claim an exact lost-event count.
- Stop notifications while keeping the transport reachable; require transition to stale at the configured age boundary.
- Re-register and then deliver a callback from the old local observation instance; require rejection by its owner.
- Cancel while a notification callback is queued and verify memory lifetime and terminal state.
- Restart the peer and exercise the documented recovery path without assuming its old Observe sequence continues forever.
Record accepted representation identity, order decision, age, quality state, and recovery action. Preserve failed cases. A count of received notifications cannot reveal whether the consumer temporarily used older state.
Hypothetical consumer policy
A status panel receives states numbered 300 and 304, then a delayed 302. The panel should keep 304 under the ordering rule, but it cannot infer that exactly three application events were lost. If no usable notification arrives before its freshness limit, it shows the last state as stale and initiates the approved recovery path.
Release the ordering fixtures, age policy, and observation-lifetime tests together. Observe can support a useful current-state interface when gaps, reordering, and silence are all given explicit meanings.