Proving the pipeline end-to-end - from Cloud Run to BigQuery to this chart.
Published
July 8, 2026
This is the first post through the full pipeline: a Cloud Run Job pulls pitch-level Statcast data daily and appends it to BigQuery, and this page reads straight from that table and renders a chart. Nothing here is a deep analytical claim yet - the point of this post is narrower and more important than that: confirming that data really does flow from the source all the way to a published page, with no manual step in between.
Querying the pipeline’s output
from google.cloud import bigqueryclient = bigquery.Client()query ="""SELECT pitch_type, COUNT(*) AS n_pitches, AVG(release_speed) AS avg_release_speed, AVG(release_spin_rate) AS avg_spin_rateFROM `maydaystats.mlb_statcast.pitches`WHERE pitch_type IS NOT NULLGROUP BY pitch_typeORDER BY n_pitches DESC"""df = client.query(query).to_dataframe()df
pitch_type
n_pitches
avg_release_speed
avg_spin_rate
0
FF
1479
95.085936
2340.053414
1
SI
832
93.824760
2213.739183
2
SL
593
85.249747
2434.556492
3
CH
461
85.870065
1746.629067
4
ST
433
83.066051
2686.658199
5
FC
386
89.453627
2413.054404
6
CU
283
80.454770
2629.568905
7
FS
148
88.922973
1570.432432
8
KC
124
84.465323
2385.064516
9
FO
22
83.954545
1258.636364
10
FA
20
63.150000
1503.700000
11
SV
13
82.800000
2873.769231
12
CS
1
67.200000
2449.000000
As of this post, the table holds 4795 pitches across 13 distinct pitch types.
Average release speed by pitch type
import matplotlib.pyplot as pltfig, ax = plt.subplots(figsize=(8, 5))ax.bar(df["pitch_type"], df["avg_release_speed"], color="#2c3e50")ax.set_xlabel("Pitch type")ax.set_ylabel("Average release speed (mph)")ax.set_title("Average Release Speed by Pitch Type")ax.spines[["top", "right"]].set_visible(False)plt.tight_layout()plt.show()
Figure 1: Average release speed by pitch type, all games loaded so far
What this confirms, and what’s next
Every number and every bar above came from a live query against mlb_statcast.pitches, which itself only exists because a Cloud Scheduler job triggers a Cloud Run Job every morning that pulls the previous day’s Statcast data. As more days accumulate, this same query pattern (and others like it) becomes the basis for real analysis rather than pipeline validation.
This post uses Quarto’s frozen execution (freeze: true): the code above only re-runs when I re-render locally with fresh BigQuery access. The deployed site reuses that committed output rather than re-querying BigQuery on every build, which is what lets Cloudflare Pages build this site without needing my GCP credentials.