AsyncAPI 3.0: Event-Driven API Specification Complete Guide

AsyncAPI 3.0: Event-Driven API Specification Complete Guide

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.

AsyncAPI 3 event driven API specification object model showing channels, operations, messages, schemas, servers and bindings

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.

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.

AsyncAPI 2.x versus 3.x structure showing channel keys, publish and subscribe nesting versus decoupled channels and operations with send and receive

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.

asyncapi 3 event driven api specification applied to a cold chain pipeline with MQTT ingest, bridge, Kafka topic and consumers

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.

  1. Direction of publish/subscribe. In 2.x, publish on a channel meant the application accepts messages that other parties publish, so for the service that owns the spec it maps to receive. A converter that maps by name instead of by role flips your spec. Check each operation against the actual code: does this service call producer.send or does it run a consumer loop?
  2. 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.
  3. 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.
  4. Parameter schemas. Parameters lost most of JSON Schema. Constraints such as pattern or minLength on 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.

AsyncAPI governance pipeline from pull request through lint, breaking-change diff, registry compatibility, model generation, contract tests and docs publication

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.0 and validates cleanly with the current parser.
  • [ ] Every operation’s action has been verified against the implementing code.
  • [ ] Every message has at least one realistic example and a correlationId where 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

By Riju — about

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *