CoAP Protocol — RESTful Communication for Constrained IoT Devices Explained
CoAP (Constrained Application Protocol) is a specialized UDP-based protocol designed for IoT devices with limited memory, processing power, and battery, implementing a RESTful request-response model similar to HTTP.
Why CoAP Matters
MQTT dominates the pub/sub space, but not every IoT use case fits the pub/sub pattern. Sometimes you need to read from or write to a specific resource on a device — get the current temperature, turn on a specific light, update a configuration value. This is the RESTful request-response model, and CoAP brings it to constrained devices with 10KB of RAM and 8-bit microcontrollers. CoAP runs over UDP, avoiding TCP’s connection overhead, and uses binary encoding that’s far more compact than HTTP’s text headers.
Plain-Language Explanation
Imagine HTTP as sending a formal letter. You write the address, a greeting, the message, a closing, and your signature. The recipient acknowledges receipt and sends a reply with their own formal letter. It’s reliable but verbose.
CoAP is like sending a text message. “Temp?” and the reply is “23.5”. Short, fast, and if the reply doesn’t arrive, you send “Temp?” again. Same intent as HTTP — read a resource (GET /.well-known/core), create a resource (POST /sensors), update a resource (PUT /actuators/light), delete a resource (DELETE /actuators/light). But the overhead is dramatically smaller.
graph LR
Client[CoAP Client
Dashboard] -->|CON GET /temperature| Server[CoAP Server
ESP8266 Sensor]
Server -->|ACK 2.05 Content "23.5"| Client
Client -->|CON PUT /actuators/light| Server
Server -->|ACK 2.04 Changed| Client
subgraph "CoAP Message Types"
CON[Confirmable]
NON[Non-Confirmable]
ACK[Acknowledgment]
RST[Reset]
end
style Server fill:#3498db,color:#fff
style Client fill:#27ae60,color:#fff
CoAP vs HTTP Mapping
| HTTP | CoAP | Method |
|---|---|---|
| GET | GET | Retrieve resource |
| POST | POST | Create resource |
| PUT | PUT | Update resource |
| DELETE | DELETE | Delete resource |
| - | GET (observe) | Subscribe to changes |
Resource Discovery
CoAP devices advertise their resources through /.well-known/core, returning a link format listing available endpoints:
# CoAP resource discovery response
# Request: GET coap://sensor.local/.well-known/core
# Response: </temperature>;rt="temperature-celsius";if="sensor",
# </humidity>;rt="percent";if="sensor",
# </actuators/light>;rt="light-switch";if="actuator"Python CoAP Server Example
# coap_server.py
from aiocoap import *
import asyncio, random, json
class TemperatureResource(Resource):
async def render_get(self, request):
temp = round(random.uniform(18.0, 30.0), 1)
payload = json.dumps({"temperature": temp, "unit": "C"}).encode()
return Message(payload=payload, code=Code.CONTENT)
async def render_put(self, request):
# Update configuration (threshold, etc.)
print(f"Received config update: {request.payload}")
return Message(code=Code.CHANGED)
class LightResource(Resource):
def __init__(self):
super().__init__()
self.state = False # Off
async def render_get(self, request):
payload = json.dumps({"state": "on" if self.state else "off"}).encode()
return Message(payload=payload, code=Code.CONTENT)
async def render_put(self, request):
data = json.loads(request.payload)
self.state = data.get("state", False)
print(f"Light turned {'ON' if self.state else 'OFF'}")
return Message(code=Code.CHANGED)
async def main():
root = Site()
root.add_resource(['temperature'], TemperatureResource())
root.add_resource(['actuators', 'light'], LightResource())
await Context.create_server_context(root, bind=('0.0.0.0', 5683))
print("CoAP server running on coap://0.0.0.0:5683")
print("Resources:")
print(" GET /temperature")
print(" PUT /actuators/light")
await asyncio.get_event_loop().run_forever()
if __name__ == "__main__":
asyncio.run(main())# coap_client.py
from aiocoap import *
import asyncio, json
async def main():
protocol = await Context.create_client_context()
# GET temperature
request = Message(code=GET, uri='coap://localhost:5683/temperature')
response = await protocol.request(request).response
data = json.loads(response.payload)
print(f"Temperature: {data['temperature']}°C")
# PUT light on
payload = json.dumps({"state": True}).encode()
request = Message(code=PUT, uri='coap://localhost:5683/actuators/light', payload=payload)
response = await protocol.request(request).response
print(f"Light control: {response.code}")
if __name__ == "__main__":
asyncio.run(main())Expected output:
Temperature: 24.7°C
Light control: 2.04 ChangedCoAP Observe (Subscription)
Instead of polling, a client can “observe” a resource and receive notifications when it changes:
# Observe temperature changes
request = Message(code=GET, uri='coap://localhost:5683/temperature')
observation = await protocol.request(request).observation
async for response in observation:
data = json.loads(response.payload)
print(f"Updated temperature: {data['temperature']}°C")
# Exit after 5 updatesCoAP vs MQTT
| Feature | CoAP | MQTT |
|---|---|---|
| Transport | UDP | TCP |
| Pattern | Request-response (REST) | Pub/Sub |
| Header size | 4 bytes | 2 bytes |
| Reliability | CON/NON messages | QoS 0,1,2 |
| Discovery | Built-in (.well-known/core) | No discovery |
| Best for | Device-to-device, REST APIs | Device-to-cloud, event streaming |
When CoAP wins: You need to read/write specific resources on individual devices. You’re building a device-to-device mesh. UDP-only networks (some LPWAN technologies).
When MQTT wins: You need pub/sub event streaming. Devices communicate through a cloud broker. You need many-to-many communication patterns.
Common Mistakes
Treating CoAP exactly like HTTP: CoAP runs over UDP, so packets can be lost. Use CON (confirmable) messages for important requests and implement retry logic.
Not implementing resource discovery: CoAP’s discovery is one of its best features. Always expose
/.well-known/coreso clients can discover available resources.Ignoring block-wise transfer: CoAP has a 64KB message limit. For larger payloads (firmware updates), use block-wise transfer (
Block1andBlock2options).No security: DTLS (Datagram TLS) secures CoAP (CoAPS on port 5684). Without it, anyone on the network can read or write resources.
Polling instead of observing: Don’t poll
GET /temperatureevery second. Use the observe pattern to receive push updates.
Practice Questions
What transport protocol does CoAP use and why? UDP. TCP’s connection overhead and three-way handshake are too expensive for constrained devices. UDP is lightweight but requires CoAP’s CON/NON reliability mechanism.
How does CoAP achieve reliability over UDP? CON (confirmable) messages require an ACK (acknowledgment). If no ACK arrives, the sender retransmits with exponential backoff. NON (non-confirmable) messages have no acknowledgment.
What is the difference between CoAP observe and MQTT subscribe? Both provide push updates, but CoAP observe is per-resource (client observes a specific URI) while MQTT subscribe uses topic hierarchies with wildcards.
How does CoAP resource discovery work? Clients send
GET /.well-known/coreand the server responds with a link format listing available resources, their types, and interfaces.What is CoAP block-wise transfer? A mechanism to split large payloads into multiple blocks, each sent in a separate CoAP message. Required for firmware updates or large configuration files.
Mini Project
Build a CoAP-controlled LED simulator:
from aiocoap import *
import asyncio, json, time
class LEDResource(Resource):
colors = {"red": (255,0,0), "green": (0,255,0), "blue": (0,0,255)}
active_color = "off"
async def render_get(self, request):
return Message(
payload=json.dumps({"state": self.active_color}).encode(),
code=Code.CONTENT
)
async def render_put(self, request):
data = json.loads(request.payload)
color = data.get("color", "off")
if color in self.colors or color == "off":
self.active_color = color
status = "on" if color != "off" else "off"
print(f"LED: {color.upper()} ({status})")
return Message(code=Code.CHANGED)
return Message(code=Code.BAD_REQUEST, payload=b"Invalid color")
async def main():
root = Site()
root.add_resource(['led'], LEDResource())
await Context.create_server_context(root, bind=('0.0.0.0', 5683))
print("LED simulator running. Try:")
print(" coap://localhost:5683/led")
await asyncio.get_event_loop().run_forever()
asyncio.run(main())Cross-References
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro