Missed slots
Analysis of missed slots on Ethereum mainnet.
A missed slot is a slot where the assigned proposer failed to produce a block. The chain skips these slots and continues. Typical miss rate is ~0.3%.
Definition: slot exists in proposer_duty AND no block exists in canonical_beacon_block
Show code
display_sql("block_production_timeline", target_date)
View query
Show code
df = load_parquet("block_production_timeline", target_date)
# Identify missed slots: ClickHouse LEFT JOIN returns epoch date (1970-01-01) instead of NULL
# for non-matching rows, so we detect missed slots by checking for the epoch timestamp
epoch = pd.Timestamp("1970-01-01")
df["is_missed"] = df["block_first_seen"] == epoch
total_slots = len(df)
missed_slots = df["is_missed"].sum()
produced_slots = total_slots - missed_slots
miss_rate = missed_slots / total_slots * 100
print(f"Total slots: {total_slots:,}")
print(f"Blocks produced: {produced_slots:,} ({produced_slots/total_slots*100:.2f}%)")
print(f"Missed slots: {missed_slots:,} ({miss_rate:.2f}%)")
Missed slots by proposer entity¶
Which staking entities had the most missed slots? Note that larger entities have more assigned slots, so absolute counts don't reflect performance.
Show code
# Missed slots by entity
df_missed = df[df["is_missed"]].copy()
if len(df_missed) > 0:
# Fill empty entities
df_missed["proposer_entity"] = df_missed["proposer_entity"].fillna("unknown").replace("", "unknown")
entity_misses = df_missed.groupby("proposer_entity").size().reset_index(name="missed_count")
entity_misses = entity_misses.sort_values("missed_count", ascending=True)
fig = go.Figure()
fig.add_trace(go.Bar(
y=entity_misses["proposer_entity"],
x=entity_misses["missed_count"],
orientation="h",
marker_color="#e74c3c",
text=entity_misses["missed_count"],
textposition="outside",
))
fig.update_layout(
margin=dict(l=150, r=50, t=30, b=60),
xaxis=dict(title="Missed slots"),
yaxis=dict(title=""),
height=max(300, len(entity_misses) * 25 + 100),
)
fig.show(config={"responsive": True})
else:
print("No missed slots today.")
Entity miss rates¶
Normalized view: what percentage of each entity's assigned slots were missed? This accounts for entity size.
Show code
if len(df_missed) > 0:
# Calculate miss rate per entity
df["proposer_entity_clean"] = df["proposer_entity"].fillna("unknown").replace("", "unknown")
entity_stats = df.groupby("proposer_entity_clean").agg(
total_slots=("slot", "count"),
missed_slots=("is_missed", "sum")
).reset_index()
entity_stats["miss_rate"] = entity_stats["missed_slots"] / entity_stats["total_slots"] * 100
# Only show entities with at least 1 missed slot
entity_stats = entity_stats[entity_stats["missed_slots"] > 0]
entity_stats = entity_stats.sort_values("miss_rate", ascending=True)
# Color by miss rate
fig = go.Figure()
fig.add_trace(go.Bar(
y=entity_stats["proposer_entity_clean"],
x=entity_stats["miss_rate"],
orientation="h",
marker_color=entity_stats["miss_rate"],
marker_colorscale="YlOrRd",
text=entity_stats.apply(lambda r: f"{r['miss_rate']:.1f}% ({int(r['missed_slots'])}/{int(r['total_slots'])})", axis=1),
textposition="outside",
hovertemplate="<b>%{y}</b><br>Miss rate: %{x:.2f}%<extra></extra>",
))
fig.update_layout(
margin=dict(l=150, r=100, t=30, b=60),
xaxis=dict(title="Miss rate (%)", range=[0, max(entity_stats["miss_rate"]) * 1.3]),
yaxis=dict(title=""),
height=max(300, len(entity_stats) * 25 + 100),
)
fig.show(config={"responsive": True})
Missed slots by time of day¶
Are there patterns in when slots are missed? Spikes could indicate coordinated issues or network events.
Show code
if len(df_missed) > 0:
# Extract hour from slot time
df_missed["hour"] = pd.to_datetime(df_missed["slot_start_date_time"]).dt.hour
hourly_misses = df_missed.groupby("hour").size().reset_index(name="missed_count")
# Fill in missing hours with 0
all_hours = pd.DataFrame({"hour": range(24)})
hourly_misses = all_hours.merge(hourly_misses, on="hour", how="left").fillna(0)
hourly_misses["missed_count"] = hourly_misses["missed_count"].astype(int)
fig = go.Figure()
fig.add_trace(go.Bar(
x=hourly_misses["hour"],
y=hourly_misses["missed_count"],
marker_color="#e74c3c",
))
fig.update_layout(
margin=dict(l=60, r=30, t=30, b=60),
xaxis=dict(title="Hour (UTC)", tickmode="linear", dtick=2),
yaxis=dict(title="Missed slots"),
height=350,
)
fig.show(config={"responsive": True})
Missed slots timeline¶
When did each missed slot occur? Clusters may indicate network-wide issues.
Show code
if len(df_missed) > 0:
df_plot = df_missed.copy()
df_plot["time"] = pd.to_datetime(df_plot["slot_start_date_time"])
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df_plot["time"],
y=[1] * len(df_plot), # All at same y level
mode="markers",
marker=dict(size=10, color="#e74c3c", symbol="x"),
customdata=np.column_stack([df_plot["slot"], df_plot["proposer_entity"]]),
hovertemplate="<b>Slot %{customdata[0]}</b><br>Time: %{x}<br>Entity: %{customdata[1]}<extra></extra>",
))
fig.update_layout(
margin=dict(l=60, r=30, t=30, b=60),
xaxis=dict(title="Time (UTC)", tickformat="%H:%M"),
yaxis=dict(visible=False),
height=200,
)
fig.show(config={"responsive": True})
All missed slots¶
Complete list of missed slots with proposer details.
Show code
if len(df_missed) > 0:
df_table = df_missed[["slot", "slot_start_date_time", "proposer_entity"]].copy()
df_table["proposer_entity"] = df_table["proposer_entity"].fillna("unknown").replace("", "unknown")
df_table["time"] = pd.to_datetime(df_table["slot_start_date_time"]).dt.strftime("%H:%M:%S")
df_table = df_table.sort_values("slot")
# Create Lab links
df_table["lab_link"] = df_table["slot"].apply(
lambda s: f'<a href="https://lab.ethpandaops.io/ethereum/slots/{s}" target="_blank">View</a>'
)
# Build HTML table
html = '''
<style>
.missed-table { border-collapse: collapse; width: 100%; font-family: monospace; font-size: 13px; }
.missed-table th { background: #c0392b; color: white; padding: 8px 12px; text-align: left; position: sticky; top: 0; }
.missed-table td { padding: 6px 12px; border-bottom: 1px solid #eee; }
.missed-table tr:hover { background: #ffebee; }
.missed-table a { color: #1976d2; text-decoration: none; }
.missed-table a:hover { text-decoration: underline; }
.table-container { max-height: 500px; overflow-y: auto; }
</style>
<div class="table-container">
<table class="missed-table">
<thead>
<tr><th>Slot</th><th>Time (UTC)</th><th>Proposer entity</th><th>Lab</th></tr>
</thead>
<tbody>
'''
for _, row in df_table.iterrows():
html += f'''<tr>
<td>{row["slot"]}</td>
<td>{row["time"]}</td>
<td>{row["proposer_entity"]}</td>
<td>{row["lab_link"]}</td>
</tr>'''
html += '</tbody></table></div>'
display(HTML(html))
print(f"\nTotal missed slots: {len(df_table):,}")
else:
print("No missed slots today.")