Chapters

13 CoAP Observe: Subscriptions and Updates

coap
protocols
observe
server-push

13.1 Start With the Decision

A thermostat needs fresh changes, not a flood of copies after a link returns. Observe must bind each update to one live relation.

13.2 Route Overview

This is part 1 of 2. Continue with CoAP Observe: Efficiency and Lifecycle.

13.3 Part Objectives

  • Trace registration, notification, token, and sequence state.
  • Set update pace and recovery rules for a CoAP observer.
In 60 Seconds

Prove One Update Without Building a Flood

Picture a thermostat that gets no warning when a room freezes, then receives many copies after reconnecting. The useful result is a fresh change at the right pace, not merely an open subscription.

A protocol means the shared rules for a message exchange. A broker means a service that receives and forwards named messages. Telemetry means measured status sent from a device. CoAP means Constrained Application Protocol, a compact system for small devices. MQTT means Message Queuing Telemetry Transport, a broker-based system. Both can carry updates, but they manage subscriptions in different ways.

Register one observer, change the value, lose a notification, reconnect, and remove the observer. Record identity, sequence, time, threshold, rate, final value, and cleanup. Reject stale order and cap a rapid burst.

This runway does not prove every push design or energy saving. The deeper sections explain Observe registration, freshness, pacing, cancellation, edge cases, and measured comparison with polling.

The CoAP Observe extension (RFC 7641) enables server-push notifications by letting a client register interest in a resource once, then automatically receiving updates whenever the value changes. This eliminates polling overhead — instead of 8,640 GET requests per day for 10-second updates, the server pushes only on change, reducing traffic by 90%+ for slowly-changing sensor data.

13.4 Start With the Quiet Thermostat

A thermostat does not need to ask a room sensor the same question every ten seconds when the temperature barely changes. It needs one registration, then a trustworthy update only when the value crosses a meaningful boundary.

Observe is that subscription story for CoAP. The chapter builds from the simple “tell me when it changes” promise into registries, notification pacing, cleanup, edge cases, and evidence that the push stream saves energy instead of creating a flood.

The mathematical gist. Polling creates 1,440 daily radio events, while Observe creates one registration plus 240 notifications, or 241 events. If each wake-transmit-sleep cycle costs about 3.00 mJ, energy falls from 4.32 to 0.723 J/day, an 83.3% event-energy saving. That differs from the chapter’s 89.3% byte saving because wake overhead does not shrink with payload size. A 225 mAh, 3.0 V cell stores 2,430 J, so the same ledger gives 563 versus 3,361 idealised days.

Math Bridge · guided foundationsWhy do event savings and byte savings disagree?Let Eddie connect radio wakes, joules, traffic, and an idealised cell life.
Chapter Roadmap
  • In 60 Seconds
  • Start With the Quiet Thermostat
  • Phoebe’s Field Notes: Why 89% Less Traffic Is Not 89% More Battery
  • Phoebe’s Field Notes: Count Radio Wakes, Not Only Bytes
  • Quick Check: Observe Fit
  • Prerequisites
  • Continue: CoAP Observe Registration and Freshness Contracts
  • The Observe Extension (RFC 7641)
  • Minimum Viable Understanding: CoAP Observe
  • Smart Updates Without Asking!
  • For Beginners: Understanding Observe vs Polling
  • Putting Numbers to It
  • Checkpoint: Push or Poll?
  • Interactive Calculator: Polling vs Observe Bandwidth
  • Observe Protocol Flow
  • Try It: Deregistration Method Advisor
  • Observer Management Implementation
  • Try It: Observer Registry Simulator
  • Putting Numbers to It
  • Interactive: CoAP Congestion Control
  • Interactive Calculator: Rate Limiting Configuration
  • Checkpoint: Registry Discipline
  • Deep Dive: Observe Internals
  • Deep Dive: Observe Sequence Numbers and Ordering
  • Interactive Tool: Sequence Number Freshness Checker
  • Deep Dive: Observer Removal Conditions
  • Try It: CON Retransmission Timeline Simulator
  • Edge Cases and Gotchas

13.5 Learning Objectives

By the end of this chapter, you will be able to:

  • Implement Server Push: Configure and build CoAP Observe for real-time notifications using RFC 7641
  • Design Observer Registries: Construct server-side data structures to manage registration, notifications, and deregistration lifecycle
  • Calculate Bandwidth Savings: Apply formulas to quantify Observe benefits over traditional polling for specific IoT scenarios
  • Diagnose Edge Cases: Identify and resolve NAT timeout, token reuse, and ghost observer problems in deployed systems
  • Evaluate Protocol Trade-offs: Justify selecting Observe versus polling versus MQTT based on resource change rate and energy constraints
  • Apply Rate Limiting: Configure change-threshold and interval parameters to prevent notification floods on rapidly-changing resources
Quick Check: Observe Fit

13.6 Prerequisites

Before diving into this chapter, you should be familiar with:

13.7 Continue: CoAP Observe Registration and Freshness Contracts

The main chapter below stays focused on Observe as a server-push pattern and implementation workflow. For the deeper contract behind registration-token binding, Max-Age freshness, Confirmable liveness probes, notification ordering, wraparound, and stale-update handling, continue to CoAP Observe Registration and Freshness Contracts.

13.8 The Observe Extension (RFC 7641)

Minimum Viable Understanding: CoAP Observe

Core Concept: Observe transforms CoAP from pure request-response into a publish-subscribe pattern. A client registers interest in a resource once, then receives automatic notifications whenever the resource changes - no repeated polling needed.

Why It Matters: Polling wastes energy and bandwidth. If you need temperature updates every 10 seconds, polling requires 8,640 GET requests/day. With Observe, the server pushes only when values change, potentially reducing traffic by 90%+ for slowly-changing resources.

Key Takeaway: Register with Observe: 0 in your GET request, receive notifications with incrementing sequence numbers, and deregister with Observe: 1 or RST when done.

Meet our friends: Temperature Terry, Lila the Light, and the microcontroller!

Sammy says: “Imagine you want to know the temperature in your room. You could keep asking me every minute - ‘What’s the temperature? What’s the temperature?’ - but that’s SO tiring for both of us!”

Lila explains: “With CoAP Observe, it’s like subscribing to a YouTube channel! You subscribe ONCE, and then you automatically get notified whenever there’s a new video - or in our case, a new temperature reading!”

Real-world example: Think about getting text messages from your favorite pizza place. You don’t call them every 5 minutes asking “Is my pizza ready?” - that would be annoying! Instead, they TEXT YOU when it’s ready. That’s exactly what Observe does for IoT devices!

Max’s tip: “Observe is like a magic subscription service:

  1. Subscribe once (I want to know about temperature)
  2. Relax while the sensor does its job
  3. Get notified only when something changes
  4. Unsubscribe when you’re done - no more messages!”

Why it’s awesome: Less talking = more battery life! Your smart devices can last much longer because they’re not constantly asking “Any updates? Any updates?” - they just wait patiently for news!

What is polling? Polling is like repeatedly asking “Are we there yet?” on a road trip. You keep asking the same question over and over, even if the answer hasn’t changed.

What is Observe? Observe is like asking your parent to tell you when you arrive. You ask once, then relax - they’ll let you know when something changes.

Why does this matter for IoT?

  • Battery life: Every time a device sends a message, it uses power. Fewer messages = longer battery life.
  • Network traffic: If 1,000 sensors all poll every second, that’s 1,000 messages per second! With Observe, you might only send 10 messages when values actually change.
  • Speed: With polling, you might not know about changes for up to your polling interval. With Observe, you know immediately.

Simple rule: Use Observe when you want real-time updates without wasting energy.

13.8.1 Traditional Polling vs. Observe

The energy question is not simply “push or poll”; it is how many exchanges occur when a resource changes less often than a client would ask for it. Inspect Figure 13.1 to compare those timelines before reading the 24-hour totals.

Parallel timelines compare repeated polling GETs with one Observe registration and change notifications. Observe avoids repeated GETs while the representation is unchanged.
Figure 13.1: Sequence diagram comparing CoAP polling (repeated GET requests) versus Observe pattern (single registration with change-driven notifications), showing reduced message exchange with Observe

Read Figure 13.1 from left to right along each lane. Polling repeats a GET and response even when the value has not changed; Observe pays once to register, then the server emits notifications when the representation changes. The diagram explains the message-count difference, while the figures below quantify it for one assumed polling interval and change rate rather than claiming the same saving for every workload.

Traffic comparison for 24 hours of temperature monitoring:

  • Polling (every 60 seconds): 1,440 requests + 1,440 responses, about 62 kB, high battery impact.
  • Observe (10 changes/hour): 1 registration + 240 notifications, about 6.6 kB, about 89% less traffic.

We can quantify the exact bandwidth savings. For polling every 60 seconds over 24 hours:

Poll Messages=24×3,600 s60 s=1,440 requests\text{Poll Messages} = \frac{24 \times 3{,}600 \text{ s}}{60 \text{ s}} = 1{,}440 \text{ requests}

With CoAP request (16 bytes) + response (28 bytes) = 44 bytes per exchange:

Poll Bandwidth=1,440×44=63,360 bytes61.9 KB\text{Poll Bandwidth} = 1{,}440 \times 44 = 63{,}360 \text{ bytes} \approx 61.9 \text{ KB}

For Observe with 10 changes/hour over 24 hours:

Observe Messages=1 (registration)+(10×24)=241 total\text{Observe Messages} = 1 \text{ (registration)} + (10 \times 24) = 241 \text{ total}

Observe Bandwidth=44+(240×28)=6,764 bytes6.6 KB\text{Observe Bandwidth} = 44 + (240 \times 28) = 6{,}764 \text{ bytes} \approx 6.6 \text{ KB}

The bandwidth reduction is:

Savings=61.96.661.90.893=89.3% reduction\text{Savings} = \frac{61.9 - 6.6}{61.9} \approx 0.893 = 89.3\% \text{ reduction}

13.8.2 Observer Architecture Overview

Before treating notifications as isolated responses, inspect Figure 13.2 to see the state that the client, wire exchange, and server must preserve together.

CoAP Observe state contract separating client state, wire identity and ordering, server observer registry and retention, failure ownership, and the bounded evidence needed before a notification is accepted by the application.
Figure 13.2: CoAP Observe state contract across client, wire, and server, including registry retention, ordering, failure ownership, and bounded acceptance evidence

Read the architecture in Figure 13.2 from client state through Token and Observe ordering on the wire to the server’s observer registry. Then follow failure ownership and the bounded evidence required before the application accepts a notification. Multiple clients can observe one resource, but each relationship needs its own retained state; the next sections trace registration and notification behavior inside that contract.

Broker BexCheckpoint: Push or Poll?
  • You now know why polling every 60 seconds creates 1,440 requests and 1,440 responses over 24 hours.
  • You now know how Observe reduces that same example to one registration plus 240 notifications, about 89% less traffic.
  • You now know the first design question: whether the resource changes far less often than the client would poll it.
Interactive Calculator: Polling vs Observe Bandwidth

How to use: Adjust the sliders to match your scenario. The calculator shows total messages, bandwidth, and savings percentage. Use this to decide whether Observe is worth implementing for your specific use case.

13.9 Observe Protocol Flow

13.9.1 Registration

Client sends GET with Observe: 0 to register:

Request Read these points as one connected sequence: start with Client -> Server: GET /temperature; then Token: 0xAB12; then Observe: 0 (register); and finish with Accept: text/plain.

  • Client -> Server: GET /temperature
  • Token: 0xAB12
  • Observe: 0 (register)
  • Accept: text/plain

