Transport protocols
Analysis of transport protocol usage (QUIC vs TCP) in Ethereum mainnet libp2p connections.
This notebook examines the distribution of QUIC and TCP transport protocols across the network, with breakdown by consensus client implementation. Data is sourced from the EthPandaOps Xatu sentries observing libp2p connection events over a 7-day rolling window.
Overall transport protocol distribution¶
Breakdown of all libp2p connections by transport protocol. UDP indicates QUIC transport, TCP indicates traditional TCP connections.
Show code
display_sql("transport_overall", target_date)
View query
Show code
df_overall = load_parquet("transport_overall", target_date)
df_overall["label"] = df_overall.apply(
lambda r: f"{'QUIC' if r['transport'] == 'udp' else 'TCP'} ({r['percentage']:.1f}%)",
axis=1
)
fig = px.pie(
df_overall,
values="connections",
names="label",
color="transport",
color_discrete_map={"tcp": "#3b82f6", "udp": "#22c55e"},
hole=0.4,
)
fig.update_layout(
title="Transport protocol distribution (7-day rolling)",
height=400,
showlegend=True,
legend=dict(orientation="h", yanchor="bottom", y=-0.15, xanchor="center", x=0.5),
)
fig.show()
Daily transport protocol trends¶
Daily breakdown showing TCP vs QUIC connection counts over time.
Show code
display_sql("transport_daily", target_date)
View query
Show code
df_daily = load_parquet("transport_daily", target_date)
df_daily["transport_label"] = df_daily["transport"].map({"tcp": "TCP", "udp": "QUIC"})
fig = px.bar(
df_daily,
x="date",
y="connections",
color="transport_label",
color_discrete_map={"TCP": "#3b82f6", "QUIC": "#22c55e"},
barmode="stack",
labels={"date": "Date", "connections": "Connections", "transport_label": "Transport"},
)
fig.update_layout(
title="Daily connections by transport protocol",
height=400,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
)
fig.show()
Unique peers by transport protocol¶
Unique peer analysis showing how many distinct peers connected via each transport protocol. Note that some peers connect via both protocols.
Show code
display_sql("transport_peers", target_date)
View query
Show code
df_peer_breakdown = load_parquet("transport_peers", target_date)
peer_data = [
{"category": "TCP only", "peers": int(df_peer_breakdown["tcp_only"].iloc[0])},
{"category": "QUIC only", "peers": int(df_peer_breakdown["quic_only"].iloc[0])},
{"category": "Both", "peers": int(df_peer_breakdown["both"].iloc[0])},
]
df_peers = pd.DataFrame(peer_data)
df_peers["percentage"] = (df_peers["peers"] / df_peers["peers"].sum() * 100).round(1)
fig = px.bar(
df_peers,
x="category",
y="peers",
color="category",
color_discrete_map={"TCP only": "#3b82f6", "QUIC only": "#22c55e", "Both": "#a855f7"},
text=df_peers.apply(lambda r: f"{r['peers']:,} ({r['percentage']}%)", axis=1),
)
fig.update_traces(textposition="outside")
fig.update_layout(
title="Unique peers by transport capability (7-day rolling)",
height=400,
showlegend=False,
xaxis_title="",
yaxis_title="Unique peers",
)
fig.show()
Transport protocol by client implementation¶
QUIC adoption varies significantly by consensus client. This table shows the breakdown of unique peers by their transport protocol capability for each known client implementation.
Show code
display_sql("transport_by_client", target_date)
View query
Show code
df_clients = load_parquet("transport_by_client", target_date)
df_clients
Show code
df_clients_long = df_clients.melt(
id_vars=["client", "total"],
value_vars=["tcp_only", "quic_only", "both"],
var_name="protocol_type",
value_name="peers",
)
df_clients_long["protocol_label"] = df_clients_long["protocol_type"].map({
"tcp_only": "TCP only",
"quic_only": "QUIC only",
"both": "Both",
})
client_order = df_clients.sort_values("total", ascending=True)["client"].tolist()
fig = px.bar(
df_clients_long,
x="peers",
y="client",
color="protocol_label",
color_discrete_map={"TCP only": "#3b82f6", "QUIC only": "#22c55e", "Both": "#a855f7"},
orientation="h",
category_orders={"client": client_order},
labels={"peers": "Unique peers", "client": "Client", "protocol_label": "Transport"},
)
fig.update_layout(
title="Transport protocol support by client (7-day rolling)",
height=500,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
barmode="stack",
)
fig.show()
QUIC capability by client¶
Percentage of peers supporting QUIC (either exclusively or alongside TCP) for each client implementation.
Show code
df_quic_pct = df_clients.sort_values("quic_capable_pct", ascending=True).copy()
fig = px.bar(
df_quic_pct,
x="quic_capable_pct",
y="client",
orientation="h",
text=df_quic_pct["quic_capable_pct"].apply(lambda x: f"{x:.1f}%"),
color="quic_capable_pct",
color_continuous_scale="Greens",
)
fig.update_traces(textposition="outside")
fig.update_layout(
title="QUIC-capable peers by client (7-day rolling)",
height=500,
showlegend=False,
xaxis_title="% of peers supporting QUIC",
yaxis_title="",
coloraxis_showscale=False,
)
fig.update_xaxes(range=[0, 100])
fig.show()
Connection patterns by protocol¶
Comparison of connection frequency: how many connections each unique peer makes on average, broken down by transport protocol.
Show code
display_sql("transport_connection_patterns", target_date)
View query
Show code
df_conn_patterns = load_parquet("transport_connection_patterns", target_date)
df_conn_patterns["transport_label"] = df_conn_patterns["transport"].map({"tcp": "TCP", "udp": "QUIC"})
fig = make_subplots(
rows=1, cols=2,
subplot_titles=("Unique peers", "Connections per peer"),
)
colors = {"TCP": "#3b82f6", "QUIC": "#22c55e"}
fig.add_trace(
go.Bar(
x=df_conn_patterns["transport_label"],
y=df_conn_patterns["unique_peers"],
marker_color=[colors[t] for t in df_conn_patterns["transport_label"]],
text=df_conn_patterns["unique_peers"].apply(lambda x: f"{x:,}"),
textposition="outside",
showlegend=False,
),
row=1, col=1,
)
fig.add_trace(
go.Bar(
x=df_conn_patterns["transport_label"],
y=df_conn_patterns["connections_per_peer"],
marker_color=[colors[t] for t in df_conn_patterns["transport_label"]],
text=df_conn_patterns["connections_per_peer"].apply(lambda x: f"{x:.1f}"),
textposition="outside",
showlegend=False,
),
row=1, col=2,
)
fig.update_layout(
title="Connection patterns by transport (7-day rolling)",
height=400,
)
fig.show()
Daily unique peers trend¶
Daily count of unique peers connecting via each transport protocol.
Show code
df_daily_pivot = df_daily.pivot(index="date", columns="transport_label", values="unique_peers").reset_index()
df_daily_pivot["QUIC %"] = (df_daily_pivot["QUIC"] / (df_daily_pivot["TCP"] + df_daily_pivot["QUIC"]) * 100).round(1)
fig = make_subplots(
rows=2, cols=1,
subplot_titles=("Unique peers by transport", "QUIC share of unique peers"),
row_heights=[0.6, 0.4],
vertical_spacing=0.12,
)
fig.add_trace(
go.Scatter(
x=df_daily_pivot["date"],
y=df_daily_pivot["TCP"],
name="TCP",
mode="lines+markers",
line=dict(color="#3b82f6"),
),
row=1, col=1,
)
fig.add_trace(
go.Scatter(
x=df_daily_pivot["date"],
y=df_daily_pivot["QUIC"],
name="QUIC",
mode="lines+markers",
line=dict(color="#22c55e"),
),
row=1, col=1,
)
fig.add_trace(
go.Scatter(
x=df_daily_pivot["date"],
y=df_daily_pivot["QUIC %"],
name="QUIC %",
mode="lines+markers",
line=dict(color="#22c55e"),
fill="tozeroy",
fillcolor="rgba(34, 197, 94, 0.2)",
showlegend=False,
),
row=2, col=1,
)
fig.update_yaxes(title_text="Unique peers", row=1, col=1)
fig.update_yaxes(title_text="QUIC %", range=[0, 20], row=2, col=1)
fig.update_xaxes(title_text="Date", row=2, col=1)
fig.update_layout(
title="Daily unique peer trends",
height=600,
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
)
fig.show()
Summary¶
Key findings from the transport protocol analysis.
Show code
from IPython.display import HTML, display
tcp_pct = df_overall[df_overall["transport"] == "tcp"]["percentage"].iloc[0]
quic_pct = df_overall[df_overall["transport"] == "udp"]["percentage"].iloc[0]
tcp_only_peers = int(df_peer_breakdown["tcp_only"].iloc[0])
quic_only_peers = int(df_peer_breakdown["quic_only"].iloc[0])
both_peers = int(df_peer_breakdown["both"].iloc[0])
total_peers = int(df_peer_breakdown["total"].iloc[0])
quic_clients = df_clients[df_clients["quic_capable_pct"] > 10]["client"].tolist()
no_quic_clients = df_clients[df_clients["quic_capable_pct"] < 1]["client"].tolist()
html = f'''
<style>
.summary-table {{ border-collapse: collapse; font-family: system-ui; width: 100%; }}
.summary-table th {{ background: #1e293b; color: white; padding: 12px; text-align: left; }}
.summary-table td {{ padding: 10px; border-bottom: 1px solid #e2e8f0; }}
.summary-table tr:hover {{ background: #f8fafc; }}
.metric {{ font-size: 1.5em; font-weight: bold; }}
.tcp {{ color: #3b82f6; }}
.quic {{ color: #22c55e; }}
</style>
<table class="summary-table">
<tr><th colspan="2">Connection distribution</th></tr>
<tr><td>TCP connections</td><td class="metric tcp">{tcp_pct:.1f}%</td></tr>
<tr><td>QUIC connections</td><td class="metric quic">{quic_pct:.1f}%</td></tr>
<tr><th colspan="2">Unique peer breakdown</th></tr>
<tr><td>TCP-only peers</td><td>{tcp_only_peers:,} ({tcp_only_peers/total_peers*100:.1f}%)</td></tr>
<tr><td>QUIC-only peers</td><td>{quic_only_peers:,} ({quic_only_peers/total_peers*100:.1f}%)</td></tr>
<tr><td>Dual-protocol peers</td><td>{both_peers:,} ({both_peers/total_peers*100:.1f}%)</td></tr>
<tr><td>Total unique peers</td><td><b>{total_peers:,}</b></td></tr>
<tr><th colspan="2">Client QUIC support</th></tr>
<tr><td>Significant QUIC support (>10%)</td><td>{", ".join(quic_clients) or "None"}</td></tr>
<tr><td>No QUIC support (<1%)</td><td>{", ".join(no_quic_clients) or "None"}</td></tr>
</table>
'''
display(HTML(html))