Why CoAP
Classic HTTP does not work on heavily constrained devices:
- Small microcontroller RAM (tens of KB)
- Battery-powered (cannot hold TCP sessions open)
- Lossy radio (LoRa, Zigbee, Thread mesh)
- Not all devices have a full IPv4 stack
An HTTP parser is complex and headers are large (even a minimal GET is ~80 bytes).
CoAP (Constrained Application Protocol, RFC 7252, 2014) is a REST-like protocol designed specifically for them. It runs over UDP, with a minimal header and REST semantics. A device can act as both client and server at the same time.
Applications:
- LwM2M (Lightweight M2M from OMA) - device management over CoAP
- Thread (mesh network for smart home) - CoAP in the native protocol stack
- ZigBee Pro - CoAP option
- OMA Lightweight Object Tree - sensors/actuators in LwM2M
- Energy management in HEMS
CoAP vs HTTP - semantics
| HTTP | CoAP |
|---|---|
| TCP | UDP (TCP variant RFC 8323) |
| Text headers | Bitfields, options |
| Headers ~80+ bytes | Header ~4 bytes |
| Methods GET/POST/... | Same 4 + block extensions |
| URLs | URIs, Uri-Path/Uri-Query options |
| Response codes 200/404/... | Encoded as c.dd (5 classes x 32) |
| Stateless request/response | Confirmable / Non-confirmable |
| EventSource/SSE/WebSocket for push | Observe-pattern native |
The idea: the same mental model (URL, method, status) compressed for radio and small hardware.
Wire format
The minimal CoAP header is 4 bytes:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Ver| T | TKL | Code | Message ID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Token (if any, TKL bytes) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|1 1 1 1 1 1 1 1| Payload (if any) ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- Ver - version (1)
- T - message type: CON (confirmable), NON (non-confirmable), ACK, RST
- TKL - token length (0-8 bytes)
- Code - 8 bits, format
c.dd(3-bit class + 5-bit detail). Method: 0.01=GET, 0.02=POST, 0.03=PUT, 0.04=DELETE Response: 2.05=Content (approx 200), 4.04=Not Found (approx 404), 5.00=Internal Error - Message ID - 16-bit, for duplicate detection
- Token - 0-8 bytes, for request-response correlation (differs from Message ID: one request can produce many responses via observe)
- Options - URI path, content-format, ETag, Observe, ...
- 0xFF - payload delimiter
A minimal GET with empty payload is 4 bytes plus URI options.
Confirmable vs Non-confirmable
CoAP runs over UDP and has no built-in reliability. The message type field handles this:
CON (Confirmable) - reliable
client --CON GET /sensor (mid=42)--> server
client <--ACK 2.05 Content (mid=42)-- server
An ACK is required. Without one, the sender retries with exponential backoff: 2s, 4s, 8s, 16s, 32s, up to 5 attempts (RFC 7252).
With piggyback, the server sends the ACK with the ready response already included (one packet).
NON (Non-confirmable) - fire-and-forget
client --NON GET /sensor (mid=42)--> server
No ACK. If the packet is lost, it is lost. Use this for high-rate telemetry where a single dropped message does not matter.
This is similar to mqtt QoS 0 (NON) and QoS 1 (CON).
Methods and status codes
Methods (RFC 7252):
- GET 0.01 - retrieve a resource
- POST 0.02 - create
- PUT 0.03 - update
- DELETE 0.04 - delete
Extensions:
- FETCH 0.05 (RFC 8132) - GET with body (for complex queries)
- PATCH 0.06, iPATCH 0.07 (RFC 8132)
Response classes:
- 2.xx - Success: 2.01 Created, 2.02 Deleted, 2.03 Valid (ETag), 2.04 Changed, 2.05 Content
- 4.xx - Client error: 4.00 Bad Request, 4.04 Not Found, 4.05 Method Not Allowed, 4.06 Not Acceptable, 4.13 Request Entity Too Large
- 5.xx - Server error: 5.00 Internal, 5.03 Service Unavailable
URI and options
A URI like coap://gw.example/sensors/temp?units=C maps on the wire to:
Uri-Host(option 3) = "gw.example"Uri-Path(option 11) = "sensors", "temp" (per segment)Uri-Query(option 15) = "units=C"
Options are typed: 28 predefined, custom ones use the elective/critical flag in the option number.
Each option is encoded with delta-coding from the previous one, keeping it compact.
Observe pattern - server-pushed updates
RFC 7641. The client sends GET with Observe option = 0:
client --GET /temperature, Observe=0--> server
client <--2.05 Content "21.5", Observe=1-- server
client <--2.05 Content "21.6", Observe=2-- server (after 30 s)
client <--2.05 Content "21.7", Observe=3-- server
...
The server sends pushes each time the resource changes (or on a timer). The Observe counter grows monotonically, which helps detect lost or out-of-order responses.
To cancel, send GET with Observe = 1, or simply RST in response to the next push.
This is CoAP's equivalent of EventSource/WebSocket for push, but without an open session (UDP).
Block-wise transfer
Large payloads over UDP are a problem (fragmentation, loss). CoAP splits them into blocks via Block1/Block2 options (RFC 7959):
Block1 = 0/1/512 means: block 0, more=1, size 512
The client sends PUT in blocks 0..N with Block1, the last one with more=0. The server reassembles.
Block size is 16-1024 bytes. The default is chosen to fit the underlying MTU (typically 64-256 for LoRa, 512-1024 for WiFi/Thread).
DTLS - security
CoAP does not encrypt by itself. DTLS (Datagram TLS, RFC 6347) is TLS over UDP (like [[tls-handshake|TLS]] but with retransmission and no strict ordering).
CoAP+DTLS = coaps:// on UDP/5684 (vs plain coap:// on 5683).
Modes:
- NoSec - plain CoAP (for lab use)
- PSK (Pre-Shared Key) - shared secret, lightweight for constrained devices
- RPK (Raw Public Key) - cert without X.509 signature chain
- Certificate - full X.509 (like HTTPS)
PSK is the most common in IoT: the device ships with a factory key that the broker knows.
Modern extensions:
- OSCORE (RFC 8613) - object security, payload protection end-to-end even through a proxy
- EDHOC (RFC 9528) - lightweight key exchange for PSK
CoAP vs MQTT
| Property | CoAP | [[mqtt|MQTT]] |
|---|---|---|
| Transport | UDP (or TCP/TLS) | TCP/TLS |
| Model | request-response (REST) | publish-subscribe |
| Broker | not needed | required |
| Push | observe-pattern | subscribe |
| Min header | 4 bytes | 2 bytes |
| Reliability | CON ack/retry | QoS 0/1/2 |
| Multicast | yes (group communication) | no |
| Best for | sensor query, RESTful API | telemetry, fan-out |
| Stack | REST-like, familiar | specific |
| Security | DTLS | TLS (harder on microcontrollers) |
In Thread/Matter, CoAP is the base protocol. In classic IoT (industrial, mobile), MQTT is more common.
Multicast - group communication
CoAP supports IP multicast: one request reaches all devices in the multicast group. Useful for discovery and group control:
GET coap://[ff02::fd]:5683/.well-known/core
--> all CoAP servers on the local link
HTTP has no equivalent. This is useful in smart home scenarios: "all lights off".
Only NON-confirmable messages may be multicast (CON over multicast is logically impossible).
Discovery - /.well-known/core
RFC 6690 - standard resource directory:
GET /.well-known/core
--->
<--- 2.05
</sensors/temp>;rt="temperature";if="sensor",
</sensors/humidity>;rt="humidity";if="sensor",
</actuators/led>;rt="light";if="actuator"
Application-level discovery: what resources exist and what types they are.
When things go wrong
- Requests disappear - this is UDP. Check the firewall (5683/5684 UDP). Use CON instead of NON to make losses visible.
4.13 Request Entity Too Large- payload exceeds MTU. Use Block1.- DTLS handshake fails - PSK mismatch or TLS version mismatch
(CoAP DTLS normally uses DTLS 1.2). Try
openssl s_client -dtls1_2. - Observe stops working behind NAT - the NAT binding expired and the server
sends to nowhere. Add a keepalive (CON GET occasionally) or shorten the
interval. - Sequence number wrap - the Observe counter is 24-bit; on wrap, the comparison logic can fail. Most implementations have handled this.
- Multicast not arriving - the kernel does not route multicast by default.
Run
ip route add 224.0.0.0/4 dev eth0. The link layer must also support it (WiFi often filters multicast).
Further reading
- https://datatracker.ietf.org/doc/html/rfc7252 - original CoAP RFC
- https://coap.technology/ - tools and articles
- https://github.com/eclipse/californium (Java) - reference implementation
- https://github.com/obgm/libcoap (C) - for embedded use