Response Read these points as one connected sequence: start with Server -> Client: 2.05 Content; then Token: 0xAB12; then Observe: 1 (sequence number); then Max-Age: 60; and finish with Payload: "23.5".

  • Server -> Client: 2.05 Content
  • Token: 0xAB12
  • Observe: 1 (sequence number)
  • Max-Age: 60
  • Payload: "23.5"

13.9.2 Notifications

A notification is useful only if the client can prove that it belongs to the relationship and is fresher than what it already accepted. Inspect the lifecycle in Figure 13.3 beyond the simple phrase “server pushes updates.”

CoAP Observe lifecycle showing registration and token continuity, freshness and ordering checks, transport acknowledgment versus application acceptance, cancellation and relationship loss, re-registration and reconciliation, and the evidence retained at each boundary.
Figure 13.3: CoAP Observe evidence lifecycle from registration through ordered notifications, cancellation or loss, and re-registration with reconciliation

Trace Figure 13.3 from registration through successive notifications. Token continuity identifies the observation, while Observe values and freshness rules help reject stale or reordered updates; a transport ACK confirms delivery but does not by itself prove that the application accepted the value. Cancellation or relationship loss ends that state, and re-registration requires reconciliation. That lifecycle frames the sequence-number knowledge check next.

13.9.3 Deregistration

Three ways to stop receiving notifications:

1. Explicit deregistration (GET with Observe: 1): Read these points as one connected sequence: start with Client -> Server: GET /temperature; then Token: 0xAB12; and finish with Observe: 1 (deregister).

  • Client -> Server: GET /temperature
  • Token: 0xAB12
  • Observe: 1 (deregister)

2. RST response to unwanted notification: Read these points as one connected sequence: start with Server -> Client: NON 2.05 Content (notification); and finish with Client -> Server: RST (stop this observation).

  • Server -> Client: NON 2.05 Content (notification)
  • Client -> Server: RST (stop this observation)

3. Timeout (Max-Age expiration): Keep one practical point in view: If the client does not refresh the observation within Max-Age,.

  • If the client does not refresh the observation within Max-Age, the server removes that observer entry.
Try It: Deregistration Method Advisor

How to use: Select a scenario to see the recommended deregistration method, the protocol exchange, and the energy cost. Toggle between CON and NON to see how notification reliability affects cleanup behavior.

The protocol flow gives the client-side contract: register with Observe: 0, accept ordered notifications, and leave cleanly with Observe: 1 or RST. The next layer is the server-side contract that remembers each observer without letting stale entries accumulate.

13.10 Observer Management Implementation

13.10.1 Server-Side Observer Registry

from collections import defaultdict
import time

class ObserverRegistry:
    def __init__(self):
        # Map: resource_uri -> list of Observer objects
        self.observers = defaultdict(list)
        self.observer_timeout = 86400  # 24 hours default

    def register_observer(self, resource_uri, client_addr, token, max_age=None):
        observer = Observer(
            client_addr=client_addr,
            token=token,
            registered_at=time.time(),
            last_notification=time.time(),
            timeout=max_age or self.observer_timeout
        )
        self.observers[resource_uri].append(observer)
        return observer

    def notify_all(self, resource_uri, value, content_format):
        """Send notification to all observers of a resource"""
        expired = []

        for observer in self.observers[resource_uri]:
            # Check if observation expired
            if time.time() - observer.registered_at > observer.timeout:
                expired.append(observer)
                continue

            # Send notification
            self.send_notification(observer, value, content_format)

        # Clean up expired observers
        for observer in expired:
            self.observers[resource_uri].remove(observer)

    def remove_observer(self, resource_uri, client_addr, token):
        """Remove observer on explicit deregistration or RST received"""
        self.observers[resource_uri] = [
            o for o in self.observers[resource_uri]
            if not (o.client_addr == client_addr and o.token == token)
        ]
