Live Streaming
A live session is five calls in a fixed order, and the fifth one starts the bill. This page covers the whole lifecycle: connecting, the CRAM handshake, subscriptions, the two record loops, reconnection, and the timeouts that decide when a quiet feed becomes an error.
For a first working program, start with Getting Started. This page assumes you have one.
The session lifecycle
ConnectAsync → AuthenticateAsync → SubscribeAsync → StartAsync → records
↑ │
└── free ─────────┴── billed ──→
LiveClient splits connection from authentication, where upstream's Client::build() does both
at once. That is what makes ConnectTimeoutException and AuthTimeoutException nameable as the
different failures they are — a gateway you cannot reach and a key it will not accept are not the
same problem.
Nothing before StartAsync moves market data. A subscription tells the gateway what to send
later; the gateway sends nothing at all until the session is started. That is why the
repository's own smoke tests against the real gateway are free and its one session test sits
behind a second environment gate.
using DatabentoDotNet; // ApiKey, Symbols
using DatabentoDotNet.Dbn; // Metadata, Schema, SType, the record structs
using DatabentoDotNet.Live; // LiveClient, Subscription
await using var client = new LiveClient
{
ApiKey = new ApiKey(apiKeyString),
Dataset = "EQUS.MINI",
};
await client.ConnectAsync(ct);
await client.AuthenticateAsync(ct);
await client.SubscribeAsync(new Subscription
{
Schema = Schema.Trades,
Symbols = Symbols.From("AAPL"),
}, ct);
Metadata metadata = await client.StartAsync(ct);
await using matters: DisposeAsync closes the socket. A client that goes out of scope without
it leaves a connection open on the gateway's side until it times out.
Constructing the client
ApiKey and Dataset are required; everything else has a default. There is no builder —
upstream's generic type-state ClientBuilder<AK, D> exists to make "no API key" unrepresentable,
and C# required init properties do that natively, checked by the compiler at every construction
site.
| Property | Default | Notes |
|---|---|---|
ApiKey |
required | Validated at construction. Renders as …a1b2c in logs and exceptions |
Dataset |
required | e.g. EQUS.MINI, XNAS.ITCH, GLBX.MDP3. Also picks the gateway host |
Compression |
None |
Zstd compresses the record stream. Settled on the auth line, not later |
SendTsOut |
false |
Ask the gateway to append its send timestamp to each record |
HeartbeatInterval |
null |
5–1800 seconds, whole seconds only. null leaves it to the gateway |
SlowReaderBehavior |
null |
Warn or Skip. null leaves it to the gateway |
UpgradePolicy |
UpgradeToV3 |
How records from older DBN versions are handled |
ConnectTimeout |
10 s | Budget for ConnectAsync |
AuthTimeout |
10 s | Budget for the whole handshake, not per line |
ReadTimeout |
null |
See Timeouts below |
Gateway |
null |
Override the resolved address. For tests against a mock |
HeartbeatInterval is validated client-side, which upstream does not do. A value outside
5–1800 seconds raises ArgumentOutOfRangeException immediately rather than costing a round trip
and a closed connection to discover, and a fractional-second value raises ArgumentException
rather than being silently truncated — an interval that differs between your code and the wire is
exactly the kind of confidently-wrong behaviour this library is built to avoid.
Gateway resolution
LiveGateway.For(dataset) derives the host from the dataset: GLBX.MDP3 becomes
glbx-mdp3.lsg.databento.com:13000. ConnectAsync does this for you. Set Gateway explicitly
only when you are pointing at a mock, or at an address you have specifically been told to use.
Authentication
AuthenticateAsync runs the CRAM handshake: the gateway sends a challenge, the client answers
with a SHA-256 of the challenge and the key, and the gateway replies success=1|session_id=….
Afterwards Greeting holds the gateway's version banner verbatim and SessionId holds the id —
both are diagnostics, and Greeting is deliberately not parsed.
The handshake is not cancel-safe, and the client does not pretend otherwise. A half-written
authentication line desynchronises the gateway, which then closes the connection. Cancelling
AuthenticateAsync therefore tears the socket down rather than abandoning a partial write. A
caller whose AuthenticateAsync throws — for any reason, cancellation included — is disconnected
and must connect again. There is no resuming it.
The same applies to SubscribeAsync, ResubscribeAsync, and StartAsync. All four write control
lines, and half a control line is not recoverable.
Subscriptions
var sent = await client.SubscribeAsync(new Subscription
{
Schema = Schema.Mbp1,
Symbols = Symbols.From(["ES.FUT", "NQ.FUT"]),
StypeIn = SType.Parent,
}, ct);
Console.WriteLine(sent.Id); // the correlation id the gateway will quote in any error
| Property | Default | Notes |
|---|---|---|
Schema |
required | Trades, Mbo, Mbp1, Mbp10, Ohlcv1S…Ohlcv1D, Definition, Status, … |
Symbols |
required | Symbols.From(...), Symbols.FromIds(...), or Symbols.All |
StypeIn |
RawSymbol |
How to interpret the symbols — Parent, Continuous, InstrumentId, … |
Start |
null |
Intraday replay: begin from this Instant rather than from now |
UseSnapshot |
false |
Send the current book state first. MBO only, and not with Start |
Id |
assigned | A correlation handle. Leave it unset and the client assigns a monotonic one |
SubscribeAsync returns what it sent, with Id filled in, rather than mutating your object —
Subscription is an immutable record. The sent form is also appended to client.Subscriptions,
which is the list ResubscribeAsync replays.
Symbol counts and chunking
The gateway caps a subscription line at 500 symbols. SubscribeAsync splits automatically, one
line per chunk, with is_last=1 on the final line only — that flag is what makes a chunked
subscription one subscription rather than several partial ones. You do not need to chunk
yourself; Symbols.ChunkCount tells you how many lines a given set will take.
Symbols.All subscribes to everything the dataset carries. On a busy dataset that is a very large
amount of data, and the bill reflects it.
Validation happens before the socket
A subscription that combines UseSnapshot with Start, asks for a snapshot on a schema other
than MBO, or names no symbols raises ArgumentException and writes nothing. That check runs
before the connection check too, so a subscription the client would never send is rejected the
same way whether or not a socket happens to be open.
Subscribing after the session starts
Legal, and the same code path. The gateway distinguishes a mid-session subscription from a pre-session one; this client does not need to.
Reading records
Two surfaces, and the difference is whether records may cross an await.
The zero-copy loop
while (true)
{
while (client.TryNextRecord(out RecordRef record))
{
if (record.TryGet(out TradeMsg trade))
{
Process(trade); // `record` dies here — do not store it
}
}
if (await client.FillBufferAsync(ct) == 0)
{
break; // the gateway closed the stream cleanly
}
}
This allocates nothing per record. TryNextRecord reinterprets the record in place over the
read buffer, and FillBufferAsync fills that same buffer. The repository asserts this: a test
measures allocated bytes around a steady-state loop over the mock gateway's socket and requires
exactly zero.
The split into two calls is not an API wart. An async method cannot return a ref struct, so
there is no Task<RecordRef> and there never can be — upstream's single-call next_record() does
not port, and its fill_buf() / try_next_record() pair does. The compiler enforces the same
lifetime rule the loop above follows by hand: a RecordRef that survives an await is a
compile error (CS4007), not a runtime surprise.
FillBufferAsync returns 0 exactly once the stream ends, and every later call returns 0
without touching the socket. client.IsClosed distinguishes "no record yet" from "no record
ever" — TryNextRecord returns false for both.
The convenient loop
await foreach (var record in client.RecordsAsync(ct))
{
if (record.TryGet(out TradeMsg trade))
{
Process(trade);
}
}
RecordsAsync yields OwnedRecord, a heap copy. yield return carries the same restriction
await does, so a ref struct cannot leave an iterator at all — the copy is necessary, not a
convenience shortcut that could be optimised away. It costs two allocations per record, which
is roughly 110 bytes for a typical trade.
Use it when records are individually interesting and the rate is modest. Use the zero-copy loop
for full-depth MBO at market open. RecordsAsync is written in terms of FillBufferAsync and
TryNextRecord and does not bypass them, so the two surfaces cannot diverge.
More on the trade-off, and on what the compiler will and will not let you do with a RecordRef:
Zero-Copy and Allocation.
Records you will get whether you asked or not
Alongside the schema you subscribed to, the stream carries:
SystemMsg— heartbeats arrive here, as ordinary records carryingSystemCode.Heartbeat, not as control frames. Your loop sees them. Ignore them or log them, but do not be surprised.ErrorMsg— the gateway reporting a problem with a subscription, quoting theIdyou were given back fromSubscribeAsync.SymbolMappingMsg—instrument_idto symbol assignments, sent as they change. Feed these to aPitSymbolMap; see Symbol Resolution.
if (record.TryGet(out SystemMsg system) && system.Code == SystemCode.Heartbeat)
{
continue;
}
Timeouts and heartbeats
FillBufferAsync raises HeartbeatTimeoutException when nothing arrives within
EffectiveReadTimeout, and tears the connection down. That budget resolves in three steps:
ReadTimeout if you set it
HeartbeatInterval + 5s if you set an interval
35s otherwise
EffectiveReadTimeout is public so you can read back what actually applies — a derived setting
whose effect can only be discovered by waiting for it is not a setting.
Setting ReadTimeout shorter than the gateway's heartbeat interval will time out a healthy
connection. On a quiet feed a heartbeat is the only traffic guaranteed, so a budget below the
interval expires between heartbeats every time. Nothing in the client rejects that combination,
because HeartbeatInterval may be left unset — in which case the gateway picks its own interval
and the client has no way to learn what it is. If you shorten the read timeout, set the heartbeat
interval too.
Upstream has no equivalent setting; its timeout is always interval + 5s. That derivation is the
right default and is what EffectiveReadTimeout computes. It is a poor only option, because
the budget that matters is a property of the deployment: a quiet overnight replay and a busy
equities open are the same code reading very different streams.
Reconnecting
try
{
await ReadUntilClosed(client, ct);
}
catch (HeartbeatTimeoutException)
{
await client.ReconnectAsync(ct); // connect + authenticate, reusing the resolved address
await client.ResubscribeAsync(ct); // replay every subscription
await client.StartAsync(ct); // billing resumes here
}
ReconnectAsync closes whatever is open, reconnects to the already-resolved Endpoint, and
authenticates. It does not re-resolve DNS, matching upstream's reconnect() — which is why
Endpoint deliberately survives CloseAsync.
ResubscribeAsync replays client.Subscriptions with every Start cleared. That is the point
of the method: replaying an intraday-replay start after a reconnect would re-deliver hours of
records you have already seen and already paid for. The clearing happens in the retained list
before each line goes out, so a resubscribe that fails half way cannot leave a start behind for
the next attempt to replay.
Neither call is cancel-safe. A resubscribe that fails partway has sent some subscriptions and not
others, on a socket the gateway has stopped reading; the repair is another ReconnectAsync and
another ResubscribeAsync, which by then has no starts left to drop.
There is no automatic reconnection. Whether to retry, how often, and with what backoff are decisions about your deployment, not about the protocol — and a client that silently reconnected would silently resume billing.
Errors
Exception
└── LiveException
├── LiveConnectException the attempt failed — refused, unreachable
│ └── ConnectTimeoutException ConnectAsync outlived ConnectTimeout
├── AuthTimeoutException the handshake outlived AuthTimeout
├── DatabentoAuthenticationException the gateway rejected the credentials
├── HeartbeatTimeoutException the stream went silent past EffectiveReadTimeout
└── LiveProtocolException the gateway sent something that is not the protocol
DbnDecodeException comes from DatabentoDotNet.Dbn and is not a LiveException: what the
gateway sent was framed correctly and is not valid DBN. That is a codec problem, not a session
problem.
InvalidOperationException means the calls came in the wrong order — subscribing before
authenticating, reading before StartAsync. It is a programming error and is not part of the
LiveException hierarchy.
Troubleshooting maps the common messages to causes.
Threading
LiveClient is not thread-safe, and deliberately not made so. One connection is one
conversation with the gateway, and the record loop is a single reader by construction. A lock
around it would advertise a concurrency the protocol does not have.
Read on one thread. If you need to fan work out, hand off the decoded values — not the
RecordRef, which the compiler will not let you hand off anyway.
Testing against a mock
The repository ships MockLiveGateway, ported from upstream's own test harness rather than
reinvented. Point a client at it with the Gateway property:
await using var gateway = new MockLiveGateway("TEST.DATASET");
await using var client = new LiveClient
{
ApiKey = new ApiKey(MockLiveGateway.TestApiKey),
Dataset = gateway.Dataset,
Gateway = gateway.Address,
};
One caveat the repository states about its own harness, and worth repeating: the mock cannot confirm what it shares an author with. It and the client were written from the same reading of the protocol, so a misreading would sit in both and they would agree with each other. Only a real gateway settles that.
See also
- Timestamps and Prices — before you compute anything from a record
- Symbol Resolution —
instrument_idback to a ticker, live - Hosting and Dependency Injection — this client as a hosted service: registration,
IConfigurationbinding, bounded reconnection, health checks and metrics - Zero-Copy and Allocation — the guarantee and its rules
ROADMAP.md§4 — the design decisions behind this clientPORTING.md§4 — where this deviates fromdatabento-rs, and why