Solana slot timelines are read by combining getEpochInfo, getLeaderSchedule, getSlot, and getBlockTime. getEpochInfo returns absoluteSlot, blockHeight, epoch, slotIndex, slotsInEpoch, and transactionCount; getLeaderSchedule maps slot indexes to validator identities for an epoch. absoluteSlot counts every slot since genesis, while blockHeight counts only slots that produced blocks, so subtracting them estimates skipped slots in a window. Epoch arithmetic is safe only when slotsInEpoch is read from the cluster, not hard-coded. Slot-to-time mapping requires getBlockTime, which returns null for slots without blocks, so timelines must carry the nearest known timestamp. Epoch boundaries are the riskiest read window because leader schedule, epoch info, and slot range roll over together.
Slot Timeline Primitives and Their RPC Sources
A Solana slot timeline answers four questions: which epoch a slot belongs to, which validator is scheduled to lead it, when the epoch ends, and what wall-clock instant the slot maps to. The official Solana RPC method references for getEpochInfo and getLeaderSchedule define the fields and semantics used throughout this guide. The JSON-RPC 2.0 specification governs request and response framing, while the Ethereum JSON-RPC specification is not applicable to Solana method semantics; Solana uses its own method namespace.
getEpochInfo returns absoluteSlot, blockHeight, epoch, slotIndex, slotsInEpoch, and transactionCount. getLeaderSchedule returns a map of validator identity to an array of slot indexes for a given epoch. getSlot returns the current slot, and getBlockTime returns a Unix timestamp for a slot that produced a block. Together these methods let you reconstruct a timeline without relying on undocumented assumptions.
The distinction between absoluteSlot and blockHeight is the most common source of silent errors. absoluteSlot counts every slot since genesis whether or not it produced a block. blockHeight counts only slots that produced blocks. Subtracting blockHeight from absoluteSlot inside a window estimates skipped slots in that window, but confusing the two shifts every epoch calculation. For epoch arithmetic, use absoluteSlot. For block-production density, use blockHeight.
- getEpochInfo: absoluteSlot, blockHeight, epoch, slotIndex, slotsInEpoch, transactionCount.
- getLeaderSchedule: validator identity to slot index array for an epoch.
- getSlot: current slot number.
- getBlockTime: Unix timestamp for a slot that produced a block, null otherwise.
Epoch Arithmetic That Is Safe to Rely On
The relationship epoch = absoluteSlot / slotsInEpoch and slotIndex = absoluteSlot % slotsInEpoch is safe to rely on when slotsInEpoch is read from getEpochInfo. The parts that are not constant in practice are slotsInEpoch and the epoch schedule itself. These are runtime parameters, and the network has changed slot timing before. Code that hard-codes 432000 slots per epoch or 400 ms per slot will drift when the cluster parameters change.
Read slotsInEpoch from getEpochInfo on every timeline reconstruction rather than caching it across long windows. If you cache, record the epoch and slotsInEpoch pair and invalidate on epoch change. The epoch schedule is a cluster property, documented and varying by cluster, so a value observed on one cluster should not be assumed for another.
For production readers, the safe pattern is to fetch getEpochInfo, compute epoch and slotIndex from absoluteSlot and slotsInEpoch, and then fetch getLeaderSchedule for that epoch. If the epoch changes between the two calls, re-fetch getEpochInfo and repeat. This avoids pairing an old epoch number with a new slot index.
- Use absoluteSlot for epoch math, not blockHeight.
- Read slotsInEpoch from getEpochInfo; do not hard-code it.
- Treat epoch schedule as a cluster property, documented and varying by cluster.
- Re-fetch getEpochInfo after any slot request that crossed an epoch boundary.
Leader Schedule Forms and Historical Availability
getLeaderSchedule has two practical forms: a call without an epoch argument returns the schedule for the current epoch, and a call with an epoch argument returns the schedule for that specific epoch. The response maps validator identity to an array of slot indexes. The slot indexes are relative to the epoch, so slot index 0 is the first slot of that epoch, not genesis.
A historical schedule for an old epoch may be unavailable from a pruned node. This must be treated as an expected boundary rather than an error. Providers document different retention windows, and retention is a provider property. If you need historical schedules, verify retention with your provider before designing a backfill job.
The relationship between slot indexes and leader identity is direct: for a given epoch, find the validator whose array contains the slot index. If no validator array contains the index, the schedule you fetched does not cover the epoch you think it does, which is the reconciliation check described later.
- No epoch argument: current epoch schedule.
- Epoch argument: schedule for that epoch, if retained.
- Slot indexes are epoch-relative, not genesis-relative.
- Historical schedule availability is a provider retention property, documented and varying by provider.
Mapping a Slot to Wall-Clock Time
getBlockTime(slot) returns a Unix timestamp for a slot that produced a block and null for one that did not. A timeline reconstruction must handle nulls by carrying the nearest known timestamp and recording the interpolation rather than dropping the slot. Dropping null slots makes the timeline look denser than it is and hides skipped slots.
The correct pattern is to walk the slot range, call getBlockTime for each slot, and when the result is null, carry the last known timestamp forward and mark the entry as interpolated. When a later slot returns a timestamp, you can optionally backfill the interpolated entries with a linear estimate, but keep the interpolation flag so downstream consumers know the value is derived.
For coarse timelines, you can sample getBlockTime at intervals and interpolate between samples. For precise timelines, call getBlockTime per slot. The tradeoff is request volume versus precision. Both approaches must handle nulls explicitly.
- getBlockTime returns null for slots without blocks.
- Carry the nearest known timestamp and mark interpolated entries.
- Do not drop null slots; dropping hides skipped slots.
- Sampling reduces request volume but increases interpolation error.
Epoch Boundaries as the Risky Read Window
The epoch boundary is the riskiest minute for production readers. The leader schedule, the epoch info, and the slot range all roll over together. A request issued across the boundary can pair an old epoch number with a new slot index, producing a timeline that looks valid but is internally inconsistent.
A consistent read should re-fetch getEpochInfo after any slot request that crossed a boundary. Detect the boundary by comparing the epoch field before and after the slot request. If the epoch changed, discard the intermediate result and repeat the read. This is cheap compared to debugging a misattributed leader.
For batch jobs, pin the epoch at the start of the batch and verify it at the end. If the epoch changed mid-batch, split the batch at the boundary and re-run the second half with the new epoch. This keeps each batch internally consistent.
- Leader schedule, epoch info, and slot range roll over together.
- Re-fetch getEpochInfo after any slot request that crossed a boundary.
- Pin the epoch at batch start and verify at batch end.
- Split batches at the boundary rather than mixing epochs.
Reconciliation Check: Schedule Coverage Versus slotsInEpoch
A reconciliation check confirms the schedule you fetched covers the epoch you think it does. Sum the leaders' scheduled slot counts for an epoch and compare with slotsInEpoch from getEpochInfo. If the sum equals slotsInEpoch, the schedule covers the full epoch. If the sum is less, the schedule is partial or the epoch is still in progress.
For a completed epoch, a sum less than slotsInEpoch indicates missing schedule data, which may be a retention boundary. For an in-progress epoch, a sum less than slotsInEpoch is expected because the remaining slots have not been scheduled yet or the schedule is being served incrementally.
Record the reconciliation result alongside the timeline. A timeline with a failed reconciliation should be flagged rather than silently consumed. This is especially important for financial or accounting workflows where a misattributed leader changes reward attribution.
- Sum scheduled slot counts per epoch and compare with slotsInEpoch.
- Equal sum: full coverage. Less: partial or in-progress.
- Flag failed reconciliation rather than consuming silently.
- Retention boundaries can cause partial schedules for old epochs.
Runnable Node.js Example: Epoch, Leader, and End Time Table
The following Node.js snippet reads getEpochInfo and getLeaderSchedule, prints slot index, epoch, the owning validator for a chosen slot, and the epoch's end time as a table. It uses the global fetch API available in Node.js 18 and later. Replace the RPC endpoint with your provider's endpoint.
The snippet computes the epoch end slot as (epoch + 1) * slotsInEpoch - 1 and estimates the end time by sampling getBlockTime at the current slot and extrapolating using the observed slot duration. The extrapolation is an estimate, not a measured value, and should be labeled as such in any output.
const RPC_URL = process.env.SOLANA_RPC_URL || 'https://api.mainnet-beta.solana.com';
async function rpc(method, params = []) {
const res = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(JSON.stringify(json.error));
return json.result;
}
async function main() {
const epochInfo = await rpc('getEpochInfo');
const { absoluteSlot, blockHeight, epoch, slotIndex, slotsInEpoch } = epochInfo;
const schedule = await rpc('getLeaderSchedule', [epoch]);
const leaderForSlot = (targetSlotIndex) => {
for (const [validator, slots] of Object.entries(schedule)) {
if (slots.includes(targetSlotIndex)) return validator;
}
return null;
};
const chosenSlotIndex = slotIndex;
const owner = leaderForSlot(chosenSlotIndex);
const epochEndSlot = (epoch + 1) * slotsInEpoch - 1;
const currentBlockTime = await rpc('getBlockTime', [absoluteSlot]);
const sampleSlot = Math.max(0, absoluteSlot - 100);
const sampleBlockTime = await rpc('getBlockTime', [sampleSlot]);
let estimatedEndTime = null;
if (currentBlockTime && sampleBlockTime && absoluteSlot > sampleSlot) {
const msPerSlot = ((currentBlockTime - sampleBlockTime) * 1000) / (absoluteSlot - sampleSlot);
estimatedEndTime = new Date((currentBlockTime * 1000) + (epochEndSlot - absoluteSlot) * msPerSlot);
}
const rows = [
{ field: 'absoluteSlot', value: absoluteSlot },
{ field: 'blockHeight', value: blockHeight },
{ field: 'epoch', value: epoch },
{ field: 'slotIndex', value: slotIndex },
{ field: 'slotsInEpoch', value: slotsInEpoch },
{ field: 'chosenSlotIndex', value: chosenSlotIndex },
{ field: 'leader', value: owner || 'not found in schedule' },
{ field: 'epochEndSlot', value: epochEndSlot },
{ field: 'estimatedEndTime', value: estimatedEndTime ? estimatedEndTime.toISOString() : 'unavailable' }
];
console.table(rows);
}
main().catch((err) => { console.error(err); process.exit(1); });Results Table: Measuring Against Your Own Endpoint
Because slot timing, epoch length, and schedule retention are cluster and provider properties, documented and varying by cluster, you should measure against your own endpoint rather than relying on published numbers. The table below is a template to fill with your own observations. Do not treat any row as a universal constant.
Run the Node.js snippet above at several times of day and across at least one epoch boundary. Record the observed slotsInEpoch, the observed slot duration derived from getBlockTime samples, the schedule coverage sum, and whether getLeaderSchedule returned a schedule for an old epoch. This gives you a provider-specific baseline.
If your provider returns a partial schedule for an old epoch, record the oldest epoch for which a full schedule is available. That is your retention boundary. Design backfill jobs to stay within it or to fall back to a different data source.
- Observed slotsInEpoch: fill from getEpochInfo.
- Observed slot duration: derive from getBlockTime samples.
- Schedule coverage sum: sum leader slot counts and compare with slotsInEpoch.
- Oldest fully covered epoch: your retention boundary.
- Boundary behavior: whether epoch changed mid-read and required re-fetch.
Limitations and Tradeoffs
Slot timing, epoch length, and schedule retention are cluster and provider properties, documented and varying by cluster. Any timeline built on hard-coded constants will drift. The safe approach is to read parameters from the cluster and record them alongside the timeline.
getBlockTime returns null for slots without blocks, so precise timelines require per-slot calls and explicit null handling. Sampling reduces request volume but increases interpolation error. There is no free lunch; choose based on whether your workflow needs precision or trend.
Historical leader schedules may be unavailable from pruned nodes. This is an expected boundary, not an error. If your workflow requires historical schedules, verify retention with your provider and design a fallback. For related reading on commitment and confirmation, see Solana commitment levels and transaction confirmation.
- Hard-coded constants drift; read from the cluster.
- Null block times require explicit handling.
- Sampling trades precision for request volume.
- Historical schedule retention is a provider property.
Troubleshooting Common Timeline Errors
The most common error is confusing absoluteSlot with blockHeight. If your epoch calculation is off by a large margin, check which field you used. Epoch math requires absoluteSlot. Block production density requires blockHeight.
The second most common error is hard-coding slotsInEpoch. If your slotIndex is wrong near an epoch boundary, verify that slotsInEpoch came from getEpochInfo for the same epoch as the slot you are inspecting. A mismatch here produces a plausible but wrong slotIndex.
The third is dropping null block times. If your timeline shows no gaps but the cluster has skipped slots, you are dropping nulls. Carry the nearest known timestamp and mark interpolated entries. For related pagination and parsing patterns, see Solana getSignaturesForAddress pagination and Solana versioned transactions and getBlock parsing.
- Wrong epoch math: check absoluteSlot versus blockHeight.
- Wrong slotIndex: verify slotsInEpoch matches the epoch.
- Missing gaps: you are dropping null block times.
- Misattributed leader: re-fetch getEpochInfo after boundary crossing.