Try It: Observer Registry Simulator

Interactive element unavailable — mutable cell

OJS `mutable` requires the Observable reactive runtime

Show source

mutable obsRegistry = new Map([
["/temperature", [{id: "client-01", token: "0xAB12", age: 120}, {id: "client-02", token: "0xCD34", age: 3400}]],
["/humidity", [{id: "client-03", token: "0xEF56", age: 60}]],
["/pressure", []],
["/light", [{id: "client-01", token: "0xGH78", age: 7200}]]
])

Interactive element unavailable — unsupported cell

obsRegistry: references an OJS `mutable` cell

Show source

obsResult = {
const registry = new Map(obsRegistry);
const uri = obsResourceUri;
const clientId = obsClientId;
const observers = registry.get(uri) || [];

if (obsAction === "Register") {
const exists = observers.some(o => o.id === clientId);
if (exists) return {status: "warning", msg: `Client '${clientId}' already registered on ${uri}. No duplicate added.`, registry};
if (observers.length >= obsMaxObservers) return {status: "error", msg: `Registry full: ${observers.length}/${obsMaxObservers} observers on ${uri}. Registration rejected.`, registry};
const token = "0x" + Math.random().toString(16).substring(2, 6).toUpperCase();
return {status: "success", msg: `Registered '${clientId}' on ${uri} with token ${token}. Total: ${observers.length + 1} observers.`, registry};
} else if (obsAction === "Deregister") {
const exists = observers.some(o => o.id === clientId);
if (!exists) return {status: "warning", msg: `Client '${clientId}' not found on ${uri}. Nothing to remove.`, registry};
return {status: "success", msg: `Deregistered '${clientId}' from ${uri}. Remaining: ${observers.length - 1} observers.`, registry};
} else {
const count = observers.length;
if (count === 0) return {status: "warning", msg: `No observers on ${uri}. No notifications sent.`, registry};
const expired = observers.filter(o => o.age > 3600).length;
return {status: "success", msg: `Notified ${count - expired} active observer(s) on ${uri}. ${expired > 0 ? expired + " expired observer(s) cleaned up." : "All observers current."}`, registry};
}
}

Interactive element unavailable — unsupported cell

obsRegistry: references an OJS `mutable` cell

Show source

html`
<div style="background: #f8f9fa; border-left: 4px solid ${obsResult.status === 'success' ? '#16A085' : obsResult.status === 'warning' ? '#E67E22' : '#E74C3C'}; padding: 15px; margin: 10px 0; border-radius: 4px;">
<h4 style="color: #2C3E50; margin-top: 0;">Registry Operation Result</h4>
<p style="font-size: 1.05em; color: ${obsResult.status === 'success' ? '#16A085' : obsResult.status === 'warning' ? '#E67E22' : '#E74C3C'}; font-weight: bold; margin-bottom: 12px;">
${obsResult.status === 'success' ? 'OK' : obsResult.status === 'warning' ? 'WARNING' : 'REJECTED'}: ${obsResult.msg}
</p>
<h5 style="color: #2C3E50; margin: 12px 0 6px;">Current Registry State</h5>
<table style="width: 100%; border-collapse: collapse; font-size: 0.9em;">
<tr style="background: #fff;">
<th style="padding: 6px 8px; text-align: left; border-bottom: 2px solid #3498DB;">Resource</th>
<th style="padding: 6px 8px; text-align: center; border-bottom: 2px solid #3498DB;">Observers</th>
<th style="padding: 6px 8px; text-align: left; border-bottom: 2px solid #3498DB;">Client IDs</th>
</tr>
${["/temperature", "/humidity", "/pressure", "/light"].map(uri => {
const obs = obsRegistry.get(uri) || [];
return `<tr>
<td style="padding: 6px 8px; border-bottom: 1px solid #ddd; font-family: monospace;">${uri}</td>
<td style="padding: 6px 8px; text-align: center; border-bottom: 1px solid #ddd; color: ${obs.length >= obsMaxObservers ? '#E74C3C' : '#2C3E50'}; font-weight: bold;">${obs.length}/${obsMaxObservers}</td>
<td style="padding: 6px 8px; border-bottom: 1px solid #ddd; font-size: 0.85em;">${obs.length > 0 ? obs.map(o => o.id).join(', ') : '<em style="color:#7F8C8D;">none</em>'}</td>
</tr>`;
}).join('')}
</table>
<p style="margin: 10px 0 0 0; color: #7F8C8D; font-size: 0.85em;">
<strong>Try:</strong> Register the same client twice (see duplicate detection). Fill a resource to its max limit (see rejection). Notify a resource with expired observers (age > 3600s) to see automatic cleanup.
</p>
</div>
`

13.10.2 Notification Rate Limiting

Prevent notification floods on rapidly-changing resources:

class RateLimitedResource:
    def __init__(self, min_interval=1.0, change_threshold=0.5):
        self.min_interval = min_interval    # Minimum seconds between notifications
        self.change_threshold = change_threshold  # Minimum change to trigger notification
        self.last_notify_time = {}          # Per-observer last notification time
        self.last_notified_value = {}       # Per-observer last sent value

    def on_value_change(self, new_value):
        now = time.time()

        for observer in self.observers:
            last_time = self.last_notify_time.get(observer.token, 0)
            last_value = self.last_notified_value.get(observer.token, None)

            # Check if we should notify
            should_notify = (
                last_value is None or
                abs(new_value - last_value) >= self.change_threshold or
                (now - last_time) >= self.min_interval
            )

            if should_notify:
                self.send_notification(observer, new_value)
                self.last_notify_time[observer.token] = now
                self.last_notified_value[observer.token] = new_value

The rate limiting logic above combines two thresholds to prevent notification storms. For a sensor with value v(t)v(t) at time tt, a notification is sent when:

notify=Δvθv OR Δtθt\text{notify} = |\Delta v| \geq \theta_v \text{ OR } \Delta t \geq \theta_t

where Δv=vnewvlast\Delta v = v_{\text{new}} - v_{\text{last}} is the value change and Δt=tnowtlast\Delta t = t_{\text{now}} - t_{\text{last}} is time since last notification.

For example, a temperature sensor monitoring a boiler room:

  • Change threshold: θv=0.5°C\theta_v = 0.5°\text{C}
  • Time threshold: θt=60s\theta_t = 60\text{s}

If temperature jumps from 80°C to 82°C in 10 seconds: Δv=8280=2°C0.5°Cnotify immediately|\Delta v| = |82 - 80| = 2°\text{C} \geq 0.5°\text{C} \Rightarrow \text{notify immediately}

If temperature drifts slowly from 80.0°C to 80.3°C over 65 seconds: Δv=0.3°C<0.5°C BUT Δt=65s60snotify|\Delta v| = 0.3°\text{C} < 0.5°\text{C} \text{ BUT } \Delta t = 65\text{s} \geq 60\text{s} \Rightarrow \text{notify}

This dual-threshold approach prevents both change-based flooding (rapid fluctuations) and staleness (no updates for too long).

Interactive: CoAP Congestion Control

Interactive Calculator: Rate Limiting Configuration

How to use: Configure your sensor’s update rate and desired notification thresholds. The calculator shows how many notifications will actually be sent and the reduction factor achieved by rate limiting.

Broker BexCheckpoint: Registry Discipline
  • You now know the registry must keep client address, token, registration time, and timeout for each observer.
  • You now know rate limiting combines a value threshold such as 0.5 degrees C with a time threshold such as 60 seconds.
  • You now know 100 observers at 10 changes per second would create 1,000 notifications per second without pacing.

13.11 Deep Dive: Observe Internals

