Solana's getBlockProduction returns, for a slot range and optionally grouped by validator identity, a range object (firstSlot, lastSlot) and a byIdentity map whose entries are [leaderSlots, blocksProduced]. Skip rate for an identity is (leaderSlots - blocksProduced) / leaderSlots, and the range-wide rate is computed the same way from the totals. With no arguments the method reports the current epoch aggregated by validator identity, which makes it a natural fit for periodic validator monitoring. Because leaderSlots counts the slots a validator was scheduled to lead, a low produced-to-leader ratio means skipped blocks, not missing schedule entries; you can confirm the denominator against getLeaderSchedule. The counts are the queried node's view, can lag the tip, and may differ across nodes behind a load balancer, so treat a single observation as a sample and alert on sustained thresholds rather than one reading.
What getBlockProduction Returns and Why the Shape Matters
The Solana JSON-RPC method getBlockProduction returns a result object with two top-level fields: a range object containing firstSlot and lastSlot, and a byIdentity map. Each entry in byIdentity is keyed by a validator identity (a vote-account address) and holds a two-element array, conventionally read as [leaderSlots, blocksProduced]. The method reference at the Solana getBlockProduction reference documents this shape directly, and it is the authoritative description of the fields.
The two-element array is the whole measurement. leaderSlots is the number of slots in the queried range for which that identity was scheduled to be the leader; blocksProduced is how many of those slots actually yielded a block in the node's view. The difference is skipped slots attributable to that identity within the range. Because both numbers share the same denominator, you can compute a rate without any additional call.
This aggregate shape is what distinguishes getBlockProduction from methods that return enumerated data. If you need the actual list of produced slots for gap detection, that is a different method; if you need a point-in-time slot number, that is another. getBlockProduction is specifically the aggregate per-identity production and skip-rate measurement over a slot range.
- range.firstSlot and range.lastSlot bound the window the node actually evaluated.
- byIdentity maps vote-account identity to [leaderSlots, blocksProduced].
- Skip rate per identity = (leaderSlots - blocksProduced) / leaderSlots.
- Range-wide skip rate = (sum of leaderSlots - sum of blocksProduced) / sum of leaderSlots.
Scoping the Range with range, identity, and the Default Epoch Window
getBlockProduction accepts an optional configuration object. The range parameter takes { firstSlot, lastSlot } and restricts the measurement to that inclusive window. The identity parameter restricts the byIdentity map to a single validator identity, which is useful when you are monitoring one validator and do not want to parse the full map. Both are documented in the method reference at the Solana getBlockProduction reference.
When you call the method with no arguments, it reports the current epoch aggregated by validator identity. That default is the most convenient monitoring mode: one call gives you every identity's leaderSlots and blocksProduced for the epoch so far. The tradeoff is that an epoch in progress is a partial window, so the rate you compute is provisional and will move as the epoch completes.
If you need the slot timeline and leader schedule that frame the window, the Solana getEpochInfo and leader schedule page covers how epoch boundaries and the leader schedule are read over RPC. Pairing that context with getBlockProduction lets you reason about whether a range is complete or still open.
- range: { firstSlot, lastSlot } for a bounded historical or recent window.
- identity: restrict the map to one vote-account address.
- No arguments: current epoch, aggregated by identity (partial window).
- Confirm the window against epoch boundaries before treating a rate as final.
Computing Skip Rate Correctly from leaderSlots and blocksProduced
Skip rate is a ratio, not a count. For a single identity, divide the difference between leaderSlots and blocksProduced by leaderSlots. For the whole range, sum leaderSlots across all identities, sum blocksProduced across all identities, and apply the same formula to the totals. Summing the per-identity rates and averaging them is a different (and usually misleading) statistic because it weights a validator with few leader slots the same as one with many.
Guard the denominator. If leaderSlots is zero for an identity, the rate is undefined, not zero; skip that identity or report it as no scheduled slots in the window. This happens naturally for validators with no leader slots in a short range.
Because leaderSlots is the scheduled count, a low produced-to-leader ratio means the validator did not produce blocks in slots it was scheduled to lead. It does not mean the schedule was wrong. You can validate the denominator independently against getLeaderSchedule, documented at https://solana.com/docs/rpc/http/getleaderschedule, which returns the leader schedule by identity for an epoch.
- Per identity: (leaderSlots - blocksProduced) / leaderSlots.
- Range-wide: (Σ leaderSlots - Σ blocksProduced) / Σ leaderSlots.
- Do not average per-identity rates; weight by leaderSlots instead.
- Treat leaderSlots = 0 as undefined, not as a perfect score.
Validating the Denominator Against getLeaderSchedule
The most common interpretation error is assuming leaderSlots reflects something other than scheduled leadership. It does not: it is the count of slots in the range for which the identity was scheduled to lead. To confirm this, fetch the leader schedule for the relevant epoch with getLeaderSchedule and count the slots assigned to each identity, then compare that count to the leaderSlots value getBlockProduction reported for the same window.
When the two agree, your skip-rate denominator is trustworthy and any shortfall in blocksProduced is genuinely skipped production. When they disagree, the likely causes are a range that does not align with the epoch you fetched the schedule for, or a node whose view of the range differs from the schedule you queried. Aligning the range to epoch boundaries removes most of this ambiguity.
For a deeper treatment of epoch boundaries and how the schedule is read, see Solana getEpochInfo and leader schedule.
- getLeaderSchedule returns scheduled leadership by identity for an epoch.
- Compare its per-identity slot counts to leaderSlots for the same window.
- Mismatches usually indicate a range/epoch misalignment, not a broken method.
Runnable Node.js Example: Current-Epoch Skip Rate by Identity
The following example calls getBlockProduction with no arguments to get the current epoch aggregated by identity, computes each identity's skip rate, sorts the list by rate descending, and flags identities above a threshold. It uses the global fetch available in modern Node.js and a configurable RPC endpoint.
Set the endpoint to your own Solana RPC URL. The threshold is a parameter you choose; the script does not assume any particular value is healthy. Run it periodically and compare successive outputs rather than acting on a single run.
// measure-skip-rate.mjs
const RPC_URL = process.env.SOLANA_RPC_URL || "https://your-solana-rpc-endpoint";
const SKIP_THRESHOLD = Number(process.env.SKIP_THRESHOLD || 0.05); // 5%
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;
}
const result = await rpc("getBlockProduction");
const { range, byIdentity } = result;
const rows = Object.entries(byIdentity).map(([identity, pair]) => {
const [leaderSlots, blocksProduced] = pair;
const skipRate = leaderSlots > 0 ? (leaderSlots - blocksProduced) / leaderSlots : null;
return { identity, leaderSlots, blocksProduced, skipRate };
});
rows.sort((a, b) => (b.skipRate ?? -1) - (a.skipRate ?? -1));
console.log(`Range: ${range.firstSlot}..${range.lastSlot}`);
for (const r of rows) {
const rate = r.skipRate === null ? "n/a" : (r.skipRate * 100).toFixed(2) + "%";
const flag = r.skipRate !== null && r.skipRate > SKIP_THRESHOLD ? " <-- above threshold" : "";
console.log(`${r.identity} leader=${r.leaderSlots} produced=${r.blocksProduced} skip=${rate}${flag}`);
}
const totalLeader = rows.reduce((s, r) => s + r.leaderSlots, 0);
const totalProduced = rows.reduce((s, r) => s + r.blocksProduced, 0);
const rangeSkip = totalLeader > 0 ? (totalLeader - totalProduced) / totalLeader : null;
console.log(`Range-wide skip rate: ${rangeSkip === null ? "n/a" : (rangeSkip * 100).toFixed(2) + "%"}`);Runnable curl Example: Bounded Range and Single Identity
For a bounded historical window, pass the range parameter explicitly. The example below queries a specific slot range and prints the raw JSON so you can inspect range.firstSlot and range.lastSlot alongside the byIdentity map.
To narrow the response to one validator, add the identity parameter with the vote-account address. This is convenient when you already know which identity you are monitoring and do not want to filter a large map client-side.
# Bounded range
curl -s https://your-solana-rpc-endpoint -X POST -H "content-type: application/json" -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlockProduction",
"params": [
{ "range": { "firstSlot": 250000000, "lastSlot": 250000431 } }
]
}'
# Single identity
curl -s https://your-solana-rpc-endpoint -X POST -H "content-type: application/json" -d '{
"jsonrpc": "2.0",
"id": 1,
"method": "getBlockProduction",
"params": [
{ "identity": "YourVoteAccountIdentityHere" }
]
}'Choosing Between getBlockProduction, getBlocks, getSlot, and getEpochInfo
These methods answer different questions and are not substitutes. getBlockProduction gives an aggregate per-identity production and skip-rate measurement over a range. getBlocks returns an enumerated list of confirmed blocks in a range, which is what you want for gap detection and indexer reconciliation; the Solana getBlocks and skipped-slot gap detection page covers that workflow. getSlot and getEpochInfo give point-in-time timeline information rather than production counts.
A practical division of labor: use getBlockProduction for validator-level monitoring and skip-rate alerting, getBlocks when you need to know exactly which slots are missing for a consumer, and getSlot or getEpochInfo when you need the current position in the timeline. If you are building event-driven monitoring instead of polling, the Solana slotSubscribe and blockSubscribe mechanics page explains the subscription alternatives.
- getBlockProduction: aggregate production and skip rate per identity.
- getBlocks: enumerated confirmed blocks for gap detection.
- getSlot / getEpochInfo: point-in-time timeline position.
- Subscriptions: push-based alternatives for event-driven monitoring.
Operational Meaning of a Rising Skip Rate
Skip rate matters because a validator that produces late or not at all in its scheduled slots degrades confirmation latency for everyone downstream. When a leader skips, the network waits for the next scheduled leader, which can add latency to transaction confirmation and to any consumer that depends on block cadence. A rising skip rate for a specific identity is therefore a signal worth investigating, whether the cause is local to that validator or a broader network condition.
Alert on a sustained threshold rather than a single observation. Because the counts are the queried node's view and can lag the tip, one reading can be noisy. A useful pattern is to compute the rate over a rolling window, compare it to a threshold you have chosen, and require the condition to persist across several windows before paging. This reduces false positives from transient tip lag or a partially completed epoch.
For general endpoint monitoring patterns that complement this method, see Monitoring RPC endpoints.
- Skipped slots delay confirmation for downstream consumers.
- Use a rolling window and a persistence requirement before alerting.
- Distinguish a single noisy reading from a sustained trend.
Reproducible Results Table for Your Own Endpoint or Validator
The numbers you get depend on the endpoint you query and the window you choose, so the honest way to present them is as a table you fill yourself. Run the Node.js example or the curl calls against your endpoint, record the range and the totals, and repeat across endpoints or over time. Do not treat any single row as a benchmark; treat the table as your own measurement record.
Fill one row per endpoint or per validator per window. Keeping the range columns explicit makes it possible to compare rows fairly and to spot when a difference is explained by a different window rather than by endpoint behavior.
- Endpoint / identity: which RPC URL or vote-account address you queried.
- Range firstSlot / lastSlot: the window the node evaluated.
- Σ leaderSlots and Σ blocksProduced: totals for the window.
- Range-wide skip rate: computed from the totals, not averaged.
- Per-identity skip rate: for the validator you are tracking.
- Observed at (timestamp): when you ran the query.
- Notes: epoch complete or in progress, endpoint behind a load balancer, etc.
Limitations, Tradeoffs, and Honest Caveats
The reported counts are the queried node's view. They can lag the tip, and different nodes behind a load balancer can return different values for the same nominal query. If you sample through a load balancer, treat each response as one node's perspective and consider pinning monitoring queries to a specific node when consistency matters.
The identity keys are vote-account addresses. Mapping them to human-readable validator names, operators, or your own inventory is work you must do yourself; the RPC method does not provide that mapping. Without a mapping, a skip-rate table is a list of opaque addresses.
An epoch in progress is a partial window, so its rate is not final and will move as the epoch completes. If you need a stable number, wait for the epoch to close or use a bounded historical range that is fully in the past.
Finally, skipped slots are normal protocol behavior, not necessarily a fault. A nonzero skip rate is not by itself evidence of misconfiguration. Interpret it alongside other signals, and be cautious about attributing a skip to a specific cause without corroborating evidence.
- Counts are node-local and can lag the tip or differ across nodes.
- Identity keys are vote-account addresses; mapping is your responsibility.
- In-progress epochs yield provisional rates.
- Skipped slots are normal; a nonzero rate is not proof of a fault.
Troubleshooting Common getBlockProduction Problems
If the byIdentity map is empty, the most likely cause is a range with no scheduled leadership or a window that does not overlap the epoch you intended. Widen the range or drop the range parameter to fall back to the current epoch. If a single identity is missing from the map, that identity may simply have had no leader slots in the window; confirm against getLeaderSchedule.
If the range object's firstSlot and lastSlot do not match what you requested, the node may have clamped or adjusted the window. Always read the returned range rather than assuming your request was honored verbatim. If results differ between two calls to the same endpoint, tip lag or a partially completed epoch is a common explanation; re-run after the epoch closes.
If you are comparing endpoints and seeing disagreement, remember that each node has its own view. Pin the query to one node, or accept that cross-endpoint comparison is approximate. For endpoint selection and connectivity context, see the Solana API guide (RPC Assistant) and the Solana network page.
- Empty map: widen the range or use the default epoch window.
- Missing identity: verify it had leader slots in the window.
- Unexpected range: read the returned range, do not assume your request was honored.
- Cross-endpoint disagreement: nodes have independent views.
Next Steps for Validator and Endpoint Monitoring
Start by running the current-epoch example against your endpoint and recording a baseline in the results table. Then schedule it periodically, compute a rolling skip rate, and alert only on sustained threshold breaches. Pair getBlockProduction with getLeaderSchedule to keep the denominator honest, and with getBlocks when you need enumerated gaps for a consumer.
If you are choosing or scaling RPC access for this kind of monitoring, review RPC pricing and the API service options, and browse the OnFinality Learn hub for adjacent Solana reliability topics. The goal is a repeatable measurement you own, not a one-off number.
- Record a baseline, then schedule periodic measurement.
- Alert on sustained rolling thresholds, not single readings.
- Pair with getLeaderSchedule and getBlocks for full coverage.