Empty Speedtest Tiles Are Not Places Without Internet

On this page

Problem statement

Map Ookla's Speedtest tiles for the lower 48 and most of the country is blank: only 4.6% of the land has a fixed-broadband tile in April–June 2026. It is tempting to read the blank areas as places without internet. They are places where nobody ran a fixed-broadband test that quarter: empty land, forests, farms, and homes whose owners never opened the app. Tiles also come and go between quarters. This page measures how much of the map is empty, how unstable sparse tiles are, and how to avoid reading absence as lack of service.

Quick answer

Treat a missing tile as no data, never as no service. For coverage questions use the FCC's National Broadband Map, which records availability by location. To reduce gaps in speed maps, pool several quarters or aggregate to larger areas.

area_km2 = counties.set_index("GEOID").to_crs(5070).area / 1e6
tile_km2 = (40075 / 2 ** 16) ** 2 * np.cos(np.radians(fixed.tile_y)) ** 2
cov = (tile_km2.groupby(fixed.GEOID).sum() / area_km2).reindex(area_km2.index).fillna(0)
print(f"share of the lower 48's land with a fixed-broadband tile: {tile_km2.sum() / area_km2.sum():.1%}; counties with no tile: {(cov == 0).sum()}")
share of the lower 48's land with a fixed-broadband tile: 4.6%; counties with no tile: 1

Tiles with at least one fixed-broadband test cover 4.6% of the land area of the lower 48, and only one county has no tile at all. Everywhere else, the map's blank areas are land where no one tested.

Map of Montana, the Dakotas, Wyoming and Nebraska with county outlines and tiny blue specks for the tiles that hold a fixed-broadband test, almost all of the land empty. Map of Montana, the Dakotas, Wyoming and Nebraska with county outlines and tiny blue specks for the tiles that hold a fixed-broadband test, almost all of the land empty.
Every tile with a fixed-broadband test in five northern Plains states: most of the land has none.

Churn, sparse tiles and county coverage

1. Tiles come and go

a, b = set(fixed25.quadkey), set(fixed.quadkey)
print(f"tiles in Q2 2025: {len(a):,}; in Q2 2026: {len(b):,}; in both: {len(a & b):,}; only in one: {len(a ^ b):,}")
tiles in Q2 2025: 1,546,257; in Q2 2026: 1,548,841; in both: 1,023,742; only in one: 1,047,614

Between Q2 2025 and Q2 2026 the number of tiles hardly changed (1,546,257 and 1,548,841), but only 1,023,742 appear in both and 1,047,614 in just one. The map's coverage reshuffles every year.

2. Who lives in empty places

one_test = fixed.tests == 1
print(f"tiles with exactly one test: {one_test.mean():.1%}; tiles that disappear next year among 2025 tiles with one test: "
      f"{(~fixed25[fixed25.tests == 1].quadkey.isin(b)).mean():.1%}, with 10+ tests: {(~fixed25[fixed25.tests >= 10].quadkey.isin(b)).mean():.1%}")
tiles with exactly one test: 28.4%; tiles that disappear next year among 2025 tiles with one test: 61.1%, with 10+ tests: 6.9%

The churn is in sparse tiles: 61.1% of 2025 tiles with one test vanish a year later, against 6.9% of tiles with 10 or more tests. An empty tile next to a filled one is usually chance.

Bar chart of the share of Q2 2025 tiles absent a year later by test count: 61% for one test, 46% for two, 34% for three or four, 20% for five to nine and 7% for ten or more. Bar chart of the share of Q2 2025 tiles absent a year later by test count: 61% for one test, 46% for two, 34% for three or four, 20% for five to nine and 7% for ten or more.
61.1% of tiles with one test vanish a year later, against 6.9% with ten or more.

3. Coverage by county

print(cov.describe(percentiles=[0.1, 0.5, 0.9])[["10%", "50%", "90%"]].round(3).to_dict())
{'10%': 0.004, '50%': 0.035, '90%': 0.205}

By county, the median county has tiles over 3.5% of its land, and the least covered tenth under 0.4%. Large rural counties look almost empty while their towns are fully tested.

Pooling quarters

The data used on this page

Ookla's Speedtest open data performance tiles for fixed broadband in April–June 2025 and April–June 2026, read from Amazon S3, and Census Bureau 2023 county boundaries.

from pathlib import Path

import duckdb
import geopandas as gpd
import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
# Ookla Speedtest open data (AWS Open Data, keyless): one Parquet file per quarter and network type, zoom-16 web
# Mercator tiles with average speeds, latency, tests and devices. Published quarterly under fixed names.
BASE = "https://ookla-open-data.s3.amazonaws.com/parquet/performance"


def tiles(kind, year, q):
    """One quarter's tiles inside a box around the contiguous US, cached locally as Parquet."""
    path = f"data/ookla-{kind}-{year}q{q}-conus.parquet"
    if not Path(path).exists():
        url = f"{BASE}/type={kind}/year={year}/quarter={q}/{year}-{(q - 1) * 3 + 1:02d}-01_performance_{kind}_tiles.parquet"
        con = duckdb.connect()
        con.sql("INSTALL httpfs; LOAD httpfs;")
        con.sql(f"""COPY (SELECT quadkey, tile_x, tile_y, avg_d_kbps, avg_u_kbps, avg_lat_ms, avg_lat_down_ms, avg_lat_up_ms, tests, devices
                         FROM read_parquet('{url}') WHERE tile_x BETWEEN -125 AND -66 AND tile_y BETWEEN 24 AND 50
                         ORDER BY quadkey) TO '{path}' (FORMAT parquet)""")
    return pd.read_parquet(path)


for name in ["cb_2023_us_county_20m.zip"]:
    if not Path(f"data/{name}").exists():
        Path(f"data/{name}").write_bytes(requests.get(f"https://www2.census.gov/geo/tiger/GENZ2023/shp/{name}", timeout=300).content)
counties = gpd.read_file("data/cb_2023_us_county_20m.zip")
counties = counties[~counties.STUSPS.isin(["AK", "HI", "PR"])].to_crs(4326)


def in_counties(df):
    """Attach the county each tile's centre falls in; tiles outside the lower 48 (Canada, Mexico, sea) drop out."""
    pts = gpd.GeoDataFrame(df, geometry=gpd.points_from_xy(df.tile_x, df.tile_y), crs=4326)
    j = gpd.sjoin(pts, counties[["GEOID", "STUSPS", "NAME", "geometry"]], predicate="within")
    return pd.DataFrame(j.drop(columns=["geometry", "index_right"]))


def wmedian(values, weights):
    """Median of tile averages, each tile counted once per test."""
    o = np.argsort(values.to_numpy())
    v, w = values.to_numpy()[o], weights.to_numpy()[o]
    c = np.cumsum(w)
    return v[np.searchsorted(c, c[-1] / 2)]


fixed = in_counties(tiles("fixed", 2026, 2))
fixed25 = in_counties(tiles("fixed", 2025, 2))
print(f"Ookla fixed broadband, April-June 2026: {len(fixed):,} tiles in the lower 48, {fixed.tests.sum():,} tests")
Ookla fixed broadband, April-June 2026: 1,548,841 tiles in the lower 48, 15,134,971 tests

Example 1: pool quarters to fill gaps

pooled = pd.concat([fixed25, fixed]).groupby("quadkey").agg(tests=("tests", "sum"))
print(f"tiles with a test in either quarter: {len(pooled):,} ({len(pooled) / len(b) - 1:+.1%} on 2026 alone)")
tiles with a test in either quarter: 2,071,356 (+33.7% on 2026 alone)

Pooling the two quarters gives 2,071,356 tiles with a test, 33.7% more than 2026 alone. Pooling a year of quarters fills more, at the cost of mixing time periods.

Map of United States counties shaded by the share of county land covered by fixed-broadband tiles, under 0.5% across most of the West and Plains, higher in the East and around cities. Map of United States counties shaded by the share of county land covered by fixed-broadband tiles, under 0.5% across most of the West and Plains, higher in the East and around cities.
How much of each county's land a Speedtest map can colour.

Example 2: what not to conclude

lo = cov.sort_values().head(3)
print("lowest tile coverage:", {counties.set_index("GEOID").loc[k, "NAME"] + ", " + counties.set_index("GEOID").loc[k, "STUSPS"]: round(v, 4) for k, v in lo.items()})
lowest tile coverage: {'Miner, SD': 0.0, 'Ziebach, SD': 0.0, 'Treasure, MT': 0.0001}

The least covered counties, Miner and Ziebach in South Dakota and Treasure County, Montana, are sparsely settled farm and ranch land. Their blank maps say how few people tested, not whether they can get online.

Why tiles are empty

A tile exists only if at least one test was run in it during the quarter. Most land has no buildings, and of the buildings, most households never run a speed test in a given three months. Rural tiles with one test appear and disappear from quarter to quarter at random. Speedtest data therefore maps tests, which follow people and their reasons for testing, and says nothing about what is available where nobody tested.

Gap-handling

  • Pool quarters for sparse areas, and say so in the caption.
  • Aggregate to counties or hexagons large enough to hold tests.
  • Never fill empty tiles with zeros or a low value.
  • Use the FCC map for availability and Speedtest for performance.

FAQ

Do empty Speedtest tiles mean no internet?

No. They mean no test was run there that quarter. In April–June 2026, tiles covered 4.6% of the lower 48's land, while broadband reaches far more.

Why do Ookla tiles change between quarters?

Tiles with one or two tests appear and disappear at random: 61.1% of 2025 tiles with one test had no test a year later, against 6.9% of tiles with 10 or more.