The Observe option value is a sequence number that helps clients detect:

  1. Out-of-order notifications (UDP doesn’t guarantee ordering)
  2. Notification freshness (which update is newer)

Sequence Number Rules (RFC 7641 Section 4.4):

def is_notification_fresh(current_seq, new_seq):
    """
    Determine if new notification is fresher than current.
    Handles 24-bit wraparound.
    """
    # Sequence numbers are 24-bit (0 to 16,777,215)
    MAX_SEQ = (1 << 24) - 1

    # Calculate difference handling wraparound
    diff = (new_seq - current_seq) % (MAX_SEQ + 1)

    # If diff < 2^23, new is fresher (forward direction)
    # If diff >= 2^23, new is older (backward direction - out of order)
    return diff < (1 << 23)

Example scenario:

Notification 1: Observe=100, temp=22.5
Notification 2: Observe=102, temp=23.0  (arrived out of order)
Notification 3: Observe=101, temp=22.8

Client receives: 100 -> 102 -> 101
Client should display: 22.5 -> 23.0 (ignore 101, it's older than 102)
Interactive Tool: Sequence Number Freshness Checker

How to use: Enter current and new sequence numbers to see if the notification should be accepted or discarded. This tool implements RFC 7641 Section 4.4 sequence number comparison logic with 24-bit wraparound handling.

Automatic observer removal triggers:

Read these points as one connected sequence: start with RST received: Client sends RST in response to notification; then Timeout: No activity within observation lifetime; then CON notification fails: After 4 retransmissions without ACK; and finish with Resource deleted: Server removes all observers when resource gone.

  1. RST received: Client sends RST in response to notification
  2. Timeout: No activity within observation lifetime
  3. CON notification fails: After 4 retransmissions without ACK
  4. Resource deleted: Server removes all observers when resource gone

Retransmission behavior for CON notifications:

Server sends CON notification
Wait 2 seconds for ACK
Retransmit with same Message ID
Wait 4 seconds (exponential backoff)
Retransmit
Wait 8 seconds
Retransmit
Wait 16 seconds
Final attempt
After 4 failures -> Remove observer
Try It: CON Retransmission Timeline Simulator

How to use: Adjust the timeout parameters and backoff multiplier to see how the retransmission timeline changes. Set “ACK arrives after attempt #” to simulate scenarios where the client eventually responds. With the default settings, it takes about 30 seconds to detect an unreachable client.

Deep internals explain why the same notification stream can survive UDP reordering and unreachable clients. Now apply those mechanics to deployment failures, where restarts, NAT mappings, and stale tokens are the usual reasons Observe looks unreliable.

13.12 Edge Cases and Gotchas

13.12.1 Token Reuse After Client Restart

Problem:

- Client registers observation with Token=0x42
- Client crashes and restarts
- Server sends notification with Token=0x42
- Client doesn't recognize token (state lost) -> sends RST
- Server removes observer

Solutions:

  1. Server MUST remove observer when RST received
  2. Client should re-register observations after restart
  3. Consider persisting observation state to flash

13.12.2 NAT Timeout Issue

Problem:

UDP NAT mappings expire (typically 30-60 seconds)
- Client behind NAT registers observation
- Server tries to push notification 5 minutes later
- NAT mapping expired -> notification never reaches client

Solutions:

Read these points as one connected sequence: start with Server sends periodic keep-alive NON notifications (every 30 sec); then Client sends periodic re-registration (GET with Observe=0); then Use Max-Age option to set notification frequency; and finish with Consider CoAP over TCP for NAT-hostile networks.

  1. Server sends periodic keep-alive NON notifications (every 30 sec)
  2. Client sends periodic re-registration (GET with Observe=0)
  3. Use Max-Age option to set notification frequency
  4. Consider CoAP over TCP for NAT-hostile networks

13.13 Continue to the Next Part

Carry this evidence into CoAP Observe: Efficiency and Lifecycle, which begins with Interactive Calculator: NAT Keep-Alive Interval.