Home Posts AsyncAPI and NATS JetStream Real-Time API Guide [2026]
System Architecture

AsyncAPI and NATS JetStream Real-Time API Guide [2026]

AsyncAPI and NATS JetStream Real-Time API Guide [2026]
Dillip Chowdary
Dillip Chowdary
Tech Entrepreneur & Innovator · May 04, 2026 · 8 min read

Bottom Line

Use AsyncAPI to lock the event contract first, then map NATS subjects into JetStream streams and durable pull consumers. That gives you replayable, observable, at-least-once event delivery without letting broker details leak into every service.

Key Takeaways

  • Use AsyncAPI 3.1.0 to define channels, operations, and payload schema before broker setup.
  • JetStream stores NATS subjects in streams; durable pull consumers add replay and at-least-once delivery.
  • nats pub --wait returns a publish ack, so you can confirm a message was persisted.
  • Keep orders.created aligned across AsyncAPI, stream subjects, and consumer filters.

Real-time APIs break down when teams treat the broker as the contract. A better approach is to define the event surface in AsyncAPI 3.1.0, then implement delivery semantics with NATS JetStream. In this walkthrough, you will model an order event, validate the spec, start a local JetStream node, create a durable pull consumer, and verify replayable delivery with the official CLI. The result is a contract-first event pipeline that is simple enough for local development and strong enough to evolve toward production.

  • AsyncAPI 3.1.0 gives you a machine-readable contract for channels, operations, and payloads.
  • JetStream adds persistence, replay, and consumer state on top of core NATS subjects.
  • Durable pull consumers are the clean default for worker fleets that need explicit acknowledgments.
  • nats pub --wait and nats consumer next make local verification fast and concrete.

Prerequisites

What you need

  • Docker so you can run the official nats image locally.
  • The nats CLI installed and available on your path.
  • npm so you can install the official @asyncapi/cli.
  • Basic familiarity with JSON payloads and publish/subscribe messaging.

Bottom Line

AsyncAPI should describe what events exist and who sends or receives them. JetStream should enforce how those events are stored, replayed, and acknowledged.

One practical rule keeps this design honest: your AsyncAPI channel address, your JetStream stream subjects, and your consumer filter must all agree. Most local failures come from that mismatch, not from the broker itself.

1. Model the event contract in AsyncAPI

Start with the event, not the infrastructure. AsyncAPI 3.1.0 supports channels, operations, and protocol bindings, but you do not need heavy protocol metadata to get immediate value. For a first pass, define the NATS server, one channel, two operations, and one message schema. If your team wants to annotate runtime intent, keep JetStream details in an x-jetstream extension so the core contract stays portable.

Why this contract-first split works

  • The AsyncAPI document becomes the review surface for payload changes.
  • Producer and consumer teams can code against one agreed event shape.
  • JetStream configuration stays operational, where retention and replay policies belong.
asyncapi: '3.1.0'
info:
  title: Orders Events API
  version: '1.0.0'
  description: Contract for order-created events over NATS.
defaultContentType: application/json
servers:
  local:
    host: localhost:4222
    protocol: nats
    description: Local NATS server with JetStream enabled
channels:
  ordersCreated:
    address: orders.created
    messages:
      orderCreated:
        $ref: '#/components/messages/OrderCreated'
operations:
  publishOrderCreated:
    action: send
    channel:
      $ref: '#/channels/ordersCreated'
    messages:
      - $ref: '#/components/messages/OrderCreated'
    x-jetstream:
      stream: ORDERS
  receiveOrderCreated:
    action: receive
    channel:
      $ref: '#/channels/ordersCreated'
    messages:
      - $ref: '#/components/messages/OrderCreated'
    x-jetstream:
      consumer: order-workers
components:
  messages:
    OrderCreated:
      name: OrderCreated
      title: Order created event
      payload:
        type: object
        required:
          - orderId
          - customerId
          - status
          - createdAt
        properties:
          orderId:
            type: string
          customerId:
            type: string
          status:
            type: string
            enum: [created]
          createdAt:
            type: string
            format: date-time
      examples:
        - payload:
            orderId: ord_1001
            customerId: cus_9001
            status: created
            createdAt: '2026-05-04T12:00:00Z'

Validate the file before you touch the broker:

npm install -g @asyncapi/cli
asyncapi validate asyncapi.yaml

If the YAML gets messy during edits, run it through TechBytes' Code Formatter before committing it to CI. That is not a spec validator, but it removes indentation noise so validation failures are easier to spot.

2. Start JetStream locally

JetStream is built into nats-server; you enable it with the -js flag. For local work, the shortest path is the official Docker image. In production, you would usually move this into a managed environment or a clustered deployment, but the local loop is enough to prove the contract and delivery semantics.

docker run --rm -p 4222:4222 nats -js

nats account info

Your verification point is simple: nats account info should show a JetStream Account Information section. If it instead says JetStream is not supported in the account, stop here and fix the runtime before creating streams or consumers.

If you want persistence across container restarts, the official image also supports mounting a volume and passing -sd to set the store directory.

3. Create the stream and durable pull consumer

Now connect the contract to the broker. In NATS, the subject is the routing key. In JetStream, a stream captures one or more subjects for storage, and a consumer defines how an application reads them. For this tutorial, one stream stores all orders.* events, while one durable pull consumer processes only orders.created.

  • The stream handles persistence and retention.
  • The durable consumer preserves delivery progress across process restarts.
  • The pull mode lets worker instances scale horizontally without push-side backpressure surprises.
nats stream add ORDERS \
  --subjects "orders.*" \
  --ack \
  --storage file \
  --retention limits \
  --discard old \
  --max-age=1h

nats consumer add ORDERS order-workers \
  --filter orders.created \
  --ack explicit \
  --pull \
  --deliver all \
  --replay instant

This is the key design move in the whole stack: your AsyncAPI channel still names the business event, while JetStream decides how long it lives and how consumers advance through it. That separation lets you change retention or replay policy without rewriting every client contract.

4. Publish, replay, and verify delivery

With the stream and consumer in place, publish a couple of events and read them back from the durable consumer. Using nats pub --wait matters because it returns the publish acknowledgment from JetStream, so you know the message was stored rather than merely accepted by core NATS routing.

nats pub orders.created --wait '{"orderId":"ord_1001","customerId":"cus_9001","status":"created","createdAt":"2026-05-04T12:00:00Z"}'
nats pub orders.created --wait '{"orderId":"ord_1002","customerId":"cus_9002","status":"created","createdAt":"2026-05-04T12:00:05Z"}'

nats consumer next ORDERS order-workers --count 2

nats stream info ORDERS
nats consumer info ORDERS order-workers

Verification and expected output

  • Each nats pub --wait call should return a JetStream publish ack containing the stream name and a sequence number.
  • nats consumer next should print the subject, message body, and Acknowledged message.
  • nats stream info ORDERS should show stored messages in the stream.
  • nats consumer info ORDERS order-workers should show delivery progress advancing and no lingering outstanding acknowledgments after the fetch completes.

That last point is what makes JetStream operationally useful. You are no longer guessing whether a worker merely saw an event. You have consumer state, replay control, and redelivery semantics attached to a durable named resource.

Troubleshooting top 3

  • JetStream is not supported in this account: your server was not started with -js, or your account is not allowed to use JetStream. Re-run nats account info before debugging anything else.
  • The consumer pulls nothing: check subject alignment first. orders.created in AsyncAPI, orders.* in the stream, and --filter orders.created in the consumer must line up exactly.
  • Messages redeliver unexpectedly: with explicit ack mode, unacked messages are redelivered after the ack wait window. Inspect nats consumer info ORDERS order-workers for outstanding acks and redelivery counts, and use idempotent handlers in the app layer.

What's next

This local setup is enough to establish a reliable engineering pattern. The next step is to harden it without collapsing the contract and infrastructure back into one file.

  • Promote the AsyncAPI file into CI so every payload change is validated before merge.
  • Move stream and consumer creation into infrastructure automation rather than ad hoc CLI runs.
  • Add publisher deduplication with Nats-Msg-Id if duplicate publishes matter in your flow.
  • For clustered production setups, prefer file-backed streams with Replicas=3 over a single-node local profile.
  • Add authentication, account scoping, and subject permissions before exposing the broker outside development.

The important architectural win is this: AsyncAPI defines the event surface your teams depend on, while JetStream provides the delivery guarantees your systems need. Keep those responsibilities separate, and real-time APIs stay understandable even as the topology grows.

Frequently Asked Questions

What does AsyncAPI describe that JetStream does not? +
AsyncAPI describes the contract: channels, operations, message payloads, and the roles of senders and receivers. JetStream handles runtime concerns such as persistence, replay, consumer state, and acknowledgment behavior. You want both, but they solve different layers of the problem.
Should I use push or pull consumers with NATS JetStream? +
For worker-style services, pull consumers are usually the safer default because they scale horizontally and make backpressure explicit. Push consumers are useful when you want immediate fan-out or ordered replay behavior. The important choice is matching the consumer mode to the processing pattern, not forcing one mode everywhere.
Can AsyncAPI model JetStream retention and replicas directly? +
Not as a portable first-class contract in the same way it models messages and operations. The usual pattern is to keep broker-specific details in infrastructure config and, if needed, annotate the AsyncAPI file with custom extensions such as x-jetstream. That keeps the contract useful without pretending broker policy is protocol-agnostic.
Why do I still see duplicate events with JetStream? +
JetStream durable consumers are commonly used in an at-least-once pattern, so redelivery is expected when a message is not acknowledged in time. Use idempotent consumers, inspect consumer state for redeliveries, and add publisher-side deduplication with Nats-Msg-Id where it fits. Duplicates are usually an application-design concern, not proof that JetStream is misbehaving.

Get Engineering Deep-Dives in Your Inbox

Weekly breakdowns of architecture, security, and developer tooling — no fluff.

Found this useful? Share it.