Skip to main content Skip to sidebar

Looking into CoAP Protocol

Most web APIs assume a device with plenty of memory, a reliable TCP stack, and enough power to keep connections alive. Constrained devices - battery-powered sensors, microcontrollers with kilobytes of RAM, nodes on lossy low-power radio links - break all of those assumptions. The Constrained Application Protocol (CoAP), defined in RFC 7252, is a request/response protocol designed for exactly this world. It keeps the familiar REST model of methods, URIs, and response codes, but runs over UDP and packs its headers into as few as four bytes.

Why not just use HTTP?

CoAP deliberately mirrors HTTP so that developers and gateways can map between the two. The difference is in what each protocol assumes about the device and the network.

AspectHTTP/1.1CoAP
TransportTCPUDP (also DTLS, TCP, WebSockets)
Header sizeHundreds of bytes, text4 bytes fixed + compact options
Message modelRequest/responseRequest/response + async notifications
ReliabilityProvided by TCPOptional, per-message (Confirmable)
MulticastNoYes
Typical footprintFull TCP/TLS stackFits in tens of KB of flash

The key insight is that HTTP’s cost is not the REST model itself - it is the verbose text framing and the TCP connection state. CoAP keeps REST semantics but strips the framing down to a binary header and moves reliability out of the transport and into the protocol, where it can be applied selectively.

The two-layer design

CoAP is split into two sub-layers that are easy to confuse but serve different jobs.

flowchart TB
    subgraph App["Application"]
        REQ["Requests / Responses<br/>GET, POST, PUT, DELETE"]
    end
    subgraph Msg["Messaging Layer"]
        CON["Confirmable / Non-confirmable<br/>ACK, Reset, retransmission, dedup"]
    end
    subgraph Net["Transport"]
        UDP["UDP / DTLS"]
    end

    REQ --> CON --> UDP

    style REQ fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
    style CON fill:#fff3cd,stroke:#ffc107,stroke-width:1px
    style UDP fill:#d4edda,stroke:#28a745,stroke-width:1px
  • The messaging layer deals with individual UDP datagrams: whether a message must be acknowledged, detecting duplicates, and retransmitting when an acknowledgement is lost.
  • The request/response layer deals with REST semantics: the method, the target resource, and matching a response back to its request using a token.

Because these are separate, a single request can be reliable while its matching response arrives later as a separate message, and reliability is decided per message rather than for a whole connection.

Message types

Every CoAP message carries a type that tells the receiver what to do about reliability:

  • Confirmable (CON) - must be acknowledged. If no ACK arrives, the sender retransmits with exponential backoff (default: first timeout ~2 seconds, doubling, up to 4 retransmissions).
  • Non-confirmable (NON) - fire and forget. Used for readings where the occasional lost sample does not matter.
  • Acknowledgement (ACK) - confirms receipt of a specific CON message, matched by Message ID.
  • Reset (RST) - says “I received this but cannot process it”, for example after a reboot when the context is gone.

The 16-bit Message ID deduplicates retransmissions and pairs a CON with its ACK. It is separate from the Token, which pairs a response with its request at the application layer - a distinction that matters for the delayed responses shown below.

Request/response flow

The simplest exchange piggybacks the response onto the acknowledgement:

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: CON GET /temperature (MID=0x1a2b, Token=0x7f)
    S-->>C: ACK 2.05 Content (MID=0x1a2b, Token=0x7f) "21.4"

When the server cannot answer immediately - the sensor needs to wake up, or a reading takes time - it acknowledges first and delivers the result later as a separate Confirmable message. This is the separate response, and it is where the Token earns its keep:

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: CON GET /temperature (MID=0x1a2b, Token=0x7f)
    S-->>C: ACK (empty, MID=0x1a2b)
    Note over S: sensor wakes, takes reading
    S->>C: CON 2.05 Content (MID=0x9c3d, Token=0x7f) "21.4"
    C-->>S: ACK (MID=0x9c3d)

The two messages share the same Token (0x7f) but have different Message IDs, so the client knows the late 2.05 Content belongs to its original GET even though the ACK arrived empty.

Message format

The fixed header is four bytes, followed by an optional token, then options, then the payload:

block-beta
    columns 5

    ver["Ver<br/>2 bits"]
    type["Type<br/>2 bits"]
    tkl["TKL<br/>4 bits"]
    code["Code<br/>8 bits"]
    mid["Message ID<br/>16 bits"]

    token["Token<br/>(0-8 bytes)"]:2
    options["Options<br/>(TLV, delta-encoded)"]:2
    marker["0xFF"]:1

    payload["Payload"]:5

    style ver fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
    style type fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
    style tkl fill:#e1f5ff,stroke:#0366d6,stroke-width:1px
    style code fill:#fff3cd,stroke:#ffc107,stroke-width:1px
    style mid fill:#fff3cd,stroke:#ffc107,stroke-width:1px
    style token fill:#d4edda,stroke:#28a745,stroke-width:1px
    style options fill:#d4edda,stroke:#28a745,stroke-width:1px
    style marker fill:#f8d7da,stroke:#dc3545,stroke-width:1px
    style payload fill:#f8d7da,stroke:#dc3545,stroke-width:1px

