Designing CoAP APIs follows REST principles: structure resources as nouns (e.g., /sensors/temp/value), use CoAP methods as verbs (GET, POST, PUT, DELETE), and select compact content formats like CBOR over JSON to minimize payload size on constrained networks. Proper response codes (2.xx/4.xx/5.xx), DTLS security, and rate limiting are essential for robust production IoT APIs.
Learning Objectives
By the end of this chapter, you will be able to:
Design RESTful CoAP APIs : Construct resource hierarchies using nouns with proper URI conventions and versioning strategies
Distinguish Content Formats : Compare JSON, CBOR, and text/plain payloads and select the appropriate format for constrained versus development environments
Implement Error Handling : Apply proper CoAP response codes (2.xx/4.xx/5.xx) and construct helpful error payloads for production systems
Configure Security Controls : Configure DTLS, rate limiting, and per-device access control for production CoAP deployments
Calculate Energy Tradeoffs : Calculate battery life impact of CON versus NON message types and justify the selection for different IoT use cases
Diagnose API Pitfalls : Identify and resolve common CoAP API problems such as Observe notification floods and proxy caching issues
Designing a CoAP API means deciding how IoT devices expose their data and accept commands. Think of it as creating a menu for your sensor – you define endpoints like /temperature or /status that other devices can read. Good API design makes your IoT system easy to use and understand, even for developers who have never seen your device before.
“I want other devices to read my temperature, but I also want them to set my sampling rate,” said Sammy the Sensor. “How do I organize all that?”
Max the Microcontroller sketched out a plan. “You create a resource tree , Sammy! Your root is /sensor, and under it you have /sensor/temperature for reading data and /sensor/config for settings. When someone sends a GET to /sensor/temperature, they get your latest reading. When they send a PUT to /sensor/config, they can change your sampling rate.”
“Keep your URIs short!” advised Bella the Battery. “Every extra character in the path costs bytes, and CoAP packets should stay small enough to fit in a single UDP datagram. Use /temp instead of /temperature-reading-in-celsius. Every byte saved is energy saved!”
Lila the LED added: “And don’t forget content negotiation ! Some devices want your data in JSON, others in CBOR – which is like compressed JSON. Your API should let the requester choose the format using the Accept option. One sensor, multiple formats, everyone is happy!”
Prerequisites
Before diving into this chapter, you should be familiar with:
RESTful Resource Design
Core Concept : CoAP follows REST principles - design resources as nouns, use HTTP-like methods as verbs. The URI identifies WHAT you’re accessing, the method specifies HOW.
Why It Matters : Consistent RESTful design makes APIs intuitive for developers, enables caching, supports proxying, and allows standard tooling to work seamlessly.
Key Takeaway : Good resource design: coap://sensor.local/v1/temperature (noun). Bad design: coap://sensor.local/getTemperature (verb in URL).
Good vs. Bad Resource Design
Good resource design (nouns):
coap://sensor.local/v1/temperature - read a temperature resource
coap://sensor.local/v1/humidity - read a humidity resource
coap://actuator.local/v1/led/state - manage LED state
coap://gateway.local/v1/config/network - update network settings
Poor resource design (verbs - avoid):
coap://sensor.local/getTemperature - verb in URL
coap://actuator.local/turnOnLED - action embedded in path
REST operations on resources:
GET on .../v1/temperature reads the current value.
PUT on .../v1/led/state updates the LED state.
POST on .../v1/logs creates a new log entry.
DELETE on .../v1/logs/2025-01 removes an old log bucket.
URI Naming Conventions
CoAP URIs should be short (constrained bandwidth) but descriptive:
Recommended structure: host + version + resource type + device ID + subresource
Examples:
.../v1/devices/temp42/reading
.../v1/devices/temp42/metadata
.../v1/devices/temp42/config
Best practices:
Version your API : use /v1/ so future changes do not break existing clients.
Use plural nouns : prefer /devices/temp42 over a singular collection name.
Keep it short : every character adds bytes on constrained links.
Lowercase with hyphens : /motion-sensors stays readable and URL-safe.
Avoid deep nesting : stop around 3-4 levels; .../devices/sensors/temp/reading is too deep.
Interactive URI Analysis
Show code
viewof uriPath = Inputs. text ({
label : "CoAP URI Path" ,
value : "coap://sensor/t42" ,
placeholder : "coap://sensor/..." ,
width : 220
})
uriAnalysis = {
const uri = uriPath;
const match = uri. match (/ ^ coap: \/\/([^\/]+)( . *)$ / );
if (! match) return {valid : false };
const host = match[1 ];
const path = match[2 ] || '/' ;
const pathBytes = new TextEncoder (). encode (path). length ;
const hostBytes = new TextEncoder (). encode (host). length ;
const totalBytes = pathBytes + hostBytes + 7 ; // Protocol overhead
// Parse path segments
const segments = path. split ('/' ). filter (s => s. length > 0 );
// Check for best practices
const hasVersion = segments. some (s => s. match (/ ^ v \d+$ / ));
const hasLongSegment = segments. some (s => s. length > 20 );
const depthLevel = segments. length ;
const usesVerbs = segments. some (s =>
['get' , 'post' , 'put' , 'delete' , 'update' , 'create' , 'read' ]. some (v => s. toLowerCase (). includes (v))
);
return {
valid : true ,
host,
path,
pathBytes,
hostBytes,
totalBytes,
segments,
depthLevel,
hasVersion,
hasLongSegment,
usesVerbs,
score : (hasVersion ? 25 : 0 ) + (! hasLongSegment ? 25 : 0 ) + (depthLevel <= 4 ? 25 : 0 ) + (! usesVerbs ? 25 : 0 )
};
}
html `
<div style="font-family: Arial, sans-serif; max-width: 700px; margin: 20px 0;">
<h4 style="color: #2C3E50; margin-bottom: 15px;">URI Resource Path Analyzer</h4>
${ uriAnalysis. valid ? `
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-bottom: 15px;">
<div style="text-align: center; padding: 15px; background: white; border-radius: 6px; border: 2px solid #e0e0e0;">
<div style="font-size: 12px; color: #7F8C8D; margin-bottom: 5px;">PATH SIZE</div>
<div style="font-size: 28px; font-weight: bold; color: ${ uriAnalysis. pathBytes < 40 ? '#16A085' : uriAnalysis. pathBytes < 60 ? '#E67E22' : '#E74C3C' } ;"> ${ uriAnalysis. pathBytes } </div>
<div style="font-size: 11px; color: #7F8C8D;">bytes</div>
</div>
<div style="text-align: center; padding: 15px; background: white; border-radius: 6px; border: 2px solid #e0e0e0;">
<div style="font-size: 12px; color: #7F8C8D; margin-bottom: 5px;">DEPTH</div>
<div style="font-size: 28px; font-weight: bold; color: ${ uriAnalysis. depthLevel <= 4 ? '#16A085' : '#E67E22' } ;"> ${ uriAnalysis. depthLevel } </div>
<div style="font-size: 11px; color: #7F8C8D;">levels</div>
</div>
<div style="text-align: center; padding: 15px; background: white; border-radius: 6px; border: 2px solid #e0e0e0;">
<div style="font-size: 12px; color: #7F8C8D; margin-bottom: 5px;">SCORE</div>
<div style="font-size: 28px; font-weight: bold; color: ${ uriAnalysis. score >= 75 ? '#16A085' : uriAnalysis. score >= 50 ? '#E67E22' : '#E74C3C' } ;"> ${ uriAnalysis. score } </div>
<div style="font-size: 11px; color: #7F8C8D;">/ 100</div>
</div>
</div>
<div style="font-size: 13px; color: #2C3E50; margin-top: 15px;">
<strong>Path Segments:</strong> ${ uriAnalysis. segments . map ((s, i) =>
`<span style="background: #E8F6F3; padding: 4px 8px; border-radius: 4px; margin: 0 4px; font-family: monospace;"> ${ s} </span>`
). join (' / ' )}
</div>
</div>
<div style="background: white; border-radius: 8px; padding: 15px; border: 2px solid #e0e0e0;">
<h5 style="color: #2C3E50; margin: 0 0 10px 0;">Best Practices Check</h5>
<div style="font-size: 13px; line-height: 1.8;">
<div style="display: flex; align-items: center; margin-bottom: 8px; gap: 10px; flex-wrap: wrap;">
<span style="display: inline-block; min-width: 46px; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: bold; background: ${ uriAnalysis. hasVersion ? '#E8F6F3' : '#FDEDEC' } ; color: ${ uriAnalysis. hasVersion ? '#138871' : '#C0392B' } ;"> ${ uriAnalysis. hasVersion ? 'PASS' : 'FIX' } </span>
<span style="color: #555;">API versioning (e.g., /v1/)</span>
</div>
<div style="display: flex; align-items: center; margin-bottom: 8px; gap: 10px; flex-wrap: wrap;">
<span style="display: inline-block; min-width: 46px; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: bold; background: ${ ! uriAnalysis. hasLongSegment ? '#E8F6F3' : '#FDEDEC' } ; color: ${ ! uriAnalysis. hasLongSegment ? '#138871' : '#C0392B' } ;"> ${ ! uriAnalysis. hasLongSegment ? 'PASS' : 'FIX' } </span>
<span style="color: #555;">Short segment names (<20 chars)</span>
</div>
<div style="display: flex; align-items: center; margin-bottom: 8px; gap: 10px; flex-wrap: wrap;">
<span style="display: inline-block; min-width: 46px; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: bold; background: ${ uriAnalysis. depthLevel <= 4 ? '#E8F6F3' : '#FDEDEC' } ; color: ${ uriAnalysis. depthLevel <= 4 ? '#138871' : '#C0392B' } ;"> ${ uriAnalysis. depthLevel <= 4 ? 'PASS' : 'FIX' } </span>
<span style="color: #555;">Shallow nesting (<=4 levels)</span>
</div>
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
<span style="display: inline-block; min-width: 46px; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: bold; background: ${ ! uriAnalysis. usesVerbs ? '#E8F6F3' : '#FFF4E5' } ; color: ${ ! uriAnalysis. usesVerbs ? '#138871' : '#B26A00' } ;"> ${ ! uriAnalysis. usesVerbs ? 'PASS' : 'WARN' } </span>
<span style="color: #555;">Resource nouns (no verbs like 'get', 'update')</span>
</div>
</div>
</div>
` : `
<div style="background: #FFEBEE; padding: 20px; border-radius: 8px; border-left: 4px solid #E74C3C; color: #C62828;">
<strong>Invalid URI format.</strong> Must start with <code>coap://</code>
</div>
` }
</div>
`
Payload Format Selection
Choose content format based on your constraints:
text/plain (0) - minimal bytes for a single value like "23.5".
application/json (50) - easiest to inspect during development and debugging.
application/cbor (60) - smaller binary payload for production constrained devices.
application/octet-stream (42) - raw binary only when both sides already share the schema.
Example - Temperature reading in different formats:
Text/plain (4 bytes) : 23.5
JSON (42 bytes) : device=temp42, value=23.5, unit=C
CBOR (~20 bytes) : binary map carrying the same fields in a compact form
Recommendation:
Battery sensors : Use text/plain or CBOR (minimize bytes)
Development : Use JSON (easy debugging)
Production : Use CBOR (efficient, supports rich structures)
Interactive Payload Comparison
Show code
viewof payloadData = Inputs. form ({
deviceId : Inputs. text ({label : "Device ID" , value : "temp42" }),
sensorValue : Inputs. range ([0 , 100 ], {label : "Sensor Value" , value : 23.5 , step : 0.1 }),
unit : Inputs. select (["C" , "F" , "K" ], {label : "Unit" , value : "C" }),
timestamp : Inputs. toggle ({label : "Include Timestamp" , value : true })
})
coapPayloadSizes = {
const data = payloadData;
// Text/plain (just the value)
const textPlain = data. sensorValue . toString ();
// JSON
const jsonObj = {
device : data. deviceId ,
value : data. sensorValue ,
unit : data. unit
};
if (data. timestamp ) {
jsonObj. timestamp = Math . floor (Date . now () / 1000 );
}
const jsonStr = JSON . stringify (jsonObj);
// CBOR estimate (roughly 50-60% of JSON for simple objects)
const cborEstimate = Math . ceil (jsonStr. length * 0.55 );
return {
textPlain : textPlain. length ,
json : jsonStr. length ,
cbor : cborEstimate,
jsonStr : jsonStr,
textStr : textPlain
};
}
html `
<div style="font-family: Arial, sans-serif; max-width: 700px; margin: 20px 0;">
<h4 style="color: #2C3E50; margin-bottom: 15px;">CoAP Payload Size Comparison</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 15px; margin-bottom: 20px;">
<div style="background: linear-gradient(135deg, #2C3E50 0%, #34495e 100%); padding: 20px; border-radius: 8px; color: white; text-align: center;">
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 5px;">Text/Plain</div>
<div style="font-size: 32px; font-weight: bold;"> ${ coapPayloadSizes. textPlain } </div>
<div style="font-size: 12px; opacity: 0.8;">bytes</div>
</div>
<div style="background: linear-gradient(135deg, #E67E22 0%, #d35400 100%); padding: 20px; border-radius: 8px; color: white; text-align: center;">
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 5px;">JSON</div>
<div style="font-size: 32px; font-weight: bold;"> ${ coapPayloadSizes. json } </div>
<div style="font-size: 12px; opacity: 0.8;">bytes</div>
</div>
<div style="background: linear-gradient(135deg, #16A085 0%, #138871 100%); padding: 20px; border-radius: 8px; color: white; text-align: center;">
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 5px;">CBOR (est)</div>
<div style="font-size: 32px; font-weight: bold;"> ${ coapPayloadSizes. cbor } </div>
<div style="font-size: 12px; opacity: 0.8;">bytes</div>
</div>
</div>
<div style="background: #f8f9fa; padding: 15px; border-radius: 6px; border-left: 4px solid #16A085;">
<div style="font-size: 13px; color: #2C3E50; margin-bottom: 10px;"><strong>Payload Examples:</strong></div>
<div style="font-family: 'Courier New', monospace; font-size: 12px; color: #555; overflow-wrap: anywhere; word-break: break-word;">
<div style="margin-bottom: 8px;"><strong>Text/plain:</strong> ${ coapPayloadSizes. textStr } </div>
<div><strong>JSON:</strong> ${ coapPayloadSizes. jsonStr } </div>
</div>
</div>
<div style="margin-top: 15px; padding: 12px; background: #E8F6F3; border-radius: 6px;">
<strong style="color: #16A085;">Recommendation:</strong>
<span style="color: #2C3E50;">For battery-powered sensors, use ${ coapPayloadSizes. cbor < 20 ? 'CBOR or text/plain' : 'CBOR' } to minimize energy per transmission.</span>
</div>
</div>
`
Versioning Strategy
IoT devices often run for years - plan for API evolution:
URI versioning (recommended for CoAP):
coap://sensor.local/v1/temperature - original API
coap://sensor.local/v2/temperature - new version with metadata
Why URI versioning for IoT:
Simple for embedded clients
No custom header parsing needed
Clear in logs and debugging
Works with all CoAP libraries
Version migration example:
v1 : GET coap://sensor.local/v1/temperature returns "23.5".
v2 : GET coap://sensor.local/v2/temperature returns a richer CBOR payload with value, unit, and timestamp.
Rollout rule : legacy devices stay on v1 while new devices adopt v2.
Error Handling
Use proper CoAP response codes and provide helpful error payloads:
Standard Response Codes
2.01 Created - POST created a new resource
2.04 Changed - PUT updated an existing resource
2.05 Content - GET returned a payload
4.00 Bad Request - request syntax or payload was invalid
4.01 Unauthorized - authentication is required
4.04 Not Found - resource does not exist
4.05 Method Not Allowed - method does not apply to that resource
5.00 Internal Server Error - server failed while handling the request
Error Payload Format
JSON for human readability:
{
"error" : {
"code" : "SENSOR_OFFLINE" ,
"message" : "Device has not reported in 5 minutes" ,
"timestamp" : "2025-01-15T10:30:00Z" ,
"device_id" : "temp42" ,
"retry_after" : 300
}
}
CBOR for production (more efficient):
1 -> "SENSOR_OFFLINE"
2 -> "Device offline"
3 -> 1642259400
4 -> "temp42"
5 -> 300
Message Type Selection
Choose between CON and NON based on criticality:
Use CON (Confirmable) for:
Actuator commands (LED on/off, valve open/close)
Configuration changes
Alerts and alarms
Any operation where failure must be detected
Use NON (Non-confirmable) for:
Frequent sensor readings (temperature every minute)
Telemetry streams
Status updates
Any data where next update supersedes previous
Decision tree:
Is the data critical?
If yes, use CON.
If no, ask whether the data will be sent again soon.
If yes, use NON.
If no, use CON so you can detect loss.
Security Best Practices
Always Use DTLS in Production
coaps://sensor.local/v1/temperature # Secure CoAP
Authentication options:
Pre-Shared Key (PSK) - Simplest for constrained devices
Raw Public Key (RPK) - No certificate infrastructure needed
X.509 Certificates - Enterprise deployments
Rate Limiting
Protect your system from misbehaving devices:
Per-device limits:
Response code: 4.29 Too Many Requests
Error key: RATE_LIMIT_EXCEEDED
Example limit: 10 requests/minute
Retry hint: retry_after = 45
Implementation strategies:
Token bucket algorithm (allow bursts, limit sustained rate)
Return Max-Age option to indicate when retry is allowed
Log violations for debugging
Interactive Rate Limit Analysis
Show code
viewof rateLimitParams = Inputs. form ({
maxRequests : Inputs. range ([1 , 100 ], {label : "Max Requests per Window" , value : 10 , step : 1 }),
windowSeconds : Inputs. range ([10 , 300 ], {label : "Time Window (seconds)" , value : 60 , step : 10 }),
burstSize : Inputs. range ([1 , 50 ], {label : "Burst Size (tokens)" , value : 5 , step : 1 }),
refillRate : Inputs. range ([0.1 , 10 ], {label : "Refill Rate (tokens/sec)" , value : 0.17 , step : 0.01 })
})
rateLimitViz = {
const p = rateLimitParams;
// Calculate effective rates
const reqPerSec = p. maxRequests / p. windowSeconds ;
const sustainedReqPerMin = reqPerSec * 60 ;
const burstDuration = p. burstSize / reqPerSec;
const refillTime = p. burstSize / p. refillRate ;
// Token bucket state over time (simulate 2 minutes)
const timeline = [];
let tokens = p. burstSize ;
let requests = 0 ;
for (let t = 0 ; t <= 120 ; t++ ) {
// Refill tokens
tokens = Math . min (p. burstSize , tokens + p. refillRate );
// Simulate request every 5 seconds initially, then every 10 seconds
if (t % (t < 30 ? 5 : 10 ) === 0 && tokens >= 1 ) {
tokens -= 1 ;
requests++;
}
if (t % 5 === 0 ) { // Sample every 5 seconds
timeline. push ({time : t, tokens : tokens. toFixed (2 ), requests});
}
}
return {
reqPerSec : reqPerSec. toFixed (3 ),
sustainedReqPerMin : sustainedReqPerMin. toFixed (1 ),
burstDuration : burstDuration. toFixed (1 ),
refillTime : refillTime. toFixed (1 ),
timeline,
totalRequests : requests
};
}
html `
<div style="font-family: Arial, sans-serif; max-width: 750px; margin: 20px 0;">
<h4 style="color: #2C3E50; margin-bottom: 15px;">CoAP Rate Limit Calculator (Token Bucket)</h4>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 20px;">
<div style="background: linear-gradient(135deg, #9B59B6 0%, #8E44AD 100%); padding: 20px; border-radius: 8px; color: white; text-align: center;">
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 5px;">Sustained Rate</div>
<div style="font-size: 36px; font-weight: bold;"> ${ rateLimitViz. sustainedReqPerMin } </div>
<div style="font-size: 14px; opacity: 0.9;">requests/minute</div>
</div>
<div style="background: linear-gradient(135deg, #E67E22 0%, #D35400 100%); padding: 20px; border-radius: 8px; color: white; text-align: center;">
<div style="font-size: 14px; opacity: 0.9; margin-bottom: 5px;">Burst Capacity</div>
<div style="font-size: 36px; font-weight: bold;"> ${ rateLimitParams. burstSize } </div>
<div style="font-size: 14px; opacity: 0.9;">tokens</div>
</div>
</div>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;">
<h5 style="color: #2C3E50; margin: 0 0 15px 0;">Rate Limit Characteristics</h5>
<div style="font-size: 13px; color: #555; line-height: 1.9;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>Requests per second:</span>
<strong style="color: #2C3E50;"> ${ rateLimitViz. reqPerSec } </strong>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>Burst duration:</span>
<strong style="color: #2C3E50;"> ${ rateLimitViz. burstDuration } seconds</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span>Full bucket refill time:</span>
<strong style="color: #2C3E50;"> ${ rateLimitViz. refillTime } seconds</strong>
</div>
<div style="display: flex; justify-content: space-between; padding-top: 8px; border-top: 2px solid #ddd;">
<span>2-min simulation allowed:</span>
<strong style="color: #16A085;"> ${ rateLimitViz. totalRequests } requests</strong>
</div>
</div>
</div>
<div style="background: #FFF9E6; padding: 15px; border-radius: 6px; border-left: 4px solid #E67E22;">
<strong style="color: #2C3E50;">Token Bucket Algorithm:</strong>
<div style="font-size: 13px; color: #555; margin-top: 8px;">
Bucket starts with <strong> ${ rateLimitParams. burstSize } tokens</strong>. Each request consumes 1 token.
Tokens refill at <strong> ${ rateLimitParams. refillRate } tokens/second</strong>.
This allows bursts of ${ rateLimitParams. burstSize } requests while enforcing a sustained rate of ${ rateLimitViz. sustainedReqPerMin } req/min.
</div>
</div>
</div>
`
Worked Example: Smart Agriculture API
Scenario: 200 soil moisture sensors across a farm, battery-powered, reporting to a central gateway.
API Design:
Resource structure .../v1/sensors/{sensor_id}/moisture .../v1/sensors/{sensor_id}/battery .../v1/sensors/{sensor_id}/config
Normal operation sensor sends NON POST to .../v1/sensors/field3-42/moisture payload uses compact CBOR with value, unit, and timestamp
Critical alerts sensor sends CON POST to .../v1/sensors/field3-42/alert payload includes type, threshold, and current reading
Battery monitoring gateway uses GET on .../battery with Observe enabled sensor notifies only when thresholds are crossed
Version rollout v1 handles moisture today v2 adds soil temperature and pH later
Why this works:
NON messages save battery (no ACK overhead)
CON ensures critical alerts aren’t lost
CBOR minimizes bandwidth
Observe pattern prevents polling battery status
Versioning allows gradual upgrades
Working Code: Python CoAP Client and Server
Real request/response examples using aiocoap (Python) and the coap-client CLI.
Python CoAP Server (Gateway)
The gateway server only needs a few moving parts:
Create a TemperatureResource that stores the latest value and timestamp.
Implement render_get() to return JSON with CoAP code 2.05 Content.
Implement render_put() to update the reading and call updated_state() for Observe subscribers.
Add a ConfigResource for reporting interval settings.
Register both resources under the v1/temperature and v1/config paths.
Start the server with create_server_context(...), binding to UDP port 5683.
Minimal GET response flow:
Read self.value and self.last_updated.
Encode a JSON object with value, unit, and timestamp as bytes.
Return an aiocoap.Message with content_format=50.
Python CoAP Client (Sensor)
The sensor client loop is similarly compact:
Create a client context with create_client_context().
Build a GET message for .../v1/temperature.
Decode the JSON payload returned with 2.05 Content.
Build a PUT message carrying a JSON body with the new value.
Expect 2.04 Changed when the update succeeds.
What to verify during testing:
GET returns 2.05 Content with a JSON payload.
PUT returns 2.04 Changed.
Wrong URI returns 4.04 Not Found.
Unsupported method returns 4.05 Method Not Allowed.
CLI Testing with coap-client
Install the CLI with apt install libcoap2-bin on Linux or brew install libcoap on macOS.
Read a value: coap-client -m get .../v1/temperature
Update a value: coap-client -m put .../v1/temperature body: {"value":23.1}
Observe changes: coap-client -m get -s 60 .../v1/temperature
Discover resources: coap-client -m get .../.well-known/core
Scenario: Battery sensor reports every 60 seconds for 1 year using CR2032 (220 mAh @ 3V).
CON (Confirmable) message energy: \[
\begin{align}
\text{TX message (50 ms @ 10 mA)} &= 50 \times 10^{-3} \times 10 \times 10^{-3} = 0.5 \text{ mAs} \\
\text{RX ACK (100 ms @ 5 mA)} &= 100 \times 10^{-3} \times 5 \times 10^{-3} = 0.5 \text{ mAs} \\
\text{Total per message} &= 1.0 \text{ mAs} = 0.278 \text{ } \mu\text{Ah}
\end{align}
\]
NON (Non-confirmable) message energy: \[
\begin{align}
\text{TX message (50 ms @ 10 mA)} &= 0.5 \text{ mAs} \\
\text{No ACK wait} &= 0 \text{ mAs} \\
\text{Total per message} &= 0.5 \text{ mAs} = 0.139 \text{ } \mu\text{Ah}
\end{align}
\]
Annual comparison (525,600 messages): \[
\begin{align}
\text{CON energy} &= 525{,}600 \times 0.278 = 146 \text{ mAh} \\
\text{Sleep energy} &= 0.005 \times 24 \times 365 = 44 \text{ mAh} \\
\text{Total CON} &= 146 + 44 = 190 \text{ mAh (battery life} = 220/190 = 1.16 \text{ years)} \\
\\
\text{NON energy} &= 525{,}600 \times 0.139 = 73 \text{ mAh} \\
\text{Total NON} &= 73 + 44 = 117 \text{ mAh} \\
\text{Battery life} &= \frac{220}{117} = 1.88 \text{ years}
\end{align}
\]
Result: CON drains battery in 14 months; NON achieves 23-month target. 63% longer battery life with NON.
Interactive Battery Life Optimizer
Show code
viewof batteryParams = Inputs. form ({
messageType : Inputs. radio (["CON" , "NON" ], {label : "Message Type" , value : "NON" }),
reportingInterval : Inputs. range ([10 , 3600 ], {label : "Reporting Interval (seconds)" , value : 60 , step : 10 }),
batteryCap : Inputs. range ([100 , 1000 ], {label : "Battery Capacity (mAh)" , value : 220 , step : 10 }),
txPower : Inputs. range ([5 , 20 ], {label : "TX Power (mA)" , value : 10 , step : 1 }),
rxPower : Inputs. range ([3 , 10 ], {label : "RX Power (mA)" , value : 5 , step : 1 }),
sleepPower : Inputs. range ([0.001 , 0.02 ], {label : "Sleep Current (mA)" , value : 0.005 , step : 0.001 })
})
batteryCalc = {
const p = batteryParams;
// Energy per message
const txEnergy = 50 * p. txPower ; // 50ms TX
const rxEnergy = p. messageType === "CON" ? 100 * p. rxPower : 0 ; // 100ms RX for ACK
const perMessageMas = (txEnergy + rxEnergy) / 1000 ; // Convert to mAs
const perMessageUah = perMessageMas / 3.6 ; // Convert to uAh
// Annual energy
const msgsPerYear = (365 * 24 * 3600 ) / p. reportingInterval ;
const radioMah = (msgsPerYear * perMessageUah) / 1000 ;
const sleepMah = p. sleepPower * 24 * 365 ;
const totalMah = radioMah + sleepMah;
// Battery life
const lifeYears = p. batteryCap / totalMah;
const lifeMonths = lifeYears * 12 ;
const lifeDays = lifeYears * 365 ;
return {
perMessageMas : perMessageMas. toFixed (3 ),
perMessageUah : perMessageUah. toFixed (3 ),
msgsPerYear : Math . round (msgsPerYear),
radioMah : radioMah. toFixed (1 ),
sleepMah : sleepMah. toFixed (1 ),
totalMah : totalMah. toFixed (1 ),
lifeYears : lifeYears. toFixed (2 ),
lifeMonths : Math . round (lifeMonths),
lifeDays : Math . round (lifeDays)
};
}
html `
<div style="font-family: Arial, sans-serif; max-width: 750px; margin: 20px 0;">
<h4 style="color: #2C3E50; margin-bottom: 15px;">CoAP Battery Life Calculator</h4>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; margin-bottom: 20px;">
<div>
<div style="background: linear-gradient(135deg, #3498DB 0%, #2980b9 100%); padding: 25px; border-radius: 8px; color: white; text-align: center;">
<div style="font-size: 16px; opacity: 0.9; margin-bottom: 8px;">Battery Life</div>
<div style="font-size: 42px; font-weight: bold;"> ${ batteryCalc. lifeYears } </div>
<div style="font-size: 16px; opacity: 0.9;">years</div>
<div style="font-size: 14px; opacity: 0.8; margin-top: 8px;">( ${ batteryCalc. lifeMonths } months / ${ batteryCalc. lifeDays } days)</div>
</div>
</div>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; border: 2px solid #e0e0e0;">
<h5 style="color: #2C3E50; margin: 0 0 15px 0;">Energy Breakdown</h5>
<div style="font-size: 13px; color: #555; line-height: 1.8;">
<div style="display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 8px;">
<span>Messages/year:</span>
<strong> ${ batteryCalc. msgsPerYear . toLocaleString ()} </strong>
</div>
<div style="display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 8px;">
<span>Radio energy:</span>
<strong> ${ batteryCalc. radioMah } mAh</strong>
</div>
<div style="display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 8px;">
<span>Sleep energy:</span>
<strong> ${ batteryCalc. sleepMah } mAh</strong>
</div>
<div style="display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding-top: 8px; border-top: 2px solid #ddd;">
<span><strong>Total:</strong></span>
<strong style="color: #E67E22;"> ${ batteryCalc. totalMah } mAh/year</strong>
</div>
</div>
</div>
</div>
<div style="background: ${ batteryParams. messageType === 'CON' ? '#FFF3E0' : '#E8F6F3' } ; padding: 15px; border-radius: 6px; border-left: 4px solid ${ batteryParams. messageType === 'CON' ? '#E67E22' : '#16A085' } ;">
<strong style="color: #2C3E50;">Energy per Message:</strong>
<span style="color: #555;"> ${ batteryCalc. perMessageMas } mAs = ${ batteryCalc. perMessageUah } uAh</span>
${ batteryParams. messageType === 'CON' ?
'<div style="margin-top: 8px; font-size: 13px; color: #D35400;"><strong>Note:</strong> CON messages require ACK reception, doubling radio time. Consider NON for periodic telemetry.</div>' :
'<div style="margin-top: 8px; font-size: 13px; color: #138871;"><strong>Optimized:</strong> NON messages save 50% radio energy. Ideal for periodic sensor readings.</div>'
}
</div>
</div>
`
Common Pitfalls
The mistake : Configuring a server to send Observe notifications on every minor resource change, overwhelming clients.
Symptoms :
Client device becomes unresponsive or crashes
Battery drains rapidly (constant wake-ups)
Network congestion with notification traffic
Client sends RST messages repeatedly
Why it happens : Developers bind notifications directly to sensor sampling rates (e.g., 10 Hz accelerometer) without throttling.
The fix : Implement server-side notification throttling:
# BAD: Notify on every sensor reading
@coap_resource ('/temperature' )
def on_read():
current_temp = read_sensor()
notify_observers(current_temp) # Called 10x/second!
# GOOD: Throttle with change threshold
MIN_NOTIFY_INTERVAL = 5.0 # seconds
CHANGE_THRESHOLD = 0.5 # degrees
@coap_resource ('/temperature' )
def on_read():
current_temp = read_sensor()
should_notify = (
abs (current_temp - last_notified_temp) >= CHANGE_THRESHOLD or
(time.time() - last_notify_time) >= MIN_NOTIFY_INTERVAL
)
if should_notify:
notify_observers(current_temp)
last_notified_temp = current_temp
last_notify_time = time.time()
Prevention:
Set minimum notification intervals (5-60 seconds)
Implement change thresholds (only notify on significant changes)
Use Max-Age option to tell clients how long values are valid
Monitor client RST responses (indicates overwhelmed client)
The Mistake : HTTP-to-CoAP proxy aggressively caches based on Max-Age without considering that freshness requirements vary by use case.
The Fix : Implement per-client cache control at the proxy:
@app.route ('/coap/<path:resource>' )
async def proxy_coap(resource):
# Client-specified freshness requirement
client_max_age = int (request.headers.get('Cache-Control' , 'max-age=60' ).split('=' )[1 ])
# Check cache with client's freshness requirement
if coap_uri in cache:
response, cached_time, server_max_age = cache[coap_uri]
age = time.time() - cached_time
effective_max_age = min (client_max_age, server_max_age)
if age < effective_max_age:
return response.payload # Cache hit
# ... fetch from device if stale
Key principle : Safety-critical clients should always request fresh data (max-age=0).
Concept Relationships
This chapter on CoAP API design connects to several key concepts:
Builds on:
Relates to:
Enables:
See Also
Related Chapters:
External Resources: