- A Yellowstone gRPC transaction update carries the
slot, but not the block time. Block time arrives only on a block-metadata update. - Subscribe to
transactions,blocks_meta, andslotson one stream, then join onslot: buffer transactions by slot, and stamp them with the block time when that slot’sblock_metaarrives. - This is far cheaper than the alternatives — a second full-block stream re-sends every transaction, and a per-block
getBlockTimeadds an RPC round trip to every slot. - Run at
confirmedcommitment, drop dead and skipped slots so the buffer can’t leak, and answer the server’s pings.
The problem
If you stream Solana transactions over Yellowstone gRPC Geyser, each transaction update tells you whichslot it landed in, but it does not include the block’s timestamp. The transaction message simply has no block-time field — this is by design.
That leaves a common question: how do you attach a block time to every transaction without either committing a second stream to block data or calling getBlockTime once per block? Both work, and both are expensive. There is a cheaper way that uses a single stream and no extra RPC calls.
The approach
A singleSubscribeRequest can carry several filters at once, and the server multiplexes all of their updates onto one stream. You subscribe to three filters:
transactions— the transactions you care about. Each update carries theslotand the transaction, but no block time.blocks_meta— one small message per block that does carryblock_time, along withblockhash,block_height,parent_slot, andexecuted_transaction_count. It does not carry the transactions, so it stays tiny.slots— the slot lifecycle, so you can tell when a slot is dead or skipped and will never produce a block.
slot. A block-metadata update for slot N normally arrives after all of slot N’s transactions, because the validator emits block metadata when the slot finishes replaying. That ordering is convenient, but it is best-effort rather than a contractual guarantee — so instead of assuming it, correlate by slot in both directions:
- Keep two maps keyed by
slot: transactions waiting for their block time, and block metadata waiting for late transactions. - When a transaction arrives, stamp and emit it if its
block_metais already cached; otherwise buffer it. - When the
block_metafor slot N arrives, cache it and flush every transaction buffered for slot N. - Evict a slot from both maps once it goes
SLOT_DEADor the confirmed tip passes it (a skipped slot).
Why not the alternatives
| Approach | Latency | Bandwidth and cost | When to use it |
|---|---|---|---|
transactions + blocks_meta join (this guide) | Lowest — block time trails the transactions by less than a slot | Minimal — one small block_meta per block, plus your filtered transactions | Real-time pipelines that need a block time on each transaction |
Full blocks filter | Atomic, but waits for server-side block reconstruction | Heavy — full transaction bodies are re-sent, megabytes per busy block | You need whole blocks (transactions, accounts, and entries) in one message |
getBlock or getBlockTime per block | A synchronous round trip per slot | Extra RPC calls, credits, and rate-limit exposure | History older than the replay window, or one-off lookups |
blocks filter is the only update type that bundles transactions and block time atomically, but it re-transports every transaction body, so it wastes bandwidth if you already receive transactions from the transactions filter. A per-block getBlockTime call defeats the purpose of a push stream. For historical data, getBlock returns the block time together with the transactions in a single call — see Solana: optimize your getBlock performance.
Prerequisites
Geyser is a paid add-on available from the Growth plan, and it runs on Solana mainnet only. See Yellowstone gRPC Geyser plugin to enable it on your node.
x-token metadata header. Take the Geyser endpoint and token from your node’s details on Chainstack.
Build the subscription
Set the commitment toconfirmed, add the three filters, and exclude votes and failed transactions to cut the volume you process:
vote, failed, account_include, account_exclude, account_required, or signature constraint returns:
vote = False already satisfies this.
Join transactions to block time
Open the stream and run the buffer-and-flush loop. Buffer each transaction byslot, and flush when that slot’s block_meta arrives:
block_time and blockhash of the block it landed in, from a single stream and with no extra RPC calls.
Getting it right
Choose the right commitment
Run the association atconfirmed. At processed, both the transaction and its block metadata can be delivered on a fork that the cluster later abandons, so the pairing you compute may belong to a dropped block. At confirmed, the slot has been voted on by a supermajority of stake, which makes the pairing reliable. Use processed only when you need the lowest possible latency and can treat associations as provisional.
Drop dead and skipped slots
A buffered slot that never receives ablock_meta would otherwise sit in memory forever. Two cases cause this:
- Dead slots — the
slotsfilter delivers aSLOT_DEADstatus. Discard anything buffered or cached for that slot. - Skipped slots — no block is produced and no status pinpoints the gap, so evict the slot from both maps once the confirmed tip moves past it by a safe margin (
slot + SKIP_MARGIN < hi_confirmedin the loop above).
Completeness and executed_transaction_count
block_meta.executed_transaction_count is the validator’s total count of executed transactions for the block, and it includes vote and failed transactions. On Solana, vote transactions alone are roughly three-quarters of block activity, so if you stream with vote = False (or filter any other way), your received count will be far lower than executed_transaction_count. Use the count as a completeness check only against an unfiltered set. In practice, the arrival of block_meta is itself the signal that the slot is sealed.
The
entries_count field is present on block metadata but is unreliable on standard validators, so do not use it as a completeness signal.Harden and recover
Answer pings and lift the message cap
The server sends periodic pings; reply with aSubscribeRequestPing on the same stream so that an idle load balancer does not drop the connection. That is why the example keeps the send half-stream open with a queue. Raise the gRPC receive limit above its 4 MB default to avoid truncation on busy slots. Chainstack Solana nodes have Jito ShredStream enabled by default, which improves tail latency and consistency for Geyser streaming.
Recover after a disconnect with from_slot
Track the last slot you cleanly flushed. On reconnect, setfrom_slot to that slot to replay the gap, then de-duplicate the overlap by signature and slot. The replay buffer is a reconnection-recovery mechanism, not a historical backfill: on Global Nodes it holds approximately the last 100 slots (around a minute). Requesting an older slot returns:
getBlock, which returns the block time together with the transactions in one call. See Replay depth with from_slot.
Verify the join
You can confirm the block time you stamped over gRPC is correct by cross-checking a captured slot against JSON-RPC. CallgetBlock for the slot and compare:
getBlock’s blockTime equals the block_time you received on block_meta, its blockhash matches, and the length of its signatures list equals executed_transaction_count — which confirms the count includes votes and failed transactions.
Caveats
block_timeis an estimate. Solana derives it from a stake-weighted mean of validator-submitted timestamps, with capped drift, so treat it as accurate to the second rather than as an exact or security-critical clock. See Solana: understanding block time.- The Alpenglow consensus upgrade is on the horizon. It collapses the commitment levels and changes how the cluster clock is derived — and it moves votes off-chain, which would reshape the
executed_transaction_countmath above. The stream mechanism in this guide — block time onblock_meta, joined byslot— is expected to carry over, but revisit the block-time derivation when Alpenglow reaches mainnet.
Full example
Complete join.py
Complete join.py
Related
- Yellowstone gRPC Geyser plugin — enable the add-on, limits, and replay depth
- Solana: understanding block time — how block time is derived and its drift rules
- Solana: understanding the difference between blocks and slots
- Solana: optimize your getBlock performance
- Solana: listening to programs using Geyser and Yellowstone gRPC (Node.js) — the Node.js counterpart
- Solana Geyser Python tutorial and the Yellowstone repository