Skip to content

🇬🇧 English | 🇺🇦 Українська

← Back to contents

8. JetStream: Publishing

Publishing into JetStream differs from an ordinary Publish (5. Core Messaging) mainly in that the server acknowledges the write: the call doesn't just queue the message for sending, it waits for the server to confirm the message was actually stored in the stream, and returns a sequence number.


Publish Message

Get JetStream
   │
   └─► Get Publisher
          │
          └─► Publish Message
                 Subject         : "orders.created"
                 Data            : "{\"id\":1,\"total\":99.99}"
                 Timeout Seconds : 5.0
                 │
                 └─ bSuccess → PubAck
                                  ├─ Stream     : "ORDERS"
                                  ├─ Sequence   : 1
                                  └─ Duplicate  : false
UNatsJetStreamPublisherImpl* Publisher = Nats->GetJetStream()->Publisher();

Publisher->Publish(TEXT("orders.created"), TEXT("{\"id\":1,\"total\":99.99}"),
    [](TJetStreamResult<FJetStreamPubAck> Result)
    {
        if (Result.IsSuccess())
        {
            UE_LOG(LogTemp, Log, TEXT("Stream=%s Seq=%d"), *Result.Value.Stream, Result.Value.Sequence);
        }
        else
        {
            UE_LOG(LogTemp, Error, TEXT("Publish failed: %s"), *Result.Error.Message);
        }
    });

If no stream captures the Subject, the operation fails (the server doesn't respond, and the result comes back with EJetStreamErrorCode::ServerError), unlike an ordinary Publish, which in that case simply does nothing. This is expected: JetStream guarantees persistence, so publishing into nothing is a configuration mistake, not a normal fire-and-forget scenario.

FJetStreamPubAck

Field Description
Stream Which stream stored the message
Sequence The sequence number assigned to the message in the stream
Duplicate true if the message was recognized as a duplicate (see Message ID below)
Domain The JetStream domain in multi-tenant deployments (usually empty)

Deduplication: Publish With Message ID

The network is unreliable: a client can fail to receive an acknowledgment even when the server has already stored the message, and retry the publish "just in case." To keep that retry from creating a duplicate order, provide your own unique message ID:

Publish With Message ID
   Subject     : "orders.created"
   Data        : "{\"id\":1,\"total\":99.99}"
   Message Id  : "order-1-attempt"
Publisher->PublishWithMsgId(TEXT("orders.created"), Data, TEXT("order-1-attempt"),
    [](TJetStreamResult<FJetStreamPubAck> Result)
    {
        if (Result.IsSuccess() && Result.Value.Duplicate)
        {
            // The server has already seen this Message Id — no new message was added,
            // Sequence points at the existing record.
        }
    });

If a message with the same Message Id was already received within the server's deduplication window (typically 2 minutes), the server returns Duplicate = true and does not create a second record — the Sequence in the response points at the existing message. This makes a retry safe: code can simply repeat the publish whenever it's unsure about the outcome, without risking duplicated data.


Optimistic concurrency: Publish With Expected Sequence

When several publishers might write to the same subject concurrently and order matters, Publish With Expected Sequence gives you a "write only if I know the stream's last state" publish:

Publish With Expected Sequence
   Subject           : "orders.created"
   Data              : (new order)
   Expected Last Seq : 41      ← I believe the last message in the stream is #41
Publisher->PublishWithExpectedSeq(Subject, Data, 41,
    [](TJetStreamResult<FJetStreamPubAck> Result)
    {
        if (!Result.IsSuccess())
        {
            // Someone else published a message between reading the state and this call —
            // ExpectedLastSeq no longer matches. Read the current LastSeq
            // (Get Stream Info) and decide whether to retry.
        }
    });

A typical loop: read Get Stream Info → State.LastSeq, attempt the publish with that value; on failure, read the current LastSeq again and decide whether to retry.


Bytes variants of publishing

Each of the four methods above has a binary counterpart — the same principle as in 6. Binary Data: bytes are transmitted exactly, without text encoding.

Text Bytes
Publish Message Publish Bytes
Publish With Headers Publish Bytes With Headers
Publish With Message ID Publish Bytes With Message ID
Publish With Expected Sequence Publish Bytes With Expected Sequence
Publish Bytes
   Subject : "assets.uploaded"
   Payload : (byte array, e.g. a compressed file)
Publisher->PublishBytes(TEXT("assets.uploaded"), CompressedBytes, Callback);

Publishing with headers

Make Map (String → String)
   ["Content-Type"] = "application/json"
   │
   └─► Publish With Headers
          Subject : "orders.created"
          Data    : "{\"id\":1}"
          Headers : (the map above)

Headers in JetStream work the same way as in Core (5. Core Messaging) — these are ordinary NATS message headers, JetStream just stores them alongside the data.


The On Publish Ack event

Besides the result returned by each individual call's callback, there's a general event — handy for centralized monitoring of every publish rather than handling each one separately:

Get Game Instance Subsystem (Nats Client Subsystem)
   │
   └─► Bind Event to On JetStream Pub Ack
            │
            └─► (bSuccess, PubAck) → update a UI counter, log it

Fires for every successful and unsuccessful publish through the Publisher, regardless of exactly where it was called from.


Practical example: reliably sending an order with retry

void AOrderService::SubmitOrder(const FString& OrderId, const FString& OrderJson)
{
    UNatsJetStreamPublisherImpl* Publisher = Nats->GetJetStream()->Publisher();

    // OrderId as the Message Id: a retry (e.g. after a network timeout)
    // is safe — there won't be a duplicate.
    Publisher->PublishWithMsgId(TEXT("orders.created"), OrderJson, OrderId,
        [this, OrderId](TJetStreamResult<FJetStreamPubAck> Result)
        {
            if (Result.IsSuccess())
            {
                UE_LOG(LogTemp, Log, TEXT("Order %s: seq=%d, duplicate=%s"),
                    *OrderId, Result.Value.Sequence, Result.Value.Duplicate ? TEXT("yes") : TEXT("no"));
            }
            else if (Result.Error.Code == EJetStreamErrorCode::Timeout)
            {
                // A network issue — the same Message Id is safe to retry.
                RetrySubmitOrder(OrderId);
            }
        });
}

Next: 9. JetStream: Consumers