AsyncAPI 3: Event-Driven API Specification Complete Guide
Last Updated: October 2026
Most teams that run Kafka or MQTT in production can tell you exactly how their REST APIs are documented and cannot tell you how their events are. The topic names live in a wiki page from two years ago, the payload shape lives in a producer’s source code, and the delivery guarantees live in the head of whoever configured the broker. This is the gap the AsyncAPI 3 event driven API specification exists to close, and in October 2026 it is more mature than most of the articles ranking for it suggest.
The current release is AsyncAPI 3.1.0, published on 31 January 2026 as a non-breaking minor update to 3.0.0 (December 2023). The 3.x model is genuinely different from 2.x: channels are decoupled from operations, publish and subscribe are replaced by send and receive, and request/reply is a first-class concept. Tooling has caught up, with the CLI now generator-v3-only and the JavaScript parser at 3.6.x. This guide corrects several claims that circulate about 3.0, shows a complete working IoT specification, and ends with a CI governance pipeline you can copy.
What this covers: the object model, a worked Kafka plus MQTT specification, migration from 2.x, protocol bindings, tooling status as of October 2026, governance and CI, failure modes, and a rollout checklist.
Context and Background
AsyncAPI is an open specification for describing message-driven APIs in a machine-readable form. It started as a community project in 2016 by Fran Méndez, modelled consciously on OpenAPI’s role for REST, and it is maintained today by the AsyncAPI Initiative under the Linux Foundation umbrella. The specification document describes who talks to whom (servers), over which logical destinations (channels), doing what (operations), exchanging which data (messages and schemas), and with which protocol-specific settings (bindings).
The reason this matters more every year is structural. Synchronous request/response APIs are only one slice of what a modern platform exposes. An industrial plant publishes sensor telemetry over MQTT, a bridge fans it into Kafka, a digital twin service consumes it, and a command channel flows back the other way. None of those interactions fit the OpenAPI model, which assumes a client that sends a request and waits for exactly one response. If you are building the kind of pipeline described in our guide to MQTT store-and-forward at the edge, the contract between gateway and broker is just as real as any REST contract, and just as capable of breaking.
It helps to be precise about what AsyncAPI is not. It is not a broker, a schema registry, or a serialization format. It does not enforce anything at runtime on its own. It is a description layer, and its value comes from the tools that read that description: validators, code generators, documentation portals, mock servers, and contract-testing harnesses. The specification is deliberately neutral on payload encoding. A message payload can be described with an embedded JSON Schema-like Schema Object, or, since 3.0, with a Multi Format Schema Object that pairs a schemaFormat string with the schema itself. The spec text gives application/vnd.apache.avro+json;version=1.9.0 as an Avro example, and the default for a plain Schema Object is the AsyncAPI-flavoured JSON Schema vocabulary at the document’s own version.
Two comparisons come up constantly. Against OpenAPI, AsyncAPI shares the philosophy (a spec-first, machine-readable contract) but not the structure. The 3.0 release notes explicitly removed the old editorial note about striving for OpenAPI compatibility; the spec still aims at interoperability with schema languages such as Avro, RAML and OpenAPI Schema, but it does not try to mirror OpenAPI’s document layout. Against schema-registry-centric approaches such as Confluent Schema Registry, AsyncAPI sits one level up: the registry stores and versions payload schemas for a subject, while AsyncAPI describes the topology around those schemas. Our piece on data contracts and schema registries for streaming lakehouses covers the registry half in depth, and the two are complements rather than substitutes.
For reference, the primary source is the specification itself at asyncapi.com/docs/reference/specification. Everything version-specific in this article was checked against the spec repository releases, the 3.0.0 and 3.1.0 release notes, and the bindings repository in October 2026.
The AsyncAPI 3 Object Model: Reference Architecture
An AsyncAPI 3 document is a single YAML or JSON file with eight top-level fields: asyncapi (the version string, required), id, info (required), servers, defaultContentType, channels, operations, and components. Channels describe addressable destinations and the messages that may appear on them. Operations describe what your application does with those channels, using action: send or action: receive. Components hold reusable parts.

Figure 1: The AsyncAPI 3 object model. Operations reference channels, channels declare messages, messages reference schemas, and bindings attach protocol detail at server, channel, operation and message level.
The diagram shows the single most important structural fact about 3.x: operations point at channels, not the other way round. In 2.x a channel owned its publish and subscribe operations, so a channel definition could not be reused by two services that played different roles on it. In 3.x a channel is a neutral description of a destination and its allowed messages, and each service writes its own operations against it.
Servers: where the broker lives
The Server Object has the required fields host and protocol, plus protocolVersion, pathname, title, summary, description, variables, security, tags, externalDocs and bindings. The 3.0 release split the old single url into host, pathname and protocol, which matters because a Kafka bootstrap string, an MQTT broker address and a WebSocket URL have very different shapes. host may include a port.
Variables let one server entry describe a family of environments. A broker host such as mqtt.{region}.example.com with a region variable constrained by an enum is cleaner than three nearly identical server entries. Security is expressed as an array of references to security schemes held in components, a change from 2.x where implicit name-based references were allowed.
Channels: addresses and messages, nothing else
A Channel Object carries an address (the real topic, subject, queue or routing key), a messages map, optional parameters for templated addresses, and metadata such as title, summary and description, plus bindings. The channel key is now an arbitrary identifier. That sounds cosmetic, but it solves a real 2.x problem: an MQTT topic like plants/{plantId}/lines/{lineId}/telemetry used to be the key itself, which made references clumsy and meant any topic rename was a breaking change to every $ref. Now the key lineTelemetry stays stable while the address evolves.
Parameters in 3.x are deliberately narrow. The Parameter Object accepts only enum, default, description, examples and location, and parameters are strings by default. If you were relying on full JSON Schema for a topic parameter, this is one of the migration surprises. A parameter’s location can point at a runtime expression, such as a message header, when the value is not derived from the address itself.
Operations: the role your service plays
An Operation Object has action and channel as its core, and may add title, summary, description, security, tags, externalDocs, bindings, traits, messages and reply. The action describes the application’s own behaviour. A service that publishes telemetry has action: send; a service that subscribes has action: receive. This is a perspective flip from 2.x, where publish meant “other parties publish to this channel”, which is the opposite of how most engineers read the word. In our experience the 2.x wording caused more mis-documented specs than any other single feature.
The optional messages list on an operation lets a service declare it handles only a subset of what the channel permits. A command channel might allow StartBatch, StopBatch and Reset, while a simple actuator controller only receives the first two. That subset is part of the contract, and generators use it to emit only the handlers needed.
Messages and schemas
A Message Object has headers, payload, correlationId, contentType, name, title, summary, description, tags, externalDocs, bindings, examples and traits. The headers schema describes application headers only; protocol-level headers belong in message bindings. The correlationId defines where a tracing or matching identifier lives, which becomes essential for request/reply. The payload may be a Schema Object, a Multi Format Schema Object or a reference.
Examples deserve more attention than most specs give them. A message’s examples list carries named example payloads and headers, and mock servers and documentation portals use them directly. A spec without examples is accurate but unusable; a spec with three realistic examples per message doubles as test data.
Traits and reuse
Operation traits and message traits let you factor out repeated blocks, such as the same correlation header on every message or the same retry tags on every operation. The 3.0 release changed the merge rule in a way that bites during migration: a trait property no longer overrides the same property on the target object. If a message sets its own description and a trait also sets one, the message’s value wins. In 2.x documents where traits were used as overrides, rendering will change after conversion.
What Changed From 2.x, and What the Popular Summaries Get Wrong
Two claims appear on many pages about AsyncAPI 3.0 and they do not survive contact with the primary sources. The first is the release date: 3.0.0 was released on 5 December 2023, not in March 2024. The spec repository also tags a 3.0.1 release on 13 December 2023, but GitHub marks it as invalid, and the page notes it changed only a library dependency; treat 3.0.0 as the baseline. The second claim is that 3.0 “mirrors OpenAPI 3.1 structure”. It does not, and the release notes record the removal of the OpenAPI-compatibility note. The shared philosophy is real; the document layout is distinct.

Figure 2: Structural shift from 2.x to 3.x. Channels become reusable destinations, operations become top-level and role-explicit, and the converter bridges the two.
The full list of breaking changes
The 3.0.0 release notes and the migration guide together give this set of changes. I have grouped them by the kind of damage they do when you upgrade.
| Change | 2.x behaviour | 3.x behaviour | Migration impact |
|---|---|---|---|
| Operations | publish / subscribe nested in channel |
Top-level operations with action: send or receive |
Semantic: must be mapped by role, not by keyword |
| Channel identity | Key is the path or topic | Key is an arbitrary ID, path moves to address |
Every $ref to a channel key must be rewritten |
| Messages | Single message, oneOf for several |
messages map keyed by message ID |
messageId removed, key becomes the ID |
| Server address | Single url |
host, pathname, protocol |
Mechanical |
| Root metadata | Root tags, externalDocs |
Moved into info |
Mechanical |
| References | Implicit name-based links allowed | Explicit $ref only |
Security and server links must be rewritten |
| Security scopes | Object | Array | Scopes move into the security scheme |
| Schema format | schemaFormat on message |
Multi Format Schema Object inside payload |
Wrap payloads that declared a format |
| Parameters | Full schema | enum, default, description, examples, location |
Constraints beyond those are lost |
| Trait merging | Trait could override | Target object wins | Rendered output can change silently |
| Channels | Required | Optional | A document may contain only components |
What is genuinely new
Three additions deserve more than a table row. First, request/reply. An operation may carry a reply object with address, channel and messages. The address can be a runtime expression such as $message.header#/replyTo, which covers the common MQTT 5 and AMQP pattern where the requester names its own reply topic. The release notes list several patterns this models: a fixed response topic with a correlation id, a per-process inbox, a temporary reply topic, and WebSocket.
Second, components grew. Reusable replies, reply addresses, tags, external docs, operations and channels can now live in components, which makes it practical to publish a shared “platform events” library and reference it from many service specs.
Third, common metadata was added: title and summary on servers and channels, title on operations. This is small, but documentation portals lean on it heavily.
What 3.1.0 added
The 3.1.0 release on 31 January 2026 is intentionally small. Its release notes describe it as a minor release without breaking changes, and its headline feature is a ROS 2 protocol binding, which matters for robotics fleets. Upgrading means changing asyncapi: '3.0.0' to asyncapi: '3.1.0'. Supporting tooling moved in step: the JavaScript parser added 3.1 support in 3.6.0 (30 January 2026), and the converter package was updated so converting to 3.1.0 changes only the version field. I could not find any 3.2 or later release on the spec repository’s release page as of late October 2026, so 3.1.0 is the version to target.
A Complete IoT Specification: MQTT Ingest, Kafka Fan-Out, Command Reply
The existing guides on this topic mostly use an e-commerce order example. For an IoT and digital-twin audience a better test of the format is a cold-chain scenario: gateways publish temperature telemetry over MQTT, a bridge forwards it to Kafka, and an operator can send a setpoint command to a gateway and expect a reply. Here is a specification for the MQTT side, written against 3.1.0.
asyncapi: 3.1.0
info:
title: Cold Chain Gateway API
version: 1.4.0
description: Telemetry and command contract for cold-chain edge gateways.
tags:
- name: iot
- name: cold-chain
defaultContentType: application/json
servers:
plantBroker:
host: "mqtt.{region}.example.com:8883"
protocol: mqtt
protocolVersion: "5.0"
description: Regional MQTT 5 broker, TLS required.
variables:
region:
default: eu
enum: [eu, us, ap]
security:
- $ref: '#/components/securitySchemes/clientCert'
bindings:
mqtt:
clientId: gateway-bridge
cleanSession: false
keepAlive: 60
sessionExpiryInterval: 3600
bindingVersion: 0.2.0
channels:
telemetry:
address: "coldchain/{siteId}/{gatewayId}/telemetry"
title: Gateway telemetry
parameters:
siteId: {}
gatewayId: {}
messages:
temperatureReading:
$ref: '#/components/messages/TemperatureReading'
setpointCommand:
address: "coldchain/{siteId}/{gatewayId}/cmd/setpoint"
parameters:
siteId: {}
gatewayId: {}
messages:
setSetpoint:
$ref: '#/components/messages/SetSetpoint'
setpointReply:
address: "coldchain/{siteId}/{gatewayId}/cmd/reply"
parameters:
siteId: {}
gatewayId: {}
messages:
setpointResult:
$ref: '#/components/messages/SetpointResult'
operations:
publishTelemetry:
action: send
channel:
$ref: '#/channels/telemetry'
summary: Gateway publishes a reading every 10 seconds.
bindings:
mqtt:
qos: 1
retain: false
messageExpiryInterval: 300
bindingVersion: 0.2.0
handleSetpoint:
action: receive
channel:
$ref: '#/channels/setpointCommand'
messages:
- $ref: '#/channels/setpointCommand/messages/setSetpoint'
reply:
channel:
$ref: '#/channels/setpointReply'
address:
location: "$message.header#/replyTo"
bindings:
mqtt:
qos: 1
components:
securitySchemes:
clientCert:
type: X509
description: Mutual TLS with a per-device certificate.
messages:
TemperatureReading:
name: TemperatureReading
contentType: application/json
headers:
type: object
properties:
traceId: {type: string}
payload:
type: object
required: [deviceTs, celsius, sensorId]
properties:
deviceTs: {type: string, format: date-time}
sensorId: {type: string}
celsius: {type: number, minimum: -80, maximum: 60}
doorOpen: {type: boolean}
examples:
- name: normal
payload:
deviceTs: "2026-10-29T08:15:00Z"
sensorId: probe-3
celsius: 4.2
doorOpen: false
SetSetpoint:
name: SetSetpoint
correlationId:
location: "$message.header#/correlationId"
headers:
type: object
required: [replyTo, correlationId]
properties:
replyTo: {type: string}
correlationId: {type: string}
payload:
type: object
required: [celsius]
properties:
celsius: {type: number, minimum: -30, maximum: 15}
SetpointResult:
name: SetpointResult
correlationId:
location: "$message.header#/correlationId"
payload:
type: object
required: [accepted]
properties:
accepted: {type: boolean}
reason: {type: string}
Several choices in this document are worth explaining, because each reflects a rule in the spec rather than taste.
The two operations are written from the gateway’s point of view. publishTelemetry is send because the gateway publishes. handleSetpoint is receive because the gateway consumes commands. A backend service would write the mirror image against the same three channels, which is exactly the reuse that 3.x enables.
The reply object carries an address whose location is a runtime expression, $message.header#/replyTo. That tells a generator that the reply destination is supplied by the requester in a message header, and the matching correlationId declarations on both messages tell it which header to copy across. This mirrors the MQTT 5 request/response pattern, and we compare MQTT 5 against Sparkplug conventions in Sparkplug B vs plain MQTT topics.
The MQTT bindings use only fields that exist in the published MQTT binding at bindingVersion 0.2.0. The server binding supports clientId, cleanSession, lastWill, keepAlive, sessionExpiryInterval and maximumPacketSize (the last two are MQTT 5 only). The operation binding supports qos, retain and messageExpiryInterval (the last MQTT 5 only). The message binding supports payloadFormatIndicator, correlationData, contentType and responseTopic, all MQTT 5 only. The binding page does not define default values for these, so a specification should state qos explicitly rather than rely on tooling to guess.
One caution about the sample above: it is a hand-written teaching example, validated structurally against the field lists in the spec and binding documents, not run through every generator. Run asyncapi validate on your own copy before you trust any snippet, including this one.
The Kafka Half: Bindings, Keys, and Schema Registry
The same specification style carries over to Kafka, but the details of the Kafka binding are where existing guides most often go wrong. They routinely place fields such as acks, compression, timeout and partitionKeyExpression in an operation binding. According to the Kafka binding document in the AsyncAPI bindings repository (latest bindingVersion 0.5.0), those fields do not exist there. Producer tuning belongs in producer configuration, not in the contract.
Here is what the binding actually defines, level by level.
| Level | Fields | What it is for |
|---|---|---|
| Server | schemaRegistryUrl, schemaRegistryVendor |
Where the registry lives; vendors named include apicurio, confluent, ibm and karapace |
| Channel | topic, partitions, replicas, topicConfiguration |
Topic name when it differs from the channel ID, plus sizing and retention |
| Operation | groupId, clientId |
Consumer group and client identity |
| Message | key, schemaIdLocation, schemaIdPayloadEncoding, schemaLookupStrategy |
Message key schema and how the registry schema id travels |
The channel-level topicConfiguration object covers cleanup.policy (an array limited to delete and compact), retention.ms, retention.bytes, delete.retention.ms and max.message.bytes, plus Confluent-specific validation and subject-naming-strategy flags. It is the only binding object that permits extra properties.

Figure 3: One specification repository drives validation at the gateway and bridge, model generation for ingest services, documentation for the twin service, and mock-based contract tests against the broker.
Partition keys are a message concern
A frequent point of confusion is how to express “all events for one device go to the same partition”. The Kafka message binding has a key field that takes a schema describing the message key. That documents what the key is (for example, a string equal to the device id), and generators can use it to produce typed key serializers. It does not itself implement partitioning. The actual guarantee, per-key ordering within a partition, is a property of Kafka’s default partitioner and your producer. If you rely on it, say so in the channel description, and test it, as ordering also depends on idempotent or transactional producer settings described in Kafka exactly-once semantics.
A compact Kafka-side fragment for the downstream topic looks like this:
servers:
streamCluster:
host: "kafka-1.example.com:9093"
protocol: kafka-secure
bindings:
kafka:
schemaRegistryUrl: https://registry.example.com
schemaRegistryVendor: confluent
bindingVersion: 0.5.0
channels:
normalizedTelemetry:
address: coldchain.telemetry.v1
messages:
reading:
$ref: '#/components/messages/NormalizedReading'
bindings:
kafka:
partitions: 24
replicas: 3
topicConfiguration:
cleanup.policy: [delete]
retention.ms: 604800000
max.message.bytes: 1048576
bindingVersion: 0.5.0
operations:
consumeReadings:
action: receive
channel:
$ref: '#/channels/normalizedTelemetry'
bindings:
kafka:
groupId:
type: string
enum: [twin-ingest]
clientId:
type: string
enum: [twin-ingest-1]
bindingVersion: 0.5.0
components:
messages:
NormalizedReading:
bindings:
kafka:
key:
type: string
description: Gateway id, used for per-device ordering.
schemaIdLocation: header
schemaIdPayloadEncoding: confluent
bindingVersion: 0.5.0
payload:
schemaFormat: application/vnd.apache.avro+json;version=1.9.0
schema:
type: record
name: NormalizedReading
namespace: coldchain
fields:
- {name: gatewayId, type: string}
- {name: celsius, type: double}
- {name: deviceTs, type: long}
Two details here are easy to miss. The groupId and clientId fields take Schema Objects, not bare strings, which is why the example uses a typed enum with one value. And the Avro payload shows the Multi Format Schema Object in practice: the schemaFormat string sits beside the schema inside payload, instead of on the message itself as in 2.x.
Where the registry fits
The schema registry remains the runtime source of truth for compatibility checks, and AsyncAPI should reference it rather than duplicate it. The healthy pattern is that the AsyncAPI document describes topology and points to registry subjects, while the registry enforces BACKWARD or FULL compatibility on the payload schemas themselves. If you maintain Avro schemas in both the AsyncAPI file and the registry, you have created two sources of truth, and they will drift. Either reference the schema from a shared file that both consume, or generate one from the other in CI. The transactional guarantee that the event was actually emitted in the first place is a separate concern, covered in the transactional outbox pattern with CDC.
Tooling Status in October 2026
A specification is only as useful as the tools that consume it. The picture in late October 2026, verified against each project’s release page, is as follows. Version numbers move quickly, so treat these as a snapshot and confirm before pinning.
| Tool | Role | Status observed |
|---|---|---|
@asyncapi/parser (JS) |
Parse and validate documents | 3.6.x; 3.6.0 (30 Jan 2026) added 3.1 support, 3.6.6 is latest on 16 Oct |
| AsyncAPI CLI | Validate, convert, generate, format | v5 dropped generator v1 and v2, v6.0.0 (Feb) dropped Postman conversion; 6.x is current |
@asyncapi/generator |
Template-driven code and doc generation | 3.4.0 current; WebSocket templates for Dart, JavaScript and Python emit runnable examples |
| Modelina | Typed model generation from schemas | 6.0.0 in pre-release (next.18, 1 Sep 2026) |
| AsyncAPI Studio | Browser editor with live preview | Supports writing 3.1.0 documents |
Some of these entries deserve interpretation. The CLI change in version 5 means that the old --use-new-generator flag is gone and the CLI requires Node.js 24 or newer with npm 11 or newer. If your CI image still ships Node 18 or 20, the CLI will not install, and that is the most likely first failure when you upgrade a pipeline. The generator moved to a template-and-components architecture; the practical consequence is that older community templates written for generator v1 or v2 will not run, and you should check each template you depend on.
Modelina is the one to watch carefully: its 6.0.0 line was still in pre-release at the time of writing, so pin an exact version and accept that output formatting can change between pre-release builds. I could confirm from release notes that the project works on Go, C#, Kotlin, Python, Java and TypeScript output, but I could not confirm a complete supported-language list from the page, so check the Modelina documentation for your language.
Beyond the official tools there is a community ecosystem: mock-and-contract-testing platforms such as Microcks, documentation catalogs such as EventCatalog, and framework integrations. Microcks documents Kafka and MQTT support for AsyncAPI-driven mocking and contract testing, but I did not verify which AsyncAPI 3.x features each of those third-party tools supports in October 2026. Test a representative spec against any such tool before committing, especially if you use reply or Multi Format Schema Objects, which are the newest constructs.
Migrating From 2.x Without Breaking Consumers
Migration is the topic where practical advice is most valuable, because the changes are broad but mostly mechanical, and the dangerous ones are semantic.
Step 1: run the converter and read the diff
The official migration guide recommends the CLI converter:
asyncapi convert asyncapi.yaml --output=asyncapi_v3.yaml --target-version=3.0.0
For a 2.x document that you want to land directly on the current version, specify the version you intend to publish and confirm the flag accepts it in your CLI release. The guide itself does not claim the converter handles every case, so treat its output as a first draft that a human reviews.
Step 2: audit the semantic traps
Four categories need manual attention.
- Direction of publish/subscribe. In 2.x,
publishon a channel meant the application accepts messages that other parties publish, so for the service that owns the spec it maps toreceive. A converter that maps by name instead of by role flips your spec. Check each operation against the actual code: does this service callproducer.sendor does it run a consumer loop? - Channel key changes. Since the key becomes an arbitrary ID and the path moves to
address, any external reference (a catalog entry, a generated client, a linter rule) that used the path as the key now breaks. - Trait precedence. Properties on the target now win over trait properties. Search for traits that you used as defaults-with-override and re-check the rendered output.
- Parameter schemas. Parameters lost most of JSON Schema. Constraints such as
patternorminLengthon a topic parameter vanish, so enforce them elsewhere (broker ACLs, topic naming linters).
Step 3: version the contract, not just the file
Bumping asyncapi: 3.x changes the spec dialect; it says nothing about your contract. Your own info.version should follow semantic versioning with a concrete rule: adding an optional payload field is a minor bump, adding a required field or removing one is a major bump, and a major bump means a new channel address (for example coldchain.telemetry.v2) so old and new consumers can coexist during a migration window.
A short parallel-run approach works well in practice. Publish both v1 and v2 channels in the same document for one release cycle, each with its own operations, and mark the old operations as deprecated in their description and tags. Consumers move on their own schedule, and when the last v1 consumer disappears, a spec change removes the old channel.
Governance: Turning the Spec Into a Pipeline
A specification that nobody enforces is documentation, and documentation drifts. The point of adopting AsyncAPI is to make the contract part of the build. A minimal pipeline has five stages, shown below.

Figure 4: A CI governance flow for AsyncAPI specifications. The spec is validated, diffed against the main branch, checked against the schema registry, used to generate models and tests, and finally published as documentation.
What each stage catches
Lint and validate. Parse the document with the official parser or the CLI validator. This catches structural errors, dangling $refs, and invalid binding fields. Add a ruleset for house rules: every message must have an example, every channel a description, every operation a summary. This stage is cheap and removes the most embarrassing mistakes.
Breaking-change diff. Compare the pull request branch with main and fail on changes that break consumers: removing a required field, tightening an enum, changing a channel address, or changing the action of an operation. The CLI includes a diff capability, but I did not confirm its current maturity against 3.1 in the release notes I read, so evaluate it on your own specs before making it a blocking gate.
Registry compatibility. Register the payload schema against a candidate subject and ask the registry for a compatibility verdict. This is the hard guarantee; the AsyncAPI diff is a convenience on top.
Generate and test. Generate typed models from the payload schemas and compile them. A generated model that fails to compile is an early signal that the schema uses a construct your language cannot express. Run contract tests with a mock broker driven by the spec’s examples, verifying that producers emit messages that validate against the schema and consumers accept every example.
Publish. On merge, render documentation and push it to a portal or catalog. The key is that the published page is always built from the merged spec, never edited by hand.
Ownership and boundaries
Technical gates fail without clear ownership. A model that works is one repository per bounded context, with the owning team holding the spec for the channels they publish to. Consumers write their own spec documenting the channels they read, using receive operations that reference the producer’s channel through a shared components library. This keeps the producer’s contract authoritative while letting consumers declare precisely what they depend on, which is what makes impact analysis possible.
It is worth deciding early how AsyncAPI sits next to your other contract formats. If you also expose REST and gRPC, the trade-offs between those styles are laid out in our gRPC vs REST vs GraphQL vs Connect comparison. A reasonable division is OpenAPI for request/response, AsyncAPI for broker-mediated flows, and a common naming and ownership convention across both. AsyncAPI also supports request/reply over brokers, but if the interaction is genuinely synchronous, a broker is rarely the right transport. Likewise, whether a queue or a log is the right substrate is a design question the spec cannot answer; see Kafka, RabbitMQ and SQS for task queues.
AsyncAPI in an Industrial and Digital-Twin Context
Industrial systems add constraints that a typical web-platform article ignores. Three deserve mention.
Existing standards define their own contracts. OPC UA PubSub over MQTT, Sparkplug B, and vendor-defined payloads each come with a published information model. AsyncAPI does not replace those; it can wrap them. A Sparkplug B topic namespace (spBv1.0/{group}/{messageType}/{edgeNode}/{device}) can be modelled as a channel with parameters, and the protobuf payload referenced through a Multi Format Schema Object. The value you add is a machine-readable statement of which message types a given service actually handles. Our comparison of OPC UA PubSub over MQTT versus UADP explains the encoding choices that determine which of those payload descriptions you will write.
Edge links are unreliable, so delivery semantics are part of the contract. QoS level, retained messages, session expiry and message expiry all change what a consumer can assume. The MQTT binding exposes exactly these knobs (qos, retain, sessionExpiryInterval, messageExpiryInterval), and writing them down turns “we think it’s at-least-once” into a reviewable statement. Whether the broker actually honours the stated QoS under reconnect storms is a runtime concern that the spec cannot verify.
Digital twins consume more than they produce. A twin service typically subscribes to many channels from many owners. This is where a catalog built from AsyncAPI documents pays off: you can answer “which services consume coldchain.telemetry.v1, and which would break if the payload changed” from the specs, instead of from a week of grepping repositories. That dependency query is, in my view, the strongest argument for adopting the format in an IoT organisation, stronger than code generation.
Trade-offs, Gotchas, and What Goes Wrong
AsyncAPI is not free, and several failure modes show up repeatedly.
Specs that describe intent rather than reality. The commonest failure is a beautifully rendered document that no longer matches production. Without the CI gates above, drift begins within weeks. The cure is a runtime check as well: sample live messages in a non-production environment and validate them against the spec’s schemas. If you only validate documents, not traffic, you have validated the wrong thing.
Binding fields invented by examples. As shown above, many tutorials, including earlier versions of this very post, show Kafka fields that the binding does not define. Strict validators will reject unknown properties in binding objects because those objects disallow extras (except Kafka’s topicConfiguration). Always check field names against the bindings repository, not against a blog.
Over-modelling. Teams sometimes capture every broker setting in the spec. The result is a document that must change whenever an operator tunes retention, and that conflates contract with configuration. Keep in the spec what a consumer needs to know to interoperate; keep operational tuning in infrastructure code.
Tooling gaps on the newest constructs. reply objects, Multi Format Schema Objects and 3.1’s ROS 2 binding are the least widely supported features across third-party tools. Before you design around them, confirm that each tool in your toolchain handles them. A fallback is to model request/reply as two ordinary operations on two channels, tied together by the correlation id.
Runtime expressions are only descriptive. $message.header#/replyTo tells a reader and a generator where the reply address comes from. It does not make your broker route anything. Implementation still lives in code.
Version skew in CI. The Node.js 24 requirement for recent CLI versions is a concrete example: a pipeline that has been stable for years can fail after a routine dependency bump. Pin the CLI version, run it in a container, and upgrade deliberately.
Schema language mismatch. Describing a protobuf payload in a JSON Schema-flavoured Schema Object loses information. Use the Multi Format Schema Object with the correct schemaFormat, and make sure your validator and generator actually understand that format, because support is not uniform.
Practical Recommendations
Start narrow and let value pull the rollout. Pick one flow that has at least three consumers and a history of breakage, such as device telemetry feeding a digital twin, and write its specification first. Your first goal is not coverage; it is to find out which of your conventions the spec cannot express.
Target 3.1.0 for new documents. It is a drop-in minor update with no breaking changes, parser support is in place, and there is no reason to start new work on 3.0.0 or 2.x. For existing 2.x documents, migrate in a branch, review the converter output by hand, and do not publish until every operation’s send or receive has been confirmed against the code.
Keep the spec and the registry coordinated. Treat the registry as the enforcement point for payload compatibility and the AsyncAPI document as the map. Do not duplicate schemas; generate or reference.
Put the document in the same repository as the code that implements it, or in a dedicated contracts repository with strict code ownership, and review spec changes like API changes.
A short checklist you can adopt as-is:
- [ ] Document targets
asyncapi: 3.1.0and validates cleanly with the current parser. - [ ] Every operation’s
actionhas been verified against the implementing code. - [ ] Every message has at least one realistic example and a
correlationIdwhere replies exist. - [ ] Binding fields have been checked against the bindings repository for the stated
bindingVersion. - [ ] QoS, retention and key semantics are stated explicitly rather than assumed.
- [ ] CI runs lint, breaking-change diff, registry compatibility and contract tests.
- [ ] The CLI and generator versions are pinned, with Node.js 24 or newer in the build image.
- [ ] A major payload change creates a new versioned channel address, not an in-place edit.
- [ ] Documentation is published from merged specs only.
Frequently Asked Questions
Is AsyncAPI 3.0 the latest version?
No. The latest published version as of late October 2026 is 3.1.0, released on 31 January 2026. It is a minor, non-breaking update that adds a ROS 2 protocol binding, and moving from 3.0.0 only requires changing the asyncapi version string. The spec repository showed no 3.2 or later release when I checked. Version 3.0.0 came out on 5 December 2023, so many articles that give a 2024 date are simply wrong.
What is the difference between AsyncAPI and OpenAPI?
OpenAPI describes request/response HTTP APIs: a client calls a path and receives a reply. AsyncAPI describes message-driven interactions over brokers and similar transports, where applications send or receive messages on channels with no guaranteed single response. They share a spec-first philosophy and both support JSON Schema-like payload description, but their document structures differ and AsyncAPI 3 does not try to mirror OpenAPI’s layout. Many platforms use both.
Should I use AsyncAPI with Kafka, MQTT, or both?
Both are supported through protocol bindings, and a single document can describe an MQTT ingest side and a Kafka distribution side, as in the example above. Bindings carry protocol-specific detail: Kafka consumer groups and topic configuration, or MQTT QoS and session settings. Use one document per bounded context rather than one per broker. The binding versions to check are 0.5.0 for Kafka and 0.2.0 for MQTT at the time of writing.
How do I migrate from AsyncAPI 2.x to 3.x?
Run asyncapi convert to get a first draft, then review manually. The mechanical changes are server URL splitting and moving tags into info. The risky ones are mapping publish and subscribe to send and receive by role, rewriting references to channel keys, re-checking trait precedence, and accepting that parameters can no longer carry full JSON Schema. The official migration guide lists each step, and the converter does not claim to cover every case.
Can AsyncAPI enforce schemas at runtime?
Not by itself. AsyncAPI is a description format; enforcement comes from tooling you add. Validate documents in CI with the parser, enforce payload compatibility in a schema registry, and validate sampled live traffic against the schemas in a test or staging environment. Generated code from the spec can include serializers and validators, but you must wire them into your producers and consumers. Treat the document as the source of the contract, not as the guard.
Does AsyncAPI support Avro and Protobuf payloads?
Yes, through the Multi Format Schema Object introduced in 3.0. Inside payload you set schemaFormat (for example application/vnd.apache.avro+json;version=1.9.0) and a schema in that format. The default, when you embed a plain Schema Object, is the AsyncAPI JSON Schema dialect at the document’s version. Support for non-default formats varies across third-party tools, so verify your validator and generator handle the format you choose.
Further Reading
- MQTT store-and-forward at the edge gateway: buffering, bridging and QoS behaviour that your MQTT bindings describe.
- Data contracts and schema registries for streaming lakehouses: the payload-compatibility half of the contract.
- Sparkplug B vs plain MQTT topics: topic namespace conventions you can model as channels.
- Kafka exactly-once semantics and transactional outbox with CDC: delivery guarantees behind the spec.
- OPC UA PubSub over MQTT vs UADP: encoding choices for industrial payloads.
- AsyncAPI 3.1.0 release notes and AsyncAPI 3.0.0 release notes: primary sources for version changes.
- Migrating to AsyncAPI v3: the official step list.
- AsyncAPI bindings repository: authoritative field lists for Kafka, MQTT and other protocols.
By Riju — about