The Code byte does double duty. In a request it holds the method (0.01 GET, 0.02 POST, 0.03 PUT, 0.04 DELETE). In a response it holds a status split as class.detail, deliberately echoing HTTP: 2.05 Content maps to HTTP 200, 4.04 Not Found to 404, 5.00 Internal Server Error to 500. So a device firmware author who knows HTTP already knows how to read CoAP result codes.

Options and delta encoding

CoAP options carry everything a URL and headers would in HTTP - the path, query string, content format, ETag, and so on - but they are encoded as a compact list. Each option records the difference between its number and the previous option’s number, not the absolute number. Since options are emitted in ascending order, these deltas stay small and usually fit in a nibble. The URI coap://host/sensors/temp?units=C becomes three Uri-Path segments and one Uri-Query, each just a few bytes, instead of a full text line.

Observing resources

Polling a sensor every few seconds wastes radio time and battery. CoAP’s Observe option (RFC 7641) turns a GET into a subscription: the client registers once, and the server pushes a fresh response every time the resource changes.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: GET /temperature (Observe: 0, Token=0x42)
    S-->>C: 2.05 Content (Observe: 12) "21.4"
    Note over S: value changes
    S->>C: 2.05 Content (Observe: 13) "21.9"
    Note over S: value changes
    S->>C: 2.05 Content (Observe: 15) "22.1"
    C->>S: GET /temperature (Observe: 1)
    Note over C,S: Observe: 1 cancels the subscription

Each notification reuses the original Token and carries an increasing Observe sequence number, which lets the client reorder or discard notifications that arrive out of order over UDP. This gives you MQTT-style push updates without a separate broker - the server itself is the source of truth. If you are weighing this against a broker-based design, my earlier post on MQTT v3.1.1 vs v5 covers the trade-offs of the publish/subscribe model.

Block-wise transfers

A single UDP datagram and a constrained radio link cannot carry a large firmware image or a long configuration document. The Block option (RFC 7959) splits a payload across many messages while keeping each one small enough to avoid IP fragmentation. The block option encodes the block number, a “more blocks follow” bit, and the block size (from 16 up to 1024 bytes). Transfers stay stateless on the wire - each block is an ordinary request/response - so a device can stream a large resource without holding the whole thing in memory at once.

Security with DTLS

Because CoAP rides on UDP, it cannot borrow TLS directly; instead it uses DTLS (Datagram TLS) over the default secure port 5684 (plain CoAP uses 5683). RFC 7252 defines several security modes for how devices are keyed:

  • NoSec - no DTLS at all. Only acceptable on a physically isolated or link-encrypted network.
  • PreSharedKey (PSK) - devices share symmetric keys out of band. Cheap and common on tiny hardware that cannot afford certificate parsing.
  • RawPublicKey - each device carries an asymmetric key pair but no certificate chain, identified by the key itself.
  • Certificate - full X.509 chains, for devices that can carry a PKI.

For very constrained deployments, OSCORE (RFC 8613) is an alternative that protects the CoAP message at the application layer with COSE/CBOR, so security survives even when a proxy needs to read and forward the message. If you want the underlying trade-off between DTLS and object security, the comparison of transport-level versus payload-level protection is the same one that shows up whenever a proxy sits in the path.

Payloads: why CBOR fits

CoAP does not mandate a payload format, but the same constraints that shaped the protocol favor a compact binary encoding over JSON. CBOR is the usual choice, and its content format is negotiated through the Content-Format and Accept options exactly as HTTP would use Content-Type and Accept. I went deep on why binary encoding wins on constrained links in JSON vs CBOR; CoAP is one of the protocols that motivated CBOR in the first place.

When CoAP is the right tool

CoAP shines when the device is genuinely constrained and the network is lossy or metered:

  • Battery sensors that sleep most of the time and wake to report a reading.
  • Mesh or low-power radio networks (6LoWPAN, Thread) where every byte and every retransmission costs energy.
  • Deployments that want REST semantics and HTTP mappability without a full TCP/TLS stack.
  • Scenarios needing multicast discovery or group control, which UDP supports and TCP cannot.

It is a poor fit when you already have capable hardware and a stable network - there, plain HTTP over TLS or an MQTT broker is simpler to operate and easier to debug. The value of CoAP is precisely that it refuses to assume the comfortable environment those protocols take for granted.

Summary

CoAP takes the REST model that developers already understand and re-expresses it for devices that cannot afford HTTP’s framing or TCP’s connection state. Its two-layer design separates message reliability from request semantics, its four-byte header and delta-encoded options keep packets tiny, and extensions like Observe, Block-wise transfer, and DTLS/OSCORE round it out into a complete protocol for the constrained edge. If you work with IoT telemetry, CoAP is worth reaching for whenever the device and the link - not the API - are the real constraint.