🇬🇧 English | 🇺🇦 Українська
7. JetStream: Streams¶
A stream is a message store; covered conceptually in 1. Introduction to NATS. This chapter is about creating and managing streams through the plugin.
Entering JetStream¶
Every JetStream operation — streams, consumers, publishing, Key-Value — is available through a single context object, which the plugin obtains automatically as soon as the client connects:
Get Game Instance Subsystem (Nats Client Subsystem)
│
└─► Get JetStream
│
├─► Get Streams Manager ──► [this chapter]
├─► Get Consumers Manager ──► 9. JetStream: Consumers
├─► Get Publisher ──► 8. JetStream: Publishing
└─► Get Key-Value Store ──► 10. JetStream: Key-Value
UNatsJetStreamContext* JS = Nats->GetJetStream();
UNatsStreamManagerImpl* Streams = JS->Streams();
Is JetStream available¶
A server can run without JetStream enabled (the plugin handles this normally — Core works independently, 1. Introduction to NATS). Check availability:
Get JetStream
│
└─► Is Available ──► (bool)
Or subscribe to status changes — useful when the server comes up slower than the game:
Get JetStream
│
└─► Bind Event to On Availability Changed
│
└─► (bAvailable) → if true, you can now create streams
Right after
On Connected, JetStream usually isn't available yet — the check happens asynchronously. If you need auto-created streams without manually waiting for this event, useAuto Create Streamsin 3. Configuration: the subsystem waits for JetStream readiness on its own.
Stream configuration¶
FJetStreamStreamConfig is the struct shared by creation, updates, and auto-creation from
Project Settings.
| Field | Type | Default | Description |
|---|---|---|---|
Name |
FString |
— | A unique name, e.g. "ORDERS". Convention is UPPERCASE |
Subjects |
TArray<FString> |
— | The subjects the stream captures, with wildcard support: "orders.>" |
Storage |
EJetStreamStorageType |
Memory |
Memory — fast, gone on server restart. File — survives a restart |
Retention |
EJetStreamRetentionPolicy |
Limits |
See below |
Discard |
EJetStreamDiscardPolicy |
Old |
Old — evict old messages with new ones. New — reject new ones once the limit is reached |
MaxMsgs |
int32 |
-1 (no limit) |
The maximum number of messages in the stream |
MaxBytes |
int32 |
-1 (no limit) |
The maximum total size |
MaxAge |
int32 |
0 (no limit) |
The maximum message age, in seconds |
MaxMsgSize |
int32 |
-1 (server limit) |
The maximum size of a single message |
Replicas |
int32 |
1 |
Number of replicas in a cluster (requires a multi-node NATS cluster) |
NoAck |
bool |
false |
Disable publish acknowledgment (not recommended) |
MaxMsgs/MaxBytesset to-1mean "no limit" — on a production server that means unbounded disk or memory growth. Set a sensible limit deliberately, don't leave the default unconsidered.
Retention: how long a message lives¶
| Value | Behavior | Typical use |
|---|---|---|
Limits (default) |
Messages live until MaxMsgs/MaxBytes/MaxAge is exhausted |
An event log, metrics, history |
Interest |
Messages are removed as soon as every active consumer has acknowledged them | Data only needed while there's at least one interested subscriber |
WorkQueue |
A message is removed immediately after being acknowledged by any consumer | A task queue: each task is processed exactly once |
Builder (C++)¶
In C++ it's more convenient to build the configuration via
FJetStreamStreamConfigBuilder — the same result as manually filling in the struct, but
with validation and no risk of forgetting a required field:
FJetStreamStreamConfig Config = FJetStreamStreamConfigBuilder()
.WithName(TEXT("ORDERS"))
.WithSubject(TEXT("orders.>"))
.WithStorage(EJetStreamStorageType::File)
.WithRetention(EJetStreamRetentionPolicy::WorkQueue)
.WithMaxAge(FTimespan::FromDays(30))
.Build();
if (TOptional<FString> Error = FJetStreamStreamConfigBuilder()
.WithName(TEXT("ORDERS"))
.Validate())
{
UE_LOG(LogTemp, Error, TEXT("Invalid configuration: %s"), **Error);
}
WithSubject adds one subject to the list; for several at once, use
WithSubjects(TArray<FString>). In Blueprint, the configuration is always filled directly
via Break/Make Jet Stream Stream Config — the builder is purely a C++ convenience.
Operations¶
Create Stream¶
Get Streams Manager
│
└─► Create Stream
Config → Name : "ORDERS"
Config → Subjects : ["orders.>"]
Config → Storage : File
│
└─ bSuccess → StreamInfo → State → Messages, Bytes...
FJetStreamStreamConfig Config;
Config.Name = TEXT("ORDERS");
Config.Subjects.Add(TEXT("orders.>"));
Config.Storage = EJetStreamStorageType::File;
Streams->CreateStream(Config, [](TJetStreamResult<FJetStreamStreamInfo> Result)
{
if (Result.IsSuccess())
{
UE_LOG(LogTemp, Log, TEXT("Stream ready: %s"), *Result.Value.Config.Name);
}
});
If a stream with that name and configuration already exists, the operation returns success
with the existing info rather than an error. This makes Create Stream safe to call on
every game start, without checking for existence first.
Get Stream Info¶
Get Stream Info
Stream Name : "ORDERS"
│
└─ bSuccess → StreamInfo
├─ Config → settings
├─ State → Messages, Bytes, FirstSeq, LastSeq, NumConsumers
└─ Created
The cheapest way to check "does this stream exist" and look at current statistics — message count, size taken up, and how many active consumers there are.
Update Stream¶
FJetStreamStreamConfig Config;
Config.Name = TEXT("ORDERS"); // must be the same name
Config.MaxMsgs = 100000; // new limit
Streams->UpdateStream(Config, Callback);
You can change: Subjects (adding new ones), MaxMsgs/MaxBytes/MaxAge, Retention,
Discard. You cannot change after creation: Name and Storage — for that, you'll
need to delete the stream and create it again.
Lowering limits takes effect immediately. If the current message count exceeds the new
MaxMsgs, the excess is removed right away, following theDiscardrule.
Delete Stream¶
Delete Stream
Stream Name : "OLD_EVENTS"
Irreversible: deletes every message in the stream and every consumer on it. If you only
need to clear out old messages, not destroy the stream entirely, lower MaxAge/MaxMsgs
via Update Stream instead.
List Streams¶
List Streams
│
└─ bSuccess → Stream Names : ["ORDERS", "EVENTS", "TELEMETRY"]
Returns names only. Details for each require a separate Get Stream Info call.
Example: a game events stream¶
void AMyGameMode::SetupEventStream()
{
UNatsJetStreamContext* JS = Nats->GetJetStream();
FJetStreamStreamConfig Config = FJetStreamStreamConfigBuilder()
.WithName(TEXT("GAME_EVENTS"))
.WithSubject(TEXT("game.events.>"))
.WithStorage(EJetStreamStorageType::File)
.WithRetention(EJetStreamRetentionPolicy::Limits)
.WithMaxAgeSeconds(7 * 24 * 3600) // one week
.Build();
JS->Streams()->CreateStream(Config, [](TJetStreamResult<FJetStreamStreamInfo> Result)
{
if (!Result.IsSuccess())
{
UE_LOG(LogTemp, Error, TEXT("Failed to create stream: %s"), *Result.Error.Message);
}
});
}
Now everything published on game.events.* is stored for a week — regardless of whether
anyone is subscribed at that moment. The next step is reading these messages through a
consumer: 9. JetStream: Consumers.
Next: 8. JetStream: Publishing