| Internet-Draft | IOTMP | March 2026 |
| Luis Bustamante | Expires 1 October 2026 | [Page] |
IOTMP (Internet of Things Message Protocol) is a compact, binary, application-layer protocol for bidirectional communication between IoT devices and servers over persistent connections. It provides a resource-oriented model with native support for remote procedure calls, real-time data streaming, and API introspection. IOTMP uses PSON (Packed Sensor Object Notation) as its data encoding format, achieving significantly lower wire overhead than text-based alternatives. The protocol is designed for constrained devices with limited memory and bandwidth, while remaining expressive enough for complex IoT applications. IOTMP operates over TCP, TLS, or WebSocket transports and supports symmetric operation where both clients and servers may expose and invoke resources.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 1 October 2026.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
IOTMP (Internet of Things Message Protocol) is an application-layer protocol designed for efficient, bidirectional communication between IoT devices and servers. It provides a resource-oriented, request-response model with native support for real-time data streaming, optimized for constrained devices with minimal memory and bandwidth.¶
IOTMP uses PSON (Packed Sensor Object Notation) as its data encoding format rather than an existing binary encoding such as CBOR [RFC8949]. The primary motivation is architectural: PSON and IOTMP share the same encoding primitives, eliminating the need for two independent encoding systems on constrained devices.¶
Protocol-Encoding Integration. IOTMP's message framing layer (Section 7) and PSON use identical encoding primitives: a tag-byte structure where type bits and an inline value are packed into a single byte, and varint encoding for variable-length integers. A single encoder/decoder implementation on a constrained device serves both the protocol framing layer (message fields) and the application data layer (payloads). Adopting an external encoding like CBOR would require a second, independent type system alongside the protocol's own field encoding -- two tag formats, two integer encodings, two sets of type definitions -- increasing code size and implementation complexity for no functional benefit.¶
This integration is particularly valuable on the most constrained targets (Cortex-M0/M0+, 2-16 KB RAM), where every kilobyte of code matters. A complete IOTMP + PSON implementation shares encoding routines between framing and payload, keeping the combined footprint under 400 lines of C.¶
Comparison with CBOR. In terms of wire size, PSON and CBOR produce comparable encodings for typical IoT data. Neither format has a significant size advantage over the other. The differences are in scope and complexity: CBOR is a general-purpose encoding designed to cover a wide range of use cases, including semantic tags (CBOR type 6, with hundreds of IANA-registered values), indefinite-length encoding, half-precision floats, and extensible simple values. PSON deliberately omits these features, which add decoder complexity without benefit for IoT applications. This reduced scope, combined with the shared primitives with IOTMP, is what makes PSON the appropriate choice for this protocol -- not a claim of superior compression.¶
This specification defines:¶
This specification does NOT define:¶
IOTMP is designed for scenarios where IoT devices maintain persistent connections with a server (broker) and require bidirectional communication. The protocol is most appropriate when:¶
IOTMP is NOT the best fit when:¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
IOTMP operates over a persistent, full-duplex connection. After transport establishment, the client authenticates via a CONNECT/OK handshake. Once authenticated, either side can send messages at any time.¶
The protocol supports five interaction patterns:¶
+--------+ +--------+
| Client | | Server |
+---+----+ +---+----+
| CONNECT [credentials] |
|-------------------------------------->|
| OK |
|<--------------------------------------|
| |
| DESCRIBE (full API) |
|<--------------------------------------|
| OK [resource list] |
|-------------------------------------->|
| |
| START_STREAM [resource, interval]|
|<--------------------------------------|
| OK |
|-------------------------------------->|
| STREAM_DATA [payload] |
|-------------------------------------->|
| STREAM_DATA [payload] |
|-------------------------------------->|
| ... |
| STOP_STREAM |
|<--------------------------------------|
| OK |
|-------------------------------------->|
| |
| RUN [WRITE_BUCKET, data] |
|-------------------------------------->|
| OK |
|<--------------------------------------|
| |
| KEEP_ALIVE |
|-------------------------------------->|
| KEEP_ALIVE |
|<--------------------------------------|
| |
| DISCONNECT |
|-------------------------------------->|
+---------------------------------------+
¶
IOTMP is designed to operate over reliable, ordered, byte-stream transports:¶
| Transport | Default Port | Description |
|---|---|---|
| TCP | 25204 | Unencrypted TCP connection |
| TLS over TCP | 25206 | TLS-encrypted TCP connection |
| WebSocket | 443 | WebSocket upgrade over HTTPS, for NAT traversal |
Implementations SHOULD support TLS. Unencrypted TCP SHOULD only be used in trusted networks or during development.¶
When operating over WebSocket, the following requirements apply:¶
"iotmp" in the Sec-WebSocket-Protocol header during the WebSocket handshake.¶
"iotmp" subprotocol in the handshake response. If the server does not confirm it, the client MUST close the connection.¶
All multi-byte numeric values (floats, doubles) MUST be encoded in little-endian byte order. Varint encoding is byte-order independent by design.¶
Rationale: Traditional Internet protocols use big-endian ("network byte order"), a convention established in the 1980s when the dominant networking hardware (Motorola 68000, SPARC, IBM mainframes) was big-endian. Today, virtually all microcontrollers and processors used in IoT are little-endian: ARM Cortex-M (the dominant embedded architecture), ESP32 (Xtensa), RISC-V, and x86. No widely deployed IoT microcontroller uses big-endian as its native byte order.¶
On the most constrained ARM cores (Cortex-M0/M0+), which lack a hardware byte-reversal instruction (REV), swapping a 32-bit float requires 4 instructions per value. For streaming telemetry -- the most common IoT workload -- this overhead is incurred on every sample. Little-endian encoding allows constrained devices to write float and double values directly from memory to the wire with no conversion.¶
This approach only affects [IEEE754] floats and doubles. Integers use varint encoding, which is byte-order independent. The same design choice is made by Protocol Buffers [ProtocolBuffers], which encodes fixed-width floats and doubles in little-endian regardless of platform.¶
Every IOTMP message on the wire consists of a header followed by a body.¶
+------------------+------------------+---------------------+ | Message Type | Body Size | Body | | (varint) | (varint) | (Body Size bytes) | +------------------+------------------+---------------------+¶
IOTMP uses Protocol Buffers-style variable-length integer encoding:¶
Examples:¶
| Decimal | Hex | Varint Bytes (hex) |
|---|---|---|
| 0 | 0x00 |
00
|
| 1 | 0x01 |
01
|
| 127 | 0x7F |
7F
|
| 128 | 0x80 |
80 01
|
| 300 | 0x012C |
AC 02
|
| 16384 | 0x4000 |
80 80 01
|
Implementations MUST NOT encode varints longer than 4 bytes, representing values up to 2^28 - 1 (268,435,455). A receiver that encounters a varint that does not terminate within 4 bytes MUST treat it as a decode error and close the connection.¶
Implementations MUST support messages up to at least 32,768 bytes (32 KB). Implementations MAY support larger messages. Both sides MAY declare their maximum message size using the "ms" parameter: the client in the CONNECT message and the server in the OK response Section 9.2. Each side MUST NOT send messages exceeding the peer's declared maximum. If "ms" is not declared, the default of 32,768 bytes is assumed. A receiver that encounters a message exceeding its maximum size SHOULD close the connection.¶
IOTMP does not define message-level fragmentation. For data transfers that exceed the maximum message size, applications SHOULD use streaming Section 11: a START_STREAM opens a persistent channel over which arbitrarily large data can be sent as a sequence of STREAM_DATA messages, each within the negotiated size limit.¶
The Message Type field identifies the purpose of each message:¶
| Value | Name | Direction | Description |
|---|---|---|---|
| 0x00 | RESERVED | -- | Reserved for future use. MUST NOT be sent. |
| 0x01 | OK | Both | Successful response to a request. |
| 0x02 | ERROR | Both | Error response to a request. |
| 0x03 | CONNECT | Client -> Server | Authentication request with credentials. |
| 0x04 | DISCONNECT | Both | Graceful connection termination. |
| 0x05 | KEEP_ALIVE | Client -> Server (echo: Server -> Client) | Connection liveness probe. Body MUST be empty. |
| 0x06 | RUN | Both | Execute a resource on the peer. |
| 0x07 | DESCRIBE | Both | Request resource metadata/API description. |
| 0x08 | START_STREAM | Both | Begin streaming data from a resource. |
| 0x09 | STOP_STREAM | Both | Stop an active stream. |
| 0x0A | STREAM_DATA | Both | Carry stream payload data. |
Values 0x0B through 0xFF are reserved for future use.¶
Messages that expect a response (CONNECT, RUN, DESCRIBE, START_STREAM, STOP_STREAM) MUST include a Stream ID field. The response (OK or ERROR) MUST carry the same Stream ID to correlate with the request.¶
Note: The Stream ID serves two roles depending on the message type. For one-time request-response exchanges (RUN, DESCRIBE), the Stream ID is a short-lived correlation identifier that is released when the response is received. For streaming operations (START_STREAM), the same Stream ID becomes a long-lived identifier for the stream, used in subsequent STREAM_DATA and STOP_STREAM messages until the stream is terminated. In both cases, the lifecycle rules in Section 6.2 apply.¶
Request Timeout: If a response (OK or ERROR) is not received within a reasonable time, the sender SHOULD consider the request failed and release the Stream ID. Implementations SHOULD use a default timeout of 30 seconds for RUN, DESCRIBE, START_STREAM, and STOP_STREAM requests. The CONNECT message has a separate timeout (RECOMMENDED: 10 seconds, Section 14.5). Implementations MAY use longer timeouts for specific resources that are known to require extended processing.¶
Since IOTMP is a symmetric protocol where both sides may initiate requests concurrently, the Stream ID space is partitioned to avoid collisions:¶
This partitioning ensures that responses (OK, ERROR) and stream data (STREAM_DATA) can be unambiguously correlated with the originating request, even when both sides send requests simultaneously with the same numeric value.¶
Stream ID Lifecycle:¶
A Stream ID is considered active from the moment the request message is sent until:¶
An endpoint MUST NOT reuse a Stream ID that is currently active. Once a Stream ID is released, it MAY be reused for a new request. Implementations SHOULD favor low-valued Stream IDs to minimize varint encoding overhead.¶
A receiver that encounters a request with a Stream ID from the wrong partition (e.g., a client-initiated message with an odd Stream ID) MUST respond with ERROR (400) and SHOULD log the violation.¶
IOTMP is a symmetric protocol. After authentication, both sides MAY send any message type except CONNECT and KEEP_ALIVE. The directional constraints are:¶
The message body consists of zero or more tagged fields. Each field is encoded as:¶
+---------------------------+ | Field Tag (1 byte) | +---------------------------+ | Field Value (variable) | +---------------------------+¶
The field tag is a single byte combining the field number and wire type:¶
Tag = (field_number << 3) | wire_type¶
| Value | Name | Description |
|---|---|---|
| 0x00 | varint | Value is a varint-encoded unsigned integer. |
| 0x01 | bytes | Value is a varint-encoded length followed by raw bytes. The content is opaque to the protocol and MAY carry any application-defined binary format. |
| 0x02 | pson | Value is PSON encoded Section 8. |
Wire types 0x03-0x07 are reserved for future use.¶
| Field Number | Name | Allowed Wire Types | Description |
|---|---|---|---|
| 0x01 | STREAM_ID | varint | 16-bit stream identifier for request/response correlation. |
| 0x02 | PARAMETERS | varint, pson | Request parameters (e.g., server operation code, stream interval). |
| 0x03 | PAYLOAD | pson, bytes | Primary data payload (credentials, resource data, error info). |
| 0x04 | RESOURCE | varint, pson | Resource identifier (string name or 16-bit hash). |
Field 0x00 is reserved and MUST NOT be used. Reserving field zero allows zero-initialized field variables to be distinguished from valid field numbers and serves as a sentinel value for error detection -- a convention shared with Protocol Buffers and other binary protocols. Fields 0x05-0x07 are reserved for future use. A maximum of 8 fields (0x00-0x07) are supported per message.¶
Fields are OPTIONAL. A receiver MUST handle messages with any subset of fields present. The field_mask is implicit in the encoding -- a field is present if and only if its tag appears in the body. Fields MAY appear in any order within the message body. A receiver MUST NOT assume a specific field ordering.¶
A formal CDDL grammar defining all message structures and field requirements is provided in Appendix B.¶
IOTMP uses PSON (Packed Sensor Object Notation) as its data encoding format. PSON is a compact, self-describing binary encoding that efficiently represents the data types commonly found in IoT applications: integers, floating-point numbers, booleans, strings, binary data, arrays, and key-value maps.¶
PSON is specified in a separate document: [PSON]. This section provides a brief summary; the PSON specification is the normative reference for encoding and decoding rules.¶
Each PSON value begins with a tag byte that encodes both the wire type (3 bits) and an inline value (5 bits). Small values (0-30) are encoded in a single byte. Eight wire types are defined:¶
| Wire Type | Name | Description |
|---|---|---|
| 0 | unsigned_t | Unsigned integer |
| 1 | signed_t | Negative integer (stored as absolute value) |
| 2 | floating_t | IEEE 754 float or double |
| 3 | discrete_t | Boolean (false/true) or null |
| 4 | string_t | UTF-8 string |
| 5 | bytes_t | Raw binary data |
| 6 | map_t | Key-value map (keys are strings) |
| 7 | array_t | Ordered array |
For the complete encoding rules, wire format, and examples, see [PSON].¶
+------------------+
| DISCONNECTED |
+--------+---------+
| Transport connect
+--------v---------+
| SOCKET_CONNECTING|
+--------+---------+
| Success
+--------v---------+
| SOCKET_CONNECTED |
+--------+---------+
| Send CONNECT
+--------v---------+
| AUTHENTICATING |
+--------+---------+
OK / \ ERROR
+---------v+ +v----------+
|AUTHENTICATED| |AUTH_FAILED |
+---------+-+ +------------+
|
+-------v-------+
| READY |<--------------+
+-------+-------+ |
| |
+---------v---------+ |
| Process Messages |------------+
| Keepalive |
| Check Streams |
+---------+---------+
| Transport error / DISCONNECT
+--------v----------+
|SOCKET_DISCONNECTED|
+--------+----------+
| Exponential backoff
+------> (reconnect)
¶
The client MUST send a CONNECT message as the first message after transport establishment.¶
CONNECT Fields:¶
| Field | Content |
|---|---|
| STREAM_ID | An even 16-bit value for response correlation (client partition, Section 6.2). |
| PAYLOAD | Authentication data. Format depends on the authentication type ("at" parameter). |
| PARAMETERS | (OPTIONAL) A PSON map with connection options. |
Connection Parameters (PARAMETERS field):¶
| Key | Type | Description | Default |
|---|---|---|---|
| "v" | uint | Protocol version. Current version: 1. | 1 |
| "ka" | uint | Keepalive interval in seconds. Max: 1800. | 60 |
| "at" | uint | Authentication type (see below). | 0 |
| "ms" | uint | Maximum message size in bytes the client can accept. Min: 1024. | 32768 |
Authentication Types:¶
The "at" parameter determines the format and interpretation of the PAYLOAD field:¶
| Code | Name | PAYLOAD Format |
|---|---|---|
| 0 | Credentials | A PSON array of 3 strings: [namespace, device_id, credential]. The namespace identifies the organizational scope (account, project, or tenant) under which the device is registered. The device_id uniquely identifies the device within the namespace. The credential is the shared secret. |
| 1 | Token | A PSON string containing a bearer token (e.g., JWT, API key, or opaque token). The server determines the device identity from the token claims or by lookup. |
| 2 | Certificate | Device identity is established via the client certificate presented during the TLS handshake (mTLS). This type MUST only be used over TLS with client certificate authentication. PAYLOAD is OPTIONAL: when the certificate identifies only the namespace (e.g., via CN or SAN), the PAYLOAD MUST be a PSON array of 2 strings [namespace, device_id] so the server can identify the specific device. When the certificate identifies both namespace and device, the PAYLOAD MAY be absent or empty. If PAYLOAD is present, the server MUST verify that the namespace matches the certificate identity and MUST reject the connection with ERROR (401) on mismatch. See Section 14.3.1 for security considerations on fleet certificates. |
Authentication types 3-255 are reserved for future use (e.g., OAuth 2.0 device flow, challenge-response). A server that receives an unrecognized authentication type MUST respond with ERROR (400).¶
If the "at" parameter is omitted, type 0 (Credentials) is assumed and the PAYLOAD MUST follow the Credentials format.¶
Server Response:¶
Extensibility of Connection Parameters:¶
The PARAMETERS maps in both the CONNECT and OK messages are extensible. Implementations MUST ignore unknown keys in PARAMETERS and process known keys normally. Future versions of this protocol or application profiles MAY define additional keys for capability negotiation (e.g., supported authentication types, maximum concurrent streams, or protocol extensions). This allows the connection handshake to evolve without breaking backward compatibility.¶
The "v" parameter in the CONNECT message indicates the protocol version the client wishes to use.¶
Version Mismatch Response Example:¶
ERROR Message:
STREAM_ID: (mirrors CONNECT)
PARAMETERS: 400
PAYLOAD: {"error": "Unsupported protocol version",
"supported": [1]}
¶
The client MAY retry the connection using a version from the supported list.¶
The keepalive mechanism verifies connection liveness for both sides. KEEP_ALIVE is a client-initiated message with an empty body (Body Size = 0). The server echoes each KEEP_ALIVE back, allowing the client to confirm reachability. The server determines client liveness by monitoring incoming traffic -- any message from the client (KEEP_ALIVE, STREAM_DATA, RUN, or any other type) resets the server's inactivity timer.¶
This asymmetric design simplifies server implementation: the server does not need to maintain per-connection keepalive send timers -- it only needs a single inactivity timeout per connection. This is the same model used by [MQTT] (PINGREQ/PINGRESP) and WebSocket (Ping/Pong).¶
Client behavior:¶
Server behavior:¶
Either side MAY send a DISCONNECT message to initiate graceful shutdown. Upon receiving DISCONNECT, the peer SHOULD close the transport connection. No response is expected.¶
DISCONNECT Fields:¶
| Field | Content |
|---|---|
| PARAMETERS | (OPTIONAL) Status code (varint). |
| PAYLOAD | (OPTIONAL) Additional information (PSON-encoded). |
A DISCONNECT message with no fields indicates a normal graceful shutdown.¶
A server MAY redirect a client to a different server in two scenarios:¶
At connection time: The server responds to CONNECT with an ERROR message containing a redirect status code and the target server information in the PAYLOAD.¶
During an active connection: The server sends a DISCONNECT message with a redirect status code and the target server in the PAYLOAD. The client SHOULD close the current connection and connect to the indicated server.¶
Redirect Status Codes:¶
| Code | Meaning | Client Behavior |
|---|---|---|
| 301 | Moved Permanently | Client MUST update its stored server address and connect to the new server for all future connections. |
| 307 | Temporary Redirect | Client SHOULD connect to the indicated server now, but MUST retain the original server address for future connections. |
Redirect Payload:¶
The PAYLOAD MUST be a PSON map containing the target server information:¶
| Key | Type | Description | Required |
|---|---|---|---|
| "host" | string | Hostname or IP address of the target. | Yes |
| "port" | uint | Port number of the target. | No |
If "port" is omitted, the client SHOULD use the same port as the current connection.¶
Redirect Example (at connection time):¶
Client -> Server: CONNECT [credentials]
Server -> Client: ERROR
STREAM_ID: (mirrors CONNECT)
PARAMETERS: 307
PAYLOAD: {"host": "server2.example.com",
"port": 25206}
¶
Redirect Example (during active connection):¶
Server -> Client: DISCONNECT
PARAMETERS: 301
PAYLOAD: {"host": "new-server.example.com"}
¶
IOTMP models capabilities as resources -- named endpoints that can be invoked by the peer. Both clients and servers MAY expose resources. Each resource has a defined I/O type that determines how data flows through it.¶
| Type | Code | Data Flow | Description |
|---|---|---|---|
| none | 0 | -- | No callback registered. |
| run | 1 | -> (trigger) | Action with no data input or output. |
| input | 2 | Caller -> Resource | Receives data (actuator, setter). |
| output | 3 | Resource -> Caller | Produces data (sensor, getter). |
| input_output | 4 | Bidirectional | Accepts input and produces output (property). |
Either side MAY send a RUN message to invoke a resource on the peer:¶
RUN Fields:¶
| Field | Content |
|---|---|
| STREAM_ID | Request correlation identifier. |
| RESOURCE | Resource name (string) or resource hash (unsigned integer, Section 10.6). |
| PAYLOAD | (OPTIONAL) Input data for the resource. |
The DESCRIBE message enables API introspection. Either peer can request:¶
DESCRIBE responses MUST include a "v" (version) field at the root level to indicate the description format version. The current version is 1.¶
| Version | Description |
|---|---|
| 1 | Resource descriptions with optional [JSON-Schema] support for input/output validation and introspection. |
Receivers that encounter an unrecognized version SHOULD ignore unknown fields and interpret the response on a best-effort basis. The version field enables forward-compatible evolution of the description format as the protocol evolves (e.g., adding richer metadata, new keywords, or alternative schema languages in future versions).¶
A DESCRIBE with no RESOURCE field returns a map containing the description version and a "res" field with all resource names and their metadata.¶
{
"v": 1,
"res": {
"temperature": {
"fn": 3,
"description": "Room temperature sensor"
},
"led": {"fn": 2, "description": "Status LED control"},
"relay": {"fn": 4},
"reboot": {"fn": 1}
}
}
¶
The "res" field is a PSON map where each key is a resource name and each value is a resource descriptor. This separation ensures that protocol-level fields (such as "v") do not collide with resource names.¶
Each resource entry MUST include a "fn" field with the I/O type code Section 10.2. Each resource entry MAY include a "description" field with a human-readable string describing the resource's purpose.¶
A DESCRIBE with a RESOURCE field returns the resource's input/output information. The response contains "in" and/or "out" fields (depending on the resource's I/O type), each structured as an object with the following fields:¶
| Field | Required | Description |
|---|---|---|
value
|
YES | Current or sample data from the resource callback. |
schema
|
NO | A [JSON-Schema] object describing the expected data structure. |
Example -- Sensor with input and output (I/O type input_output):¶
{
"v": 1,
"in": {
"value": {"brightness": 128},
"schema": {
"type": "object",
"properties": {
"brightness": {
"type": "integer",
"minimum": 0,
"maximum": 255,
"description": "LED brightness level"
}
}
}
},
"out": {
"value": {"celsius": 22.5, "fahrenheit": 72.5},
"schema": {
"type": "object",
"properties": {
"celsius": {
"type": "number",
"minimum": -40,
"maximum": 125,
"description": "Temperature in Celsius"
},
"fahrenheit": {
"type": "number",
"minimum": -40,
"maximum": 257,
"description": "Temperature in Fahrenheit"
}
}
}
}
}
¶
Example -- Output-only sensor (I/O type output):¶
{
"v": 1,
"out": {
"value": {
"latitude": 40.4168,
"longitude": -3.7038,
"altitude": 650.0
},
"schema": {
"type": "object",
"properties": {
"latitude": {
"type": "number",
"minimum": -90,
"maximum": 90
},
"longitude": {
"type": "number",
"minimum": -180,
"maximum": 180
},
"altitude": {
"type": "number",
"description": "Altitude in meters"
}
}
}
}
}
¶
Example -- Input-only actuator (I/O type input):¶
{
"v": 1,
"in": {
"value": {"on": false},
"schema": {
"type": "object",
"properties": {
"on": {"type": "boolean", "description": "Relay state"}
}
}
}
}
¶
Example -- Minimal description without schema:¶
{
"v": 1,
"out": {
"value": {"celsius": 22.5, "fahrenheit": 72.5}
}
}
¶
The "schema" field, when present, MUST be a valid [JSON-Schema] object. Implementations SHOULD use the following JSON Schema keywords where applicable:¶
| Keyword | Purpose |
|---|---|
type
|
Data type (boolean, integer, number, string, object, array). |
properties
|
Named fields within an object. |
items
|
Element schema for arrays. |
description
|
Human-readable field description. |
minimum
|
Lower bound for numeric values. |
maximum
|
Upper bound for numeric values. |
enum
|
Array of allowed values. |
readOnly
|
Field cannot be written. |
writeOnly
|
Field cannot be read. |
default
|
Default value. |
required
|
Array of required property names. |
The "schema" field is OPTIONAL. When absent, receivers SHOULD infer data types from the sample values in "value". Resources that only have output (e.g., sensors) include only "out"; resources that only have input (e.g., actuators) include only "in"; resources with no data (type run) MAY omit both.¶
Servers MAY use the schema information to validate incoming data before forwarding it to the device, generate user interfaces automatically, or produce API documentation compatible with standards such as OpenAPI.¶
When a resource with one or more active streams receives input via a RUN message, the resource owner (typically the client) SHOULD immediately send a STREAM_DATA message with the updated resource state on each active stream for that resource. This enables real-time synchronization -- for example, when a server sets a property value via RUN, all dashboards subscribed to that resource's stream receive the updated state without waiting for the next periodic sample.¶
Behavior:¶
Example:¶
S -> C: START_STREAM
(Stream 0x01, resource: "relay", interval: 5000)
C -> S: OK (Stream 0x01)
C -> S: STREAM_DATA (Stream 0x01, {"on": false})
<- initial state
S -> C: RUN (Stream 0x03, resource: "relay",
payload: {"on": true})
C -> S: OK (Stream 0x03)
C -> S: STREAM_DATA (Stream 0x01, {"on": true})
<- echo (immediate)
... 5 seconds later ...
C -> S: STREAM_DATA (Stream 0x01, {"on": true})
<- periodic sample
¶
In this example, the server has an active stream (Stream ID 0x01) observing the "relay" resource. When the server changes the relay state via RUN (Stream ID 0x03), the client sends the RUN response (OK) first, then immediately echoes the updated state on the active stream (Stream ID 0x01). This ensures that any dashboard or application consuming the stream sees the change in real time, without waiting for the next periodic sample.¶
As an optimization for high-frequency interactions, IOTMP supports identifying resources by a numeric hash of their name instead of the full string. This eliminates the need for prior DESCRIBE exchanges or state synchronization between peers.¶
The RESOURCE field in any message (RUN, DESCRIBE, START_STREAM, STOP_STREAM) accepts both:¶
/).¶
The receiver MUST support both forms. When a PSON unsigned integer is received, the receiver computes the hash of each of its defined resources and matches the incoming value. When a PSON string is received, the receiver looks up the resource by name.¶
Hash Function:¶
Implementations MUST use FNV-1a (Fowler-Noll-Vo) truncated to 16 bits:¶
hash = 0x811C9DC5 // FNV offset basis (32-bit)
for each byte b in name:
hash = hash XOR b
hash = hash * 0x01000193 // FNV prime (32-bit)
return hash AND 0xFFFF // truncate to 16 bits
¶
This function is simple to implement on constrained devices and provides good distribution across typical IoT resource names.¶
Reference Implementation (C):¶
uint16_t fnv1a_16(const char *name) {
uint32_t hash = 0x811C9DC5;
while (*name) {
hash ^= (uint8_t)*name++;
hash *= 0x01000193;
}
return (uint16_t)(hash & 0xFFFF);
}
¶
Test Vectors:¶
| Resource Name | FNV-1a (32-bit) | Truncated (16-bit) | Hex |
|---|---|---|---|
"temperature"
|
0xE9F2A935 | 0xA935 |
A935
|
"humidity"
|
0x25C5B9A0 | 0xB9A0 |
B9A0
|
"led"
|
0x406AEACA | 0xEACA |
EACA
|
"relay"
|
0x16B481C2 | 0x81C2 |
81C2
|
"reboot"
|
0x87729FB8 | 0x9FB8 |
9FB8
|
Example:¶
RUN with string: RESOURCE = "temperature" (12 bytes in PSON) RUN with hash: RESOURCE = 0xA935 (3 bytes in PSON)¶
Both identify the same resource. The hash form saves 9 bytes per message.¶
Applicability:¶
Resource hashing is best suited for simple resource names without dynamic parameters. Resources with path-based parameters (e.g., fs/home/config.txt) SHOULD use the string form, since the path contains dynamic segments that cannot be pre-hashed.¶
Collision Handling:¶
With a 16-bit hash space (65,536 values), the probability of at least one collision follows the birthday problem. For typical IoT devices with small resource sets, the probability is very low:¶
| Resources | P(collision) |
|---|---|
| 10 | < 0.1% |
| 20 | 0.3% |
| 30 | 0.7% |
| 50 | 1.9% |
| 100 | 7.3% |
For constrained devices with fewer than 30 resources, hash-based identification is safe for practical purposes. Implementations MUST verify at device startup that no two resource names produce the same hash. If a collision is detected, the implementation MUST fall back to string-based identification for the affected resources and SHOULD emit a diagnostic warning (e.g., via log output or assertion). If resources are added dynamically at runtime, the implementation MUST check for hash collisions against all existing resources before enabling hash-based identification for the new resource.¶
Unmatched Hash Handling:¶
If a receiver receives a PSON unsigned integer in the RESOURCE field and no defined resource matches the hash value, the receiver MUST respond with ERROR (404). This is consistent with the behavior for unrecognized string resource names Section 15.3.¶
Each stream is identified by a Stream ID and transitions through the following states:¶
+-----------+
| IDLE |
+-----+-----+
| Send/Receive START_STREAM
+-----v-----+
| OPENING |
+-----+-----+
OK / \ ERROR / Timeout
+-------v---+ +---v---------+
| ACTIVE | | FAILED |
+-------+---+ +---+---------+
| | Stream ID released
| +------> (IDLE)
|
| Send/Receive STOP_STREAM
+----v-----+
| CLOSING |
+----+-----+
| OK / ERROR / Timeout
+----v-----+
| IDLE | Stream ID released
+----------+
¶
State Definitions:¶
| State | Description |
|---|---|
| IDLE | No active stream for this Stream ID. The ID is available for reuse. |
| OPENING | START_STREAM has been sent; awaiting OK or ERROR response. |
| ACTIVE | Stream is established. STREAM_DATA messages are being sent by the resource owner. |
| CLOSING | STOP_STREAM has been sent; awaiting OK or ERROR response. |
| FAILED | START_STREAM was rejected (ERROR) or timed out. Stream ID is released. |
Transition Rules:¶
Edge Cases:¶
| Condition | Required Behavior |
|---|---|
| STREAM_DATA received while in OPENING state | Receiver SHOULD buffer or discard; sender MUST NOT send STREAM_DATA before receiving OK. |
| START_STREAM received for an already ACTIVE Stream ID | Receiver MUST respond with ERROR (409 Conflict). |
| STOP_STREAM received for a Stream ID not in ACTIVE state | Receiver MUST respond with ERROR (409 Conflict). |
| STOP_STREAM rejected with ERROR | Initiator MUST still consider the stream terminated and release the Stream ID. |
| RUN received targeting a resource with an ACTIVE stream | Resource executes normally. If stream echo is enabled Section 10.5, the client MUST send a STREAM_DATA with the updated state on all ACTIVE streams for that resource. |
Server Client | START_STREAM [resource, interval] | |-------------------------------------->| | OK | |<--------------------------------------| | STREAM_DATA [payload] | |<--------------------------------------| (initial state) | STREAM_DATA [payload] | |<--------------------------------------| (after interval) | ... | | STOP_STREAM | |-------------------------------------->| | OK | |<--------------------------------------|¶
| Field | Content |
|---|---|
| STREAM_ID | Identifies this stream. Used in subsequent STREAM_DATA/STOP. |
| RESOURCE | Name of the resource to stream. |
| PARAMETERS | (OPTIONAL) Stream configuration. See below. |
PARAMETERS Encoding:¶
When PARAMETERS is a simple unsigned integer (varint), it represents the stream interval in milliseconds. A value of 0 indicates event-driven streaming (no periodic sampling).¶
When PARAMETERS is a PSON map, it MAY contain the following fields:¶
| Key | Type | Description | Default |
|---|---|---|---|
| "i" | uint | Stream interval in milliseconds. 0 = event-driven. | 0 |
| "cm" | bool | Compact mode. Request compact encoding for STREAM_DATA. | false |
For backward compatibility, a server that only needs to set the interval MAY send PARAMETERS as a plain varint. A client MUST accept both encodings.¶
Upon receiving START_STREAM:¶
"cm": true. If compact mode is not active, the "cm" field SHOULD be omitted.¶
| Field | Content |
|---|---|
| STREAM_ID | The stream identifier from START_STREAM. |
| PAYLOAD | The resource output data, PSON-encoded. |
STREAM_DATA messages do NOT require a response.¶
Compact encoding mode reduces per-message overhead for resources that consistently return PSON maps with the same set of keys. It eliminates the repetition of string keys in every STREAM_DATA message after the first.¶
"cm": true in the START_STREAM PARAMETERS. If "cm" is absent or false, the server does not want compact mode and the client MUST NOT activate it.¶
"cm": true in the OK response PARAMETERS. If the resource does not support compact mode, the client omits "cm" from the OK response and sends all STREAM_DATA messages as full PSON maps.¶
"cm": true before assuming compact encoding is active. If "cm" is absent from the OK response, the server MUST decode all STREAM_DATA messages as standard PSON values.¶
The first STREAM_DATA message on a compact stream MUST be a full PSON map with string keys. This message establishes the key order (schema) for all subsequent messages on this stream.¶
First STREAM_DATA:
PAYLOAD: {"temperature": 23.5, "humidity": 60}
(PSON map -- schema)
¶
Both the client and server MUST store the key order from this first map. The key order is determined by the iteration order of the PSON map fields.¶
All subsequent STREAM_DATA messages on this stream MUST encode the PAYLOAD as a PSON array with values in the same positional order as the keys in the schema message.¶
Subsequent STREAM_DATA:
PAYLOAD: [23.6, 61]
(PSON array -- values only)
¶
The server reconstructs the full map by matching each array position to the corresponding key from the schema.¶
null at that position to maintain alignment.¶
Compact mode applies recursively to nested PSON maps. When the schema message contains a map value nested within the top-level map, that nested map is also converted to a positional array in subsequent compact messages. This process applies at all nesting levels:¶
null placeholder rule applies at every nesting level for absent values.¶
Distinguishing arrays from compacted maps:¶
The receiver MUST store the type of each value from the schema message (the first STREAM_DATA). In subsequent compact messages, when the receiver encounters a PSON array at a given position, it checks the schema to determine the original type:¶
This distinction is unambiguous because the schema message provides the complete type information for every value at every nesting level.¶
Example -- Mixed maps and arrays:¶
Schema message (first STREAM_DATA):
PAYLOAD: {"temperature": 23.5,
"tags": ["indoor", "sensor"],
"location": {"lat": 40.4168, "lon": -3.7038}}
Compact messages (subsequent STREAM_DATA):
PAYLOAD: [23.6, ["indoor", "active", "new"], [40.4200, -3.7035]]
¶
The receiver reconstructs the full structure using the schema:¶
["indoor", "active", "new"] (kept as-is, length may vary).¶
[40.4200, -3.7035] (expanded to {"lat": 40.4200, "lon": -3.7035} using stored key order).¶
Server -> Client: START_STREAM
STREAM_ID: 0x00A1
RESOURCE: "environment"
PARAMETERS: {"i": 5000, "cm": true}
Client -> Server: OK
STREAM_ID: 0x00A1
PARAMETERS: {"cm": true}
Client -> Server: STREAM_DATA (schema message)
STREAM_ID: 0x00A1
PAYLOAD: {"temperature": 23.5, "humidity": 60, "pressure": 1013}
(PSON map: 38 bytes)
Client -> Server: STREAM_DATA (compact)
STREAM_ID: 0x00A1
PAYLOAD: [23.6, 61, 1013]
(PSON array: 10 bytes -- 74% smaller)
Client -> Server: STREAM_DATA (compact)
STREAM_ID: 0x00A1
PAYLOAD: [23.7, 62, 1014]
(PSON array: 10 bytes)
¶
Compact mode is most effective when:¶
| Scenario | Normal (bytes/sample) | Compact (bytes/sample) | Savings |
|---|---|---|---|
| 2 sensors (temp + humidity) | 35 | 14 | 60% |
| 8 sensors (environmental station) | 83 | 27 | 67% |
For 100 samples of 2 sensors:¶
| Mode | Total bytes | vs MQTT 3.1.1 | vs MQTT v5 (alias) |
|---|---|---|---|
| Normal | 3,816 | 40% savings | 11% savings |
| Compact | 1,421 | 78% savings | 67% savings |
| Field | Content |
|---|---|
| STREAM_ID | The stream identifier to stop. |
Either side MAY send STOP_STREAM to terminate an active stream. The stream initiator sends STOP_STREAM when it no longer needs the data. The resource owner sends STOP_STREAM when the resource is no longer available (e.g., a terminal session has ended or the device cannot sustain the stream).¶
Upon receiving STOP_STREAM:¶
A client MUST support multiple simultaneous streams. Different resources MAY have different stream intervals and encoding modes (normal or compact). The Stream ID space is partitioned between client and server Section 6.2, providing 32,768 IDs per side -- ample for concurrent streams on a single connection.¶
IOTMP does not define an application-level flow control mechanism (such as credit-based windows). Flow control is delegated to the transport layer: TCP's built-in backpressure naturally throttles the sender when the receiver cannot consume data fast enough.¶
If a device cannot accept a new stream due to resource constraints (memory, CPU, or maximum stream limit), it MUST respond to the START_STREAM request with ERROR (429). Similarly, if a device cannot execute a RUN request due to overload, it MAY respond with ERROR (429).¶
The server controls the data rate of each stream through the interval parameter in START_STREAM. Implementations SHOULD use appropriate intervals to avoid overwhelming constrained devices.¶
IOTMP does not define application-level delivery guarantees (such as QoS levels). Request-response exchanges (RUN, DESCRIBE, START_STREAM, STOP_STREAM) provide implicit delivery confirmation through OK/ERROR responses. Streaming data (STREAM_DATA) is ephemeral by nature -- each sample supersedes the previous one -- so retransmission of individual samples is unnecessary. Connection-level reliability is delegated to the transport layer (TCP/TLS).¶
IOTMP is a symmetric protocol: both clients and servers MAY expose resources. After authentication, either side can invoke resources on the peer using RUN, DESCRIBE, START_STREAM, and STOP_STREAM messages. The resource namespace is defined by each implementation.¶
This symmetry enables diverse use cases without protocol-level changes:¶
The specific resources exposed by a server or client are outside the scope of this specification. Implementations MAY define application profiles that specify well-known resource names, their expected I/O types, and PAYLOAD formats.¶
While Section 11 describes streaming primarily in the context of periodic telemetry (resource owner sends STREAM_DATA at regular intervals), IOTMP streams are general-purpose bidirectional data channels. Once a stream is active, both sides MAY send STREAM_DATA messages on the same Stream ID simultaneously. This enables a wide range of application patterns beyond periodic sampling, using only existing protocol primitives.¶
This bidirectional capability has been validated in production implementations supporting remote terminals, file transfers of multiple gigabytes, TCP proxy tunneling, and firmware updates -- all over the same IOTMP connection used for sensor telemetry, with no protocol modifications.¶
All bidirectional stream use cases follow the same lifecycle:¶
Initiator Resource Owner | START_STREAM (resource, params) | |------------------------------------------->| | OK (negotiated params) | |<-------------------------------------------| | STREAM_DATA <-------------------------- | (both directions, | ----------------------------> STREAM_DATA | concurrent, on | STREAM_DATA <-------------------------- | the same Stream ID) | ----------------------------> STREAM_DATA | | ... | | STOP_STREAM | |------------------------------------------->| | OK | |<-------------------------------------------|¶
The content and semantics of the STREAM_DATA messages are defined by the application profile for each resource. The protocol layer treats them identically regardless of the use case.¶
For interactive sessions, STREAM_DATA carries raw binary data in both directions simultaneously. This includes:¶
In both cases, data flows continuously in both directions with no application-level acknowledgment -- TCP backpressure provides sufficient flow control for interactive traffic.¶
Initiator Resource Owner
| START_STREAM ("terminal", {"cols":80}) |
|------------------------------------------->| opens PTY
| OK |
|<-------------------------------------------|
| STREAM_DATA ("ls -la\n") | user input
|------------------------------------------->|
| STREAM_DATA ("total 42\ndrwx...") | terminal output
|<-------------------------------------------|
| ... |
¶
Updating stream parameters at runtime:¶
Some interactive sessions require runtime parameter updates without interrupting the data flow (e.g., resizing a terminal window). Since START_STREAM cannot be re-sent on an active Stream ID, the recommended application-level pattern is to expose a companion resource that accepts parameter updates via RUN:¶
Initiator Resource Owner
| START_STREAM ("terminal/main") |
|------------------------------------------->| opens PTY (80x24)
| OK |
|<-------------------------------------------|
| STREAM_DATA <---------------------------> | (bidirectional data)
| ... |
| RUN ("terminal/main/params", |
| {"size":{"cols":120,"rows":40}}) | resize request
|------------------------------------------->| ioctl(TIOCSWINSZ)
| OK |
|<-------------------------------------------|
| STREAM_DATA <---------------------------> | (stream continues)
¶
The companion resource (e.g., terminal/main/params) is a regular IOTMP resource with I/O type input that updates the session configuration. This pattern keeps the protocol simple -- no new message types or stream renegotiation mechanisms are needed -- while allowing applications to define whatever runtime parameters they require.¶
For large data transfers (files, firmware images, logs), one side sends data chunks as binary STREAM_DATA and the other side sends acknowledgments as PSON maps in the opposite direction. This provides application-level flow control suitable for multi-gigabyte transfers.¶
Data flow:¶
{"ack": <sequence_number>, "bytes": <confirmed_bytes>}.¶
Application-level flow control:¶
The recommended approach uses a byte-based sliding window:¶
window_size in the START_STREAM parameters -- the maximum number of unacknowledged bytes allowed in flight.¶
bytes_in_flight (bytes sent minus bytes acknowledged). When bytes_in_flight reaches window_size, the sender pauses until an ACK is received.¶
Recommended defaults:¶
| Parameter | Default | Description |
|---|---|---|
chunk_size
|
64 KB | Size of each binary STREAM_DATA payload. Confirmed by resource owner in OK response. |
window_size
|
2 MB | Maximum unacknowledged bytes in flight. |
ack_timeout
|
10 s | Timeout waiting for an ACK before aborting the transfer. |
Bandwidth limiting: Implementations MAY limit transfer rate by introducing controlled delays between chunks (sender-side) or between ACKs (receiver-side). Delaying ACKs naturally throttles the sender through the flow control window.¶
Small data optimization: For data that fits within a single message (less than or equal to the negotiated maximum message size), implementations SHOULD use an inline RUN request/response instead of opening a stream, avoiding the START_STREAM/STOP_STREAM overhead.¶
For operations that are self-contained (short-lived, bounded output), a simple RUN request/response is sufficient. The client exposes a resource that accepts a command as input and returns the result (stdout, stderr, exit code) in the OK payload. No streaming is needed.¶
S -> C: RUN ("cmd",
{"exec":"uname","args":["-a"],"timeout":10})
C -> S: OK ({"stdout": "Linux device 5.15.0 ...",
"stderr": "", "exit_code": 0})
¶
These use cases illustrate a key IOTMP design property: the protocol provides a small set of general-purpose primitives (RUN for one-shot operations, START_STREAM/STREAM_DATA for persistent channels) that application profiles combine to address diverse requirements. No protocol-level changes are needed to support new use cases -- only the definition of new resource names and their data formats.¶
RUN Message:
STREAM_ID: 0x1234
RESOURCE: "storage/sensor_data" (PSON string)
PAYLOAD: {"temperature": 25.3, "humidity": 60} (PSON map)
Response:
OK
STREAM_ID: 0x1234
¶
RUN Message: STREAM_ID: 0x5679 RESOURCE: "led" (PSON string) PAYLOAD: true (PSON boolean) Response: OK STREAM_ID: 0x5679¶
An ERROR message is sent in response to a failed request. It carries the same Stream ID as the request.¶
| Field | Content |
|---|---|
| STREAM_ID | Mirrors the request Stream ID. |
| PARAMETERS | (OPTIONAL) Status code (varint). |
| PAYLOAD | (OPTIONAL) Error details (PSON-encoded). |
The PARAMETERS field in OK and ERROR messages MAY carry a numeric status code. Implementations SHOULD use HTTP status codes as defined in [RFC9110].¶
Rationale: IOTMP reuses HTTP status codes rather than defining a separate code space for two reasons. First, IOTMP's resource-oriented model (named resources with read/write/invoke operations) maps naturally to HTTP semantics, making HTTP status codes directly applicable: a missing resource is 404, a malformed request is 400, a timeout is 408. Second, IOTMP is designed to support transparent HTTP bridging, where an HTTP gateway translates between HTTP requests and IOTMP messages. Reusing HTTP status codes enables this bridge to pass status codes through without a translation table, preserving the original error semantics end-to-end.¶
If the PARAMETERS field is absent or zero, a generic success (for OK) or generic error (for ERROR) is assumed.¶
The following status codes are commonly used:¶
| Code | Name | Usage |
|---|---|---|
| 200 | OK | Successful operation (implicit if omitted in OK). |
| 301 | Moved Permanently | Server redirect: client must update stored address Section 9.6. |
| 307 | Temporary Redirect | Server redirect: client should connect to indicated server Section 9.6. |
| 400 | Bad Request | Malformed request or invalid parameters. |
| 401 | Unauthorized | Authentication required or credentials invalid. |
| 403 | Forbidden | Insufficient permissions for the requested action. |
| 404 | Not Found | Resource does not exist. |
| 408 | Request Timeout | Operation timed out. |
| 409 | Conflict | Resource state conflict (e.g., already exists). |
| 413 | Content Too Large | Payload exceeds allowed size. |
| 429 | Too Many Requests | Rate limit exceeded. |
| 500 | Internal Server Error | Unexpected error during processing. |
Implementations MAY use other HTTP status codes as defined in [RFC9110]. Receivers that encounter an unrecognized status code SHOULD treat it according to the class of the code (2xx = success, 3xx = redirect, 4xx = client error, 5xx = server error).¶
When the PAYLOAD field is present in an ERROR message, it SHOULD be a PSON map containing an "error" key with a human-readable error description:¶
ERROR Message:
STREAM_ID: 0x1234
PARAMETERS: 404
PAYLOAD: {"error": "Resource 'sensor' does not exist"}
¶
Implementations MAY include additional keys in the error payload for application-specific diagnostics.¶
An OK message MAY also carry a status code in the PARAMETERS field to provide more specific success information. If omitted, a generic 200 (OK) is assumed.¶
This section follows the guidelines of [RFC3552] for security considerations.¶
IOTMP is designed for client-server communication between IoT devices and infrastructure servers (brokers). The following threat model identifies the primary attack surfaces and the protocol's defenses.¶
Actors:¶
Threat Analysis:¶
| Threat | Attack Vector | Impact | Mitigation |
|---|---|---|---|
| Eavesdropping | Passive observation of unencrypted traffic | Credential theft, data leakage | TLS encryption Section 14.2 |
| Credential Replay | Attacker captures and replays a valid CONNECT message | Unauthorized access, device impersonation | TLS prevents replay; servers SHOULD reject duplicate CONNECT on same connection |
| Man-in-the-Middle | Attacker intercepts and modifies messages in transit | Data manipulation, command injection | TLS with server certificate validation; mTLS for mutual authentication |
| Device Impersonation | Attacker uses stolen or guessed credentials | Unauthorized data injection, control of resources | Strong unique credentials per device; rate limiting on authentication; credential rotation Section 14.3 |
| Message Injection | Attacker injects crafted IOTMP messages into a connection | Unauthorized resource invocation, data corruption | TLS integrity protection; servers MUST validate Stream ID correlation |
| Message Tampering | Modification of messages in transit | Altered sensor data, modified commands | TLS message integrity |
| Denial of Service (Connection Flood) | Attacker opens many TCP connections without authenticating | Server resource exhaustion | CONNECT handshake timeout; per-IP connection limits Section 14.5 |
| Denial of Service (Message Flood) | Compromised device sends excessive messages | Server CPU/memory exhaustion | Rate limiting; maximum message size enforcement Section 14.5 |
| Stream Exhaustion | Client opens maximum number of streams without closing them | Server memory exhaustion | Per-device stream limits; idle stream timeout Section 14.6 |
| Malformed Payload | Crafted PSON data with extreme nesting, oversized lengths | Stack overflow, memory exhaustion on constrained devices | PSON validation; nesting depth limits; length bounds checking Section 14.6 |
| Resource Enumeration | Attacker uses DESCRIBE to discover all device capabilities | Information disclosure; aids targeted attacks | Authorization on DESCRIBE; TLS to prevent passive discovery Section 14.8 |
| Unauthorized Resource Access | Authenticated device invokes resources beyond its scope | Privilege escalation, unauthorized control | Per-resource authorization; least privilege principle Section 14.4 |
IOTMP itself does not provide encryption or integrity protection at the application layer. Implementations MUST rely on TLS (version 1.2 or later, as specified in [RFC8446]) for:¶
Unencrypted connections (port 25204) SHOULD only be used in isolated, trusted networks or during development. Production deployments MUST use TLS.¶
Implementations SHOULD support TLS 1.3 [RFC8446] for improved handshake performance (1-RTT vs 2-RTT) and stronger cipher suites. TLS 1.2 MAY be supported for compatibility with constrained devices that lack TLS 1.3 implementations.¶
The CONNECT message transmits authentication data in the PAYLOAD field. The format of this data depends on the authentication type ("at" parameter, Section 9.2). For type 0 (Credentials), the payload contains namespace, device ID, and credential in cleartext. For type 1 (Token), the payload contains a bearer token. Without TLS, this data is vulnerable to eavesdropping.¶
Implementations MUST use TLS when transmitting over untrusted networks.¶
Implementations MUST:¶
Implementations SHOULD:¶
The "at" parameter is extensible. Additional authentication types MAY be defined in separate specifications.¶
When using authentication type 2 (Certificate), deployments may use either per-device certificates or fleet certificates (a single certificate shared across multiple devices within a namespace). Each approach has different security properties:¶
Per-device certificates: The certificate's subject (CN or SAN) identifies both the namespace and the device. The server extracts the full device identity from the certificate. If a PAYLOAD is present, the server MUST verify that the declared namespace and device_id match the certificate identity and MUST reject the connection with ERROR (401) on mismatch. This is the RECOMMENDED approach for production deployments.¶
Fleet certificates: The certificate identifies only the namespace (e.g., a shared CA per organizational scope). The device MUST send a PAYLOAD with [namespace, device_id] so the server can identify the specific device. In this model, any device holding the fleet certificate can claim any device_id within the namespace. Deployments using fleet certificates SHOULD be aware that compromise of a single device's certificate allows impersonation of any other device in the same namespace. To mitigate this risk, operators SHOULD:¶
Connection-level mitigations:¶
Message-level mitigations:¶
Devices operating in physically accessible environments face the risk of credential extraction through hardware attacks (JTAG, flash dumping, side-channel analysis). Implementations SHOULD:¶
A conformant IOTMP client implementation MUST:¶
A conformant client SHOULD:¶
A conformant IOTMP server implementation MUST:¶
A conformant server SHOULD:¶
The following rules govern connection state transitions and edge cases:¶
Stream Recovery on Reconnect:¶
When a transport connection is lost, all active streams are terminated. Stream state (Stream IDs, compact mode schemas) is NOT preserved across connections. After reconnection:¶
In-Flight Message Handling:¶
Idempotency Guidance:¶
IOTMP's request-response model (RUN -> OK/ERROR) provides implicit delivery confirmation for the common case: if the client receives OK, the operation succeeded; if it receives ERROR, it failed. The only ambiguous scenario occurs when the connection drops after the server executes the operation but before the client receives the response.¶
IOTMP does not define protocol-level delivery guarantees (such as QoS levels) because its request-response model already covers the vast majority of cases. For the rare connection-loss scenario, the recommended approach is to design resource operations as absolute state transitions rather than relative or toggling operations:¶
| Pattern | Example | Safe to retry? |
|---|---|---|
| Absolute state (RECOMMENDED) |
{"relay": true}
|
Yes -- setting the same state twice has no additional effect. |
| Relative/toggle (AVOID) |
{"action": "toggle"}
|
No -- retrying may reverse the intended state. |
Resources that follow this pattern are inherently idempotent: re-executing the same RUN after reconnection produces the same result whether the original request was executed or not. This eliminates the need for protocol-level deduplication mechanisms.¶
Additionally, if the client needs to verify the outcome of an ambiguous request after reconnection, it can query the current resource state by sending a RUN (for output resources) or DESCRIBE to the resource. This allows the client to confirm whether the previous operation took effect before deciding whether to retry.¶
For the rare cases where non-idempotent operations are unavoidable, application profiles MAY implement their own deduplication (e.g., including a unique request identifier in the PAYLOAD that the receiver checks against recently processed requests).¶
Invalid State Handling:¶
| Condition | Required Behavior |
|---|---|
| Client sends a message before CONNECT | Server MUST close the connection. |
| Client sends CONNECT after authentication | Server MUST respond with ERROR (400) and close the connection. |
| Message received with unknown message type | Receiver SHOULD ignore the message. |
| Message received with unknown field number | Receiver MUST ignore the unknown field and process known fields. |
| STREAM_DATA received for unknown Stream ID | Receiver SHOULD ignore the message. |
| RUN/DESCRIBE received for non-existent resource | Receiver MUST respond with ERROR (404). |
| Message exceeds negotiated maximum size | Receiver MUST close the connection. |
| Varint does not terminate within 4 bytes | Receiver MUST close the connection. |
| Keepalive timeout exceeded | Server MUST close the connection. |
| Request with Stream ID from wrong partition Section 6.2 | Receiver MUST respond with ERROR (400). |
| Request with a Stream ID that is already active | Receiver MUST respond with ERROR (409). |
The following test vectors allow implementations to verify correct encoding and decoding. Each vector specifies the input, the expected wire encoding (hex), and the total byte count.¶
Input: KEEP_ALIVE message (empty body).¶
Expected encoding: 05 00 Total: 2 bytes¶
Input: CONNECT (auth type 0) with credentials ["acme1", "device1", "secret123"] (namespace, device_id, credential), Stream ID = 42 (client-initiated, even).¶
Expected encoding: 03 1C 08 2A 1A E3 85 61 63 6D 65 31 87 64 65 76 69 63 65 31 89 73 65 63 72 65 74 31 32 33 Total: 30 bytes¶
Decoding verification:¶
03: Message type CONNECT.¶
1C: Body size 28.¶
08: Field tag (STREAM_ID, varint). 2A: Stream ID = 42.¶
1A: Field tag (PAYLOAD, pson).¶
E3: PSON array, 3 elements.¶
85 61 63 6D 65 31: PSON string "acme1" (namespace, 5 bytes).¶
87 64 65 76 69 63 65 31: PSON string "device1" (device_id, 7 bytes).¶
89 73 65 63 72 65 74 31 32 33: PSON string "secret123" (credential, 9 bytes).¶
Input: OK response to Stream ID 42, no status code, no payload.¶
Expected encoding: 01 02 08 2A Total: 4 bytes¶
Input: RUN resource "led" with payload {"on": true}, Stream ID = 100 (client-initiated, even).¶
Expected encoding: 06 0D 08 64 22 83 6C 65 64 1A C1 82 6F 6E 61 Total: 15 bytes¶
Decoding verification:¶
Input: RUN resource hash 0x1A2B (FNV-1a hash of a resource name), no payload, Stream ID = 7 (server-initiated, odd).¶
Expected encoding: 06 05 08 07 20 AB 34 Total: 7 bytes¶
Decoding verification:¶
Input: ERROR for Stream ID 42, status 404, payload {"error": "Not found"}.¶
Expected encoding: 02 17 08 2A 10 94 03 1A C1 85 65 72 72 6F 72 89 4E 6F 74 20 66 6F 75 6E 64 Total: 25 bytes¶
Decoding verification:¶
Input: START_STREAM resource "temperature", interval 5000ms, compact mode, Stream ID = 161 (server-initiated, odd).¶
Expected encoding: 08 1B 08 A1 01 12 C2 81 69 1F 88 27 82 63 6D 61 22 8B 74 65 6D 70 65 72 61 74 75 72 65 Total: 29 bytes¶
Decoding verification:¶
This specification requests the assignment of the following TCP port numbers from IANA's Service Name and Transport Protocol Port Number Registry:¶
| Service Name | Port | Transport | Description | Reference |
|---|---|---|---|---|
| iotmp | 25204 | TCP | IOTMP over TCP (unencrypted) | This document |
| iotmps | 25206 | TCP | IOTMP over TLS | This document |
Registration details:¶
This specification requests registration of the following entry in the IANA WebSocket Subprotocol Name Registry, as defined in [RFC6455], Section 11.5:¶
When operating over WebSocket, the client MUST include "iotmp" in the Sec-WebSocket-Protocol header during the handshake. The server MUST confirm the subprotocol in the response. All WebSocket messages MUST use binary opcode (0x02) and each frame MUST contain exactly one complete IOTMP message Section 4.3.¶
IANA is requested to create a new registry entitled "IOTMP Message Types" in a new "Internet of Things Message Protocol (IOTMP)" registry group. The registry contains the following columns: Value, Name, and Reference.¶
New registrations in the range 0x0B-0xFF require Standards Action [RFC8126].¶
Initial values:¶
| Value | Name | Reference |
|---|---|---|
| 0x00 | RESERVED | This document |
| 0x01 | OK | This document |
| 0x02 | ERROR | This document |
| 0x03 | CONNECT | This document |
| 0x04 | DISCONNECT | This document |
| 0x05 | KEEP_ALIVE | This document |
| 0x06 | RUN | This document |
| 0x07 | DESCRIBE | This document |
| 0x08 | START_STREAM | This document |
| 0x09 | STOP_STREAM | This document |
| 0x0A | STREAM_DATA | This document |
IANA is requested to create a new registry entitled "IOTMP Authentication Types" in the "Internet of Things Message Protocol (IOTMP)" registry group. The registry contains the following columns: Code, Name, PAYLOAD Format, and Reference.¶
New registrations in the range 3-255 require Specification Required [RFC8126].¶
Initial values:¶
| Code | Name | PAYLOAD Format | Reference |
|---|---|---|---|
| 0 | Credentials | PSON array: [namespace, device_id, credential] | This document |
| 1 | Token | PSON string (bearer token) | This document |
| 2 | Certificate | Optional PSON array: [namespace, device_id] or absent | This document |
IANA is requested to create a new registry entitled "IOTMP Field Numbers" in the "Internet of Things Message Protocol (IOTMP)" registry group. The registry contains the following columns: Number, Name, Allowed Wire Types, and Reference.¶
New registrations in the range 0x05-0x07 require Standards Action [RFC8126].¶
Initial values:¶
| Number | Name | Allowed Wire Types | Reference |
|---|---|---|---|
| 0x00 | RESERVED | -- | This document |
| 0x01 | STREAM_ID | varint | This document |
| 0x02 | PARAMETERS | varint, pson | This document |
| 0x03 | PAYLOAD | pson, bytes | This document |
| 0x04 | RESOURCE | varint, pson | This document |
IANA is requested to create a new registry entitled "IOTMP Wire Types" in the "Internet of Things Message Protocol (IOTMP)" registry group. The registry contains the following columns: Value, Name, Description, and Reference.¶
New registrations in the range 0x03-0x07 require Standards Action [RFC8126].¶
Initial values:¶
| Value | Name | Description | Reference |
|---|---|---|---|
| 0x00 | varint | Variable-length unsigned integer | This document |
| 0x01 | bytes | Length-prefixed raw bytes | This document |
| 0x02 | pson | PSON-encoded value | This document |
05 00 | +- Body Size: 0 (varint) +---- Message Type: 0x05 = KEEP_ALIVE (varint)¶
Total: 2 bytes.¶
For auth type 0 credentials ["acme1", "device1", "secret123"] (namespace, device_id, credential) with Stream ID 42:¶
03 # Message Type: CONNECT (varint)
1C # Body Size: 28 (varint)
08 # STREAM_ID (field=1, wire=varint)
2A # Stream ID: 42 (varint)
1A # PAYLOAD (field=3, wire=pson)
E3 # PSON: array of 3 elements
85 61 63 6D 65 31 # string "acme1" (namespace, len=5)
87 64 65 76 69 63 65 31 # string "device1" (device_id, len=7)
# PSON: string "secret123"
89 73 65 63 72 65 74 31 32 33 # (credential, len=9)
¶
Total: 30 bytes.¶
Peer asks for resource "temperature":¶
06 # Message Type: RUN (varint)
0F # Body Size: 15 (varint)
08 # STREAM_ID (field=1, wire=varint)
2A # Stream ID: 42
22 # RESOURCE (field=4, wire=pson)
# PSON: string "temperature" (len=11)
8B 74 65 6D 70 65 72 61 74 75 72 65
¶
Total: 17 bytes.¶
Response with {"temperature": 25.3}:¶
01 # Message Type: OK (varint)
15 # Body Size: 21 (varint)
08 # STREAM_ID (field=1, wire=varint)
2A # Stream ID: 42
1A # PAYLOAD (field=3, wire=pson)
C1 # PSON: map with 1 entry
# key: "temperature" (len=11)
8B 74 65 6D 70 65 72 61 74 75 72 65
# value: float 25.3 (IEEE 754, LE)
40 66 66 CA 41
¶
Total: 23 bytes.¶
Response with 404 status and error message:¶
02 # Message Type: ERROR (varint)
20 # Body Size: 32 (varint)
08 # STREAM_ID (field=1, wire=varint)
2A # Stream ID: 42
10 # PARAMETERS (field=2, wire=varint)
94 03 # Status code: 404 (varint)
1A # PAYLOAD (field=3, wire=pson)
C1 # PSON: map with 1 entry
85 65 72 72 6F 72 # key: "error" (len=5)
# value: "Resource not found" (len=18)
92 52 65 73 6F 75 72 63 65 20 6E 6F 74 20
66 6F 75 6E 64
¶
Total: 34 bytes.¶
This appendix provides a formal definition of IOTMP messages using CDDL (Concise Data Definition Language, [RFC8610]). This grammar is normative and defines the logical structure of all IOTMP messages -- the required fields, their types, and their constraints.¶
Note on scope: CDDL is designed for CBOR-based protocols, but is used here as a structural description language. The CDDL definitions describe the semantic structure of each message (which fields exist, their types, and cardinality), NOT the wire encoding. The actual wire format uses IOTMP's own framing Section 5, field tag encoding Section 7, and PSON data encoding Section 8, which differ from CBOR. Implementations MUST follow the wire format defined in Section 5, Section 7, and Section 8; the CDDL grammar serves as a complementary formal reference for message structure validation.¶
+-------------------------------------------------------------+ | IOTMP Message Frame | +--------------+--------------+--------------------------------+ | Message Type | Body Size | Body | | (varint) | (varint) | (Body Size bytes) | | 1-2 bytes | 1-4 bytes | 0-N bytes | +--------------+--------------+--------------------------------+¶
+---------------------------------------+ | Field Tag (1 byte) | +------------------+--------------------+ | Field Number | Wire Type | | (bits 7-3) | (bits 2-0) | | 5 bits (0-31) | 3 bits (0-7) | +------------------+--------------------+ Wire Type values: 0x00 = varint (variable-length unsigned integer) 0x01 = bytes (length-prefixed raw bytes) 0x02 = pson (PSON-encoded value) Defined Field Tags: 0x08 = STREAM_ID (field 1, wire type varint) 0x10 = PARAMETERS (field 2, wire type varint) 0x12 = PARAMETERS (field 2, wire type pson) 0x1A = PAYLOAD (field 3, wire type pson) 0x19 = PAYLOAD (field 3, wire type bytes) 0x20 = RESOURCE (field 4, wire type varint -- resource hash) 0x22 = RESOURCE (field 4, wire type pson -- resource name or hash)¶
Single-byte varint (values 0-127): +---+-----------+ | 0 | value | | | (7 bits) | +---+-----------+ Multi-byte varint (values >= 128): +---+-----------+ +---+-----------+ +---+-----------+ | 1 | bits 0-6 | | 1 | bits 7-13 | ... | 0 | bits N-N+6| +---+-----------+ +---+-----------+ +---+-----------+ MSB = 1: more MSB = 1: more MSB = 0: last bytes follow bytes follow byte¶
; ===========================================================
; IOTMP Message Grammar -- CDDL (RFC 8610)
; ===========================================================
; --- Top-level frame ---
iotmp-frame = (
message-type: message-type-id,
body-size: uint,
body: bstr .size body-size,
)
message-type-id = &(
RESERVED: 0x00,
OK: 0x01,
ERROR: 0x02,
CONNECT: 0x03,
DISCONNECT: 0x04,
KEEP_ALIVE: 0x05,
RUN: 0x06,
DESCRIBE: 0x07,
START_STREAM: 0x08,
STOP_STREAM: 0x09,
STREAM_DATA: 0x0A,
)
; --- Field definitions ---
stream-id = uint .size 2 ; 0-65535 (even=client, odd=server)
status-code = uint ; HTTP status code (RFC 9110)
resource-id = tstr / uint .size 2
; string name or 16-bit FNV-1a hash
; --- Per-message field requirements ---
; CONNECT (0x03): Client -> Server
connect-body = {
stream_id: stream-id,
? payload: connect-payload,
? parameters: {
? "v": uint .default 1, ; protocol version
? "ka": uint .le 1800 .default 60, ; keepalive (seconds)
? "at": auth-type .default 0, ; authentication type
? "ms": uint .ge 1024 .default 32768, ; max message size (bytes)
},
}
auth-type = &(
credentials: 0, ; PAYLOAD = [ns, device_id, cred]
token: 1, ; PAYLOAD = bearer token string
certificate: 2, ; PAYLOAD optional: [ns, device_id]
; or absent
)
connect-payload = credentials-payload
/ token-payload
/ certificate-payload
credentials-payload = [
namespace: tstr, device_id: tstr, credential: tstr
]
token-payload = tstr ; bearer token (JWT, API key, etc.)
certificate-payload = [namespace: tstr, device_id: tstr]
/ nil / empty
; OK (0x01): Response to any request
ok-body = {
stream_id: stream-id,
? parameters: status-code / pson-value,
; status code or structured data
? payload: pson-value,
}
; ERROR (0x02): Error response to any request
error-body = {
stream_id: stream-id,
? parameters: status-code,
? payload: { "error": tstr, * tstr => any },
; error message + optional diagnostics
}
; DISCONNECT (0x04)
disconnect-body = {
? parameters: status-code, ; redirect code (301, 307)
? payload: pson-value, ; redirect target or reason
}
; KEEP_ALIVE (0x05)
keep-alive-body = empty ; body MUST be empty (size = 0)
; RUN (0x06): Execute a resource
run-body = {
stream_id: stream-id,
resource: resource-id,
? payload: pson-value, ; input data for the resource
}
; DESCRIBE (0x07): Request resource metadata
describe-body = {
stream_id: stream-id,
? resource: resource-id,
; absent = full API, present = single resource
}
; START_STREAM (0x08): Begin streaming
start-stream-body = {
stream_id: stream-id,
resource: resource-id,
? parameters: uint / { ; interval or structured params
? "i": uint, ; interval in ms (0 = event-driven)
? "cm": bool .default false, ; compact mode request
},
}
; STOP_STREAM (0x09): End streaming
stop-stream-body = {
stream_id: stream-id,
}
; STREAM_DATA (0x0A): Stream payload
stream-data-body = {
stream_id: stream-id,
payload: pson-value, ; resource data (map or compact arr)
}
; --- DESCRIBE response structures ---
; Full API DESCRIBE response (no RESOURCE in request)
full-api-describe-response = {
"v": uint, ; description format version
"res": { ; resource map (not protocol fields)
* resource-name => {
"fn": io-type-code, ; I/O type code
? "description": tstr, ; human-readable description
},
},
}
resource-name = tstr
io-type-code = &(
none: 0,
run: 1,
input: 2,
output: 3,
input_output: 4,
)
; Single Resource DESCRIBE response (RESOURCE present in request)
single-resource-describe-response = {
"v": uint,
? "in": io-descriptor,
? "out": io-descriptor,
}
io-descriptor = {
"value": pson-value, ; current/sample data
? "schema": json-schema, ; JSON Schema definition (optional)
}
json-schema = { ; subset of JSON Schema keywords
? "type": schema-type,
? "properties": { * tstr => json-schema },
? "items": json-schema,
? "description": tstr,
? "minimum": number,
? "maximum": number,
? "enum": [+ any],
? "readOnly": bool,
? "writeOnly": bool,
? "default": any,
? "required": [+ tstr],
}
schema-type = "boolean" / "integer" / "number"
/ "string" / "object" / "array"
; --- Generic types ---
pson-value = any ; any PSON-encoded value (see PSON)
empty = nil ; zero-length body
¶
The following table summarizes which fields are required (R), optional (O), conditional (C), or not used (--) for each message type:¶
| Message Type | STREAM_ID | PARAMETERS | PAYLOAD | RESOURCE |
|---|---|---|---|---|
| OK | R | O | O | -- |
| ERROR | R | O | O | -- |
| CONNECT | R | O | C | -- |
| DISCONNECT | -- | O | O | -- |
| KEEP_ALIVE | -- | -- | -- | -- |
| RUN | R | -- | O | R |
| DESCRIBE | R | -- | -- | O |
| START_STREAM | R | O | -- | R |
| STOP_STREAM | R | -- | -- | -- |
| STREAM_DATA | R | -- | R | -- |
C = Conditional: CONNECT PAYLOAD is required for authentication types 0 (Credentials) and 1 (Token). For type 2 (Certificate), PAYLOAD is optional: required when the certificate identifies only the namespace, and may be absent when the certificate identifies both namespace and device.¶
For a detailed size comparison between PSON and JSON for typical IoT payloads, see Section 10 of [PSON]. In summary, PSON achieves 40-75% size reduction for typical IoT data patterns.¶
Detailed protocol comparisons between IOTMP and other IoT protocols [MQTT], [RFC7252], [LwM2M] are available as separate companion documents.¶
| Version | Date | Changes |
|---|---|---|
| 0.1 | 2026-03-30 | Initial public draft. |