Slots & Epochs
Solana organizes time into discrete units called slots and epochs. Understanding this temporal structure is essential for grasping how Solana achieves its performance while maintaining decentralization.
Time Structure Overview
Text
┌─────────────────────────────────────────────────────────────────────────┐
│ EPOCH │
│ 432,000 slots │
│ (~2-3 days) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ Slot 0 │ │ Slot 1 │ ... │ Slot 431,999 │ │
│ │ (~400ms) │ │ (~400ms) │ │ (~400ms) │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
│ │
│ Leader: Validator A Leader: Validator B Leader: Validator X │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Within each slot:
┌─────────────────────────────────────────────────────────────────────────┐
│ SLOT │
│ ~400ms │
│ 64 ticks │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Tick 0 Tick 1 Tick 2 ... Tick 62 Tick 63 │
│ [ * ] [ * ] [ * ] [ * ] [ * ] │
│ │
│ Each tick = ~6.25ms │
│ Each tick = ~6,250 PoH hashes │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Slots
A slot is the basic unit of time in Solana, lasting approximately 400 milliseconds.
Slot Properties
TypeScript
interface Slot {
// Timing
targetDuration: number; // ~400ms
ticksPerSlot: number; // 64
hashesPerTick: number; // ~6,250
// Leader assignment
leader: PublicKey; // Who produces this slot
// Block content
entries: Entry[]; // Transactions + PoH hashes
parentSlot: number; // Previous slot
blockhash: string; // Hash of block contents
// State
status: "processed" | "confirmed" | "finalized" | "skipped";
}
Slot Lifecycle
Text
SLOT LIFECYCLE
══════════════
1. SCHEDULED
└── Leader knows it's their turn
└── Prepares to receive transactions
2. PRODUCING (Leader active)
├── Receives transactions via TPU
├── Executes transactions
├── Creates PoH entries
└── Broadcasts to validators
3. PROCESSED
└── Block exists but not yet voted on
└── May still be rolled back
4. CONFIRMED
└── Supermajority of stake voted
└── Very unlikely to roll back
5. FINALIZED
└── Maximum lockout reached
└── Cannot be rolled back
Special case:
SKIPPED
└── Leader failed to produce block
└── Timeout reached
└── Slot is empty
Querying Slot Information
TypeScript
import { Connection } from "@solana/web3.js";
const connection = new Connection("https://api.mainnet-beta.solana.com");
// Current slot
const currentSlot = await connection.getSlot();
console.log("Current slot:", currentSlot);
// Slot with specific commitment
const confirmedSlot = await connection.getSlot("confirmed");
const finalizedSlot = await connection.getSlot("finalized");
// Slot info
const slot = await connection.getSlot();
const leader = await connection.getSlotLeader();
console.log(`Slot ${slot} leader: ${leader}`);
// Block time (when slot was produced)
const blockTime = await connection.getBlockTime(slot);
console.log("Block time:", new Date(blockTime! * 1000));
// Get block data
const block = await connection.getBlock(slot, {
maxSupportedTransactionVersion: 0,
});
if (block) {
console.log("Transactions:", block.transactions.length);
console.log("Blockhash:", block.blockhash);
console.log("Parent slot:", block.parentSlot);
}
Skipped Slots
Slots can be skipped when leaders fail to produce blocks:
TypeScript
// Check slot status
async function getSlotStatus(
connection: Connection,
slot: number,
): Promise<"produced" | "skipped" | "future"> {
const currentSlot = await connection.getSlot();
if (slot > currentSlot) {
return "future";
}
try {
const block = await connection.getBlock(slot, {
maxSupportedTransactionVersion: 0,
});
return block ? "produced" : "skipped";
} catch {
return "skipped";
}
}
// Get skip rate for a range
async function calculateSkipRate(
connection: Connection,
startSlot: number,
count: number,
): Promise<number> {
let skipped = 0;
let total = 0;
for (let i = 0; i < count; i++) {
const slot = startSlot + i;
const status = await getSlotStatus(connection, slot);
if (status !== "future") {
total++;
if (status === "skipped") skipped++;
}
}
return skipped / total;
}
Epochs
An epoch is a longer time period containing 432,000 slots (~2-3 days).
Why Epochs Exist
Text
EPOCH PURPOSES
══════════════
1. LEADER SCHEDULE GENERATION
└── At epoch start, entire epoch's schedule is determined
└── Based on stake distribution at epoch boundary
└── Predictable 2-3 days in advance
2. STAKE ACTIVATION/DEACTIVATION
└── Stake changes take effect at epoch boundaries
└── Prevents rapid stake manipulation
└── Provides stability period
3. REWARDS DISTRIBUTION
└── Inflation rewards calculated per epoch
└── Vote credits tallied
└── Rewards distributed at epoch end
4. PROTOCOL FEATURES
└── Some features activate at epoch boundaries
└── Feature gates checked per epoch
Epoch Information
TypeScript
// Get epoch info
const epochInfo = await connection.getEpochInfo();
console.log("Epoch info:", {
epoch: epochInfo.epoch,
slotIndex: epochInfo.slotIndex, // Current slot within epoch
slotsInEpoch: epochInfo.slotsInEpoch, // 432,000
absoluteSlot: epochInfo.absoluteSlot, // Total slots since genesis
blockHeight: epochInfo.blockHeight, // Confirmed blocks
transactionCount: epochInfo.transactionCount,
});
// Calculate epoch progress
const epochProgress = epochInfo.slotIndex / epochInfo.slotsInEpoch;
console.log(
`Epoch ${epochInfo.epoch} is ${(epochProgress * 100).toFixed(2)}% complete`,
);
// Estimate time remaining in epoch
const slotsRemaining = epochInfo.slotsInEpoch - epochInfo.slotIndex;
const msRemaining = slotsRemaining * 400; // ~400ms per slot
const hoursRemaining = msRemaining / (1000 * 60 * 60);
console.log(`~${hoursRemaining.toFixed(1)} hours until next epoch`);
Epoch Schedule
TypeScript
// Get epoch schedule
const epochSchedule = await connection.getEpochSchedule();
console.log("Epoch schedule:", {
slotsPerEpoch: epochSchedule.slotsPerEpoch, // 432,000
leaderScheduleSlotOffset: epochSchedule.leaderScheduleSlotOffset,
warmup: epochSchedule.warmup, // Warmup period enabled
firstNormalEpoch: epochSchedule.firstNormalEpoch,
firstNormalSlot: epochSchedule.firstNormalSlot,
});
// Get first slot of an epoch
const epoch = epochInfo.epoch;
const firstSlot = epochSchedule.getFirstSlotInEpoch(epoch);
const lastSlot = epochSchedule.getLastSlotInEpoch(epoch);
console.log(`Epoch ${epoch}: slots ${firstSlot} - ${lastSlot}`);
Leader Schedule
The leader schedule determines which validator produces blocks for each slot.
Schedule Generation
Text
LEADER SCHEDULE ALGORITHM
═════════════════════════
Input:
├── Stake distribution at epoch boundary
├── Random seed from recent PoH
└── Slots per epoch (432,000)
Process:
1. Collect all validators with active stake
2. Weight each validator by stake amount
3. Use PoH-derived seed for randomness
4. Assign slots proportional to stake
Output:
├── 432,000 slot assignments
├── Each slot has exactly one leader
└── Published at epoch start
Example distribution (simplified):
Validator A (30% stake) → ~129,600 slots
Validator B (20% stake) → ~86,400 slots
Validator C (15% stake) → ~64,800 slots
...
Querying Leader Schedule
TypeScript
// Get leader schedule for current epoch
const leaderSchedule = await connection.getLeaderSchedule();
if (leaderSchedule) {
// leaderSchedule is a map: validator => [slot indices]
for (const [validator, slots] of Object.entries(leaderSchedule)) {
console.log(`${validator}: ${slots.length} slots`);
}
}
// Get upcoming slot leaders
const currentSlot = await connection.getSlot();
const leaders = await connection.getSlotLeaders(currentSlot, 10);
console.log("Next 10 slot leaders:");
leaders.forEach((leader, i) => {
console.log(` Slot ${currentSlot + i}: ${leader.toBase58()}`);
});
// Get a specific slot's leader
const slotLeader = await connection.getSlotLeader();
console.log("Current leader:", slotLeader.toBase58());
Leader Schedule Lookahead
TypeScript
// Leaders know their schedule ahead of time
async function getMyUpcomingSlots(
connection: Connection,
myValidatorId: PublicKey,
): Promise<number[]> {
const epochInfo = await connection.getEpochInfo();
const leaderSchedule = await connection.getLeaderSchedule();
if (!leaderSchedule) return [];
const mySlots = leaderSchedule[myValidatorId.toBase58()] || [];
// Filter to upcoming slots only
const upcomingSlots = mySlots.filter(
(slotIndex) => slotIndex > epochInfo.slotIndex,
);
// Convert to absolute slot numbers
const epochFirstSlot = epochInfo.absoluteSlot - epochInfo.slotIndex;
return upcomingSlots.map((index) => epochFirstSlot + index);
}
Slots, Blocks, and Confirmations
Important distinctions:
Text
TERMINOLOGY CLARITY
═══════════════════
SLOT ≠ BLOCK
Slot:
├── Time period (~400ms)
├── Always exists (time keeps moving)
└── May or may not contain a block
Block:
├── Data structure with transactions
├── Created by leader during slot
└── Can be missing if slot skipped
SLOT NUMBER vs BLOCK HEIGHT
Slot 100 → Block might not exist (skipped)
Slot 101 → Block exists (height 95)
Slot 102 → Block exists (height 96)
Slot 103 → Block might not exist (skipped)
Slot 104 → Block exists (height 97)
Block height only counts CONFIRMED blocks
Slot number counts ALL time periods
Confirmation Depth
TypeScript
// Calculate confirmation depth
async function getConfirmationDepth(
connection: Connection,
slot: number,
): Promise<{
slotDepth: number;
blockDepth: number;
status: string;
}> {
const currentSlot = await connection.getSlot("confirmed");
const slotDepth = currentSlot - slot;
// Block height at target slot
let targetBlock;
try {
targetBlock = await connection.getBlock(slot, {
maxSupportedTransactionVersion: 0,
});
} catch {
return {
slotDepth,
blockDepth: -1,
status: "skipped",
};
}
if (!targetBlock) {
return {
slotDepth,
blockDepth: -1,
status: "not_found",
};
}
const currentBlockHeight = await connection.getBlockHeight("confirmed");
const blockDepth = currentBlockHeight - (targetBlock.blockHeight || 0);
let status = "processed";
if (slotDepth >= 32) status = "finalized";
else if (slotDepth >= 1) status = "confirmed";
return { slotDepth, blockDepth, status };
}
Recent Blockhashes
Transactions require a recent blockhash for:
- Preventing replay attacks
- Providing transaction expiry
TypeScript
// Get recent blockhash
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash();
console.log("Blockhash:", blockhash);
console.log("Valid until block:", lastValidBlockHeight);
// Blockhash validity
/*
A blockhash is valid for approximately:
├── ~150 blocks (~60 seconds)
├── lastValidBlockHeight indicates exact expiry
└── After expiry, transaction is invalid
Timeline:
┌─────────────────────────────────────────────────────────────────┐
│ │
│ Block 1000 Block 1050 Block 1100 Block 1150 │
│ │ │ │ │ │
│ ▼ │ │ │ │
│ [Blockhash] │ │ │ │
│ obtained │ │ ▼ │
│ │ │ [Expired] │
│ ▼ ▼ │
│ [Still valid] [Getting stale] │
│ │
└─────────────────────────────────────────────────────────────────┘
*/
Handling Blockhash Expiry
TypeScript
async function sendTransactionWithRetry(
connection: Connection,
transaction: Transaction,
signers: Keypair[],
): Promise<string> {
const maxRetries = 3;
for (let attempt = 0; attempt < maxRetries; attempt++) {
// Get fresh blockhash each attempt
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash("confirmed");
transaction.recentBlockhash = blockhash;
transaction.feePayer = signers[0].publicKey;
// Sign with fresh blockhash
transaction.sign(...signers);
const signature = await connection.sendRawTransaction(
transaction.serialize(),
);
// Wait for confirmation
const confirmation = await connection.confirmTransaction({
signature,
blockhash,
lastValidBlockHeight,
});
if (confirmation.value.err === null) {
return signature;
}
console.log(`Attempt ${attempt + 1} failed, retrying...`);
}
throw new Error("Transaction failed after max retries");
}
Practical Applications
Timing-Sensitive Operations
TypeScript
// Wait for specific epoch
async function waitForEpoch(
connection: Connection,
targetEpoch: number,
): Promise<void> {
while (true) {
const epochInfo = await connection.getEpochInfo();
if (epochInfo.epoch >= targetEpoch) {
return;
}
const slotsToWait = epochInfo.slotsInEpoch - epochInfo.slotIndex;
const msToWait = slotsToWait * 400;
console.log(`Waiting ${msToWait / 1000}s for epoch ${targetEpoch}...`);
await new Promise((r) => setTimeout(r, Math.min(msToWait, 60000)));
}
}
// Wait for finalization
async function waitForFinalization(
connection: Connection,
signature: string,
targetSlot: number,
): Promise<void> {
const FINALIZATION_SLOTS = 32;
while (true) {
const finalizedSlot = await connection.getSlot("finalized");
if (finalizedSlot >= targetSlot) {
return;
}
const slotsRemaining = targetSlot - finalizedSlot + FINALIZATION_SLOTS;
console.log(`~${slotsRemaining} slots until finalized`);
await new Promise((r) => setTimeout(r, 5000));
}
}
Epoch-Based Scheduling
TypeScript
// Schedule action for next epoch
async function scheduleForNextEpoch(
connection: Connection,
action: () => Promise<void>,
): Promise<void> {
const epochInfo = await connection.getEpochInfo();
const targetEpoch = epochInfo.epoch + 1;
console.log(`Scheduling action for epoch ${targetEpoch}`);
// Calculate wait time
const slotsRemaining = epochInfo.slotsInEpoch - epochInfo.slotIndex;
const msToWait = slotsRemaining * 400;
// Wait with periodic checks
while (true) {
const current = await connection.getEpochInfo();
if (current.epoch >= targetEpoch) {
console.log(`Epoch ${targetEpoch} started, executing action`);
await action();
return;
}
await new Promise((r) => setTimeout(r, 10000));
}
}
Key Formulas
Text
Time Calculations
═════════════════
Slot duration:
slot_ms = 400ms (target)
Ticks per slot:
ticks = 64
Epoch duration:
epoch_slots = 432,000
epoch_ms = 432,000 × 400 = 172,800,000ms
epoch_hours ≈ 48 hours
epoch_days ≈ 2 days
Leader slots per epoch (for validator with X% stake):
leader_slots ≈ 432,000 × X%
Block height vs slot:
block_height ≤ slot_number
(equals only if no slots skipped)
Finalization time:
finalization_slots ≈ 32
finalization_ms ≈ 32 × 400 = 12,800ms ≈ 13 seconds
Key Takeaways
- Slots are ~400ms time periods; blocks are data created in slots
- Epochs are 432,000 slots (~2 days) used for schedule and rewards
- Leader schedule is determined by stake, generated per epoch
- Skipped slots occur when leaders fail; block height < slot number
- Blockhashes expire after ~150 blocks; always use fresh ones
Next: Leader Rotation - How Solana selects and rotates block producers.