18  CoAP Stack and Trace Contracts

coap
implementation
labs
debugging

18.1 Start With the Trace That Must Prove It

A lab can appear to work because the browser prints a value, while the packet trace shows token confusion, wasteful Confirmable messages, stale cache data, or repeated DTLS handshakes. The implementation is not accepted until the trace explains the behavior.

This page turns the lab into a release contract: choose the stack, map each handler, capture the response evidence, then debug by Message ID, Token, options, Max-Age, Observe state, and security session records.

18.2 Learning Objectives

After this page, you should be able to:

  • Select an appropriate CoAP stack for a host, gateway, or constrained embedded node.
  • Map each URI and method to a resource handler, response code, payload, and trace artifact.
  • Verify client behavior by checking response codes, payloads, Tokens, Message IDs, options, and retransmissions.
  • Diagnose classic implementation failures: Message ID correlation, overuse of Confirmable messages, stale proxy caches, and repeated DTLS handshakes.

18.3 Why This Contract Comes After Implementation Labs

CoAP Implementation Labs gives you runnable Python and ESP32 code. This page narrows in on the implementation contract behind those examples: which stack owns the protocol machinery, what each handler must prove, how the client accepts a response, and what a packet trace should show when the implementation is correct.

The CoAP implementation proof stays concrete: a CoAP implementation is accepted only when a working handler is tied to URI, method, response code, payload, Token, Message ID, and trace evidence.

Use it when you are turning a classroom lab into a repeatable acceptance test, comparing Python and embedded implementations, debugging Observe notifications, or reviewing whether a constrained deployment is wasting battery through avoidable ACKs or repeated security handshakes.

Overview: You Rarely Write CoAP Bytes by Hand — You Use a Stack

Almost no production code assembles the 4-byte header and delta-encoded options by hand. A CoAP library does the messaging layer for you: Confirmable retransmission, deduplication, token generation, and option encoding. Your code deals in resources, methods, and payloads. On general-purpose hosts and gateways the common stacks are aiocoap (Python, asyncio — the library these labs use), libcoap (C, which also ships the handy coap-client CLI), and Eclipse Californium (Java).

On microcontrollers you reach for stacks sized to kilobytes of RAM: RIOT's gcoap and nanocoap, Contiki-NG's Erbium, Zephyr's CoAP subsystem, and Arduino/ESP32 libraries such as coap-simple used later in this chapter. They implement the same RFC 7252 on the wire, so an aiocoap client can talk to an ESP32 server — interoperability is the whole point of the standard.

Treat each lab as a contract between a client command, a URI, a handler, and the response evidence. A laptop might run coap-client -m get coap://node/temperature and expect 2.05 Content with a numeric payload; the same server might accept a PUT /led from a mobile tool and return 2.04 Changed; an Observe registration keeps the same Token while notifications report later temperature values. That framing keeps the code examples from becoming copy-paste snippets: every run should leave a response code, a payload sample, and a packet trace you can compare with the handler you wrote.

CoAP implementation lab diagram showing sensor, mobile app, and gateway clients sending GET, PUT, and Observe requests to a CoAP server with resource handlers.
A CoAP lab is easier to debug when each client action maps to a resource handler, response code, payload, and trace artifact.

Host and gateway stacks

aiocoap (Python), libcoap (C + CLI), Californium (Java) run on Linux hosts, gateways, and cloud bridges.

Embedded stacks

RIOT gcoap/nanocoap, Contiki-NG Erbium, Zephyr CoAP, and ESP/Arduino libraries fit constrained nodes.

Server = handlers

You register a handler per URI path and method; the library routes incoming requests to it.

Client = requests

You issue a request to a coap:// URI and read the response code and payload back.

Practitioner: A Server Is Handlers by URI+Method; A Client Reads Code and Payload

On the server side you attach behavior to a path. In aiocoap you subclass Resource and implement render_get, render_put, and so on; the library reads the incoming Uri-Path options and the method Code and dispatches to the right method, which returns a representation plus a response code (a read returns 2.05 Content, a write returns 2.04 Changed). On the client side you send a request to a coap:// URI and inspect two things on what comes back: response.code to branch on success or error, and response.payload for the representation. For Observe you register once and then iterate the stream of notifications instead of polling.

Verify on the wire, not just in your logs. A small toolkit proves the exchange is doing what you think.

Make the acceptance test small enough to repeat after every edit. Start the server on UDP port 5683, run one GET for the sensor resource, one PUT for the actuator resource, one deliberately bad path, and one Observe registration if the stack supports it. The expected record is concrete: GET returns 2.05 plus a parseable value, PUT returns 2.04 or another documented success code, the bad path returns 4.04, and the Observe trace repeats the Token across notifications. If an ESP32 version and a Python version expose the same resource contract, use the same client checklist for both; only the build and deployment step changes.

Tool
Command / filter
What it proves
When to reach for it
coap-client (libcoap)
coap-client -m get coap://host/temp
Server responds with the expected code and payload
Quick manual GET/PUT and DTLS-PSK checks.
Wireshark
display filter: coap
Code, Token, options, and retransmissions on the wire
Confirming token correlation and CON retries.
Wireshark (secure)
dtls && udp.port == 5684
coaps traffic is encrypted ApplicationData
Proving DTLS actually protects the payload.
Server logs
per-request code + token
Handler ran and returned the intended code
Correlating a client trace with server behavior.

Under the Hood: The Token Bug and Three Classic Pitfalls

The single most common implementation bug in hand-rolled correlation is matching responses by Message ID instead of Token. A piggybacked ACK does reuse the request's Message ID, which lulls people into keying on it — but a separate response and every Observe notification arrive as new messages with different Message IDs and the same Token. Code that keys on Message ID silently drops those. The rule is simple: correlate application responses by Token; the Message ID is only for message-layer ACK and duplicate handling. Good libraries already do this, which is a strong reason to use one.

Test for the bug directly: open an Observe and watch the notifications in Wireshark. You will see one Token repeated across notifications while the Message ID changes each time. If your handler only fires on the first notification, you are matching on the Message ID.

Three deployment pitfalls recur, and each is visible in a capture. First, using Confirmable for every message drains batteries — the trace shows an ACK round trip per message; send disposable telemetry as NON. Second, ignoring proxy cache semantics serves stale readings — a cache honors your Max-Age, so an over-long value returns aged data; advertise freshness that matches the real change rate. Third, re-running the DTLS handshake per request wastes energy — the capture shows a full ClientHello-through-Finished before each request; keep the session open and reuse it.

Correlate by Token

Notifications and separate responses share the Token, not the Message ID; key your dispatch on the Token.

NON for telemetry

Reserve CON for messages whose loss is unsafe; frequent readings as NON save radio-on energy.

Respect Max-Age

Set freshness to the true change rate so caches and proxies never serve stale readings.

Reuse DTLS sessions

Amortize the handshake across many requests instead of renegotiating on every call.

18.4 Release Checklist

Before shipping a CoAP implementation lab or prototype, verify these records:

  • Stack choice names the library and target platform: aiocoap, libcoap, Californium, RIOT gcoap/nanocoap, Contiki-NG Erbium, Zephyr CoAP, or an ESP/Arduino library.
  • Resource table maps every URI and method to its handler, expected success code, expected error code, payload type, and Content-Format.
  • Client checks branch on response.code before trusting response.payload, and tests include at least one success path and one documented error path.
  • Trace evidence shows Token correlation for ordinary responses, separate responses, and Observe notifications; Message ID is used only for ACK and duplicate handling.
  • Message policy distinguishes CON from NON and explains which application messages can tolerate loss.
  • Freshness policy records Max-Age and proxy-cache behavior for resources that may be cached or observed.
  • Security evidence shows whether the lab uses plain CoAP, DTLS, or OSCORE, and whether DTLS sessions are reused instead of renegotiated per request.

18.5 See Also

18.6 Next

Return to CoAP Implementation Labs, then continue to CoAP Advanced Features for block-wise transfer, discovery, caching, and multicast.