Mapbox GL JS Vector Source Loads No Tiles: Diagnosing CRS Mismatch and Silent Failures
A Mapbox GL JS vector source that loads no tiles and throws no errors usually means a CRS mismatch or a broken tile URL template. Here's how to tell them apart and fix each.
01 Aug 2026, 13:12 UTC

The recognizable condition
You add a vector tile source to a Mapbox GL JS map, the style JSON parses without errors, the layer shows up in map.getStyle().layers — and nothing renders. The Network tab shows zero tile requests for that source. No console error, no 404s, no attribution entry. The source is simply silent.
This pattern almost always means the source never bound to the map's render pipeline. The two most common causes are a coordinate reference system (CRS) mismatch between your source data and the map, and a tile URL template that Mapbox GL JS cannot expand into real requests. This guide walks through distinguishing them and fixing each.
Cause and diagnostic quick reference
| Symptom | Likely cause | Where to confirm |
|---|---|---|
| No tile requests at all | CRS mismatch or source never added | Console warnings; map.getSource() output |
| Requests fire, all return 404 | Tile URL missing {z}/{x}/{y} placeholders | Network tab request URLs |
| Tiles load but features render in wrong place or not at all | Source data in a non-3857 CRS | Compare feature coordinates against Web Mercator bounds |
| Source worked, then stopped after a style update | Orphaned source after partial style diff | Check map.getStyle().sources after the update |
Check 1: Confirm the source actually registered
In the browser console on the page hosting the map, run:
map.getSource('my-vector-source');
console.log(JSON.stringify(map.getStyle().sources, null, 2));
If getSource returns undefined, the source was never added — check that your map.addSource() call runs after the map's load event. If it returns an object, inspect the tile URL or TileJSON it carries. A vector source should look like this:
map.addSource('my-vector-source', {
type: 'vector',
tiles: ['https://tiles.example.com/data/{z}/{x}/{y}.pbf'],
minzoom: 0,
maxzoom: 14
});
The {z}/{x}/{y} placeholders are mandatory. If your URL is a literal path like /data/tiles.pbf, Mapbox GL JS will request that exact URL for every tile and your server will 404 — which looks like a data problem but is a template problem.
Check 2: Verify the CRS assumption
Mapbox GL JS renders in Web Mercator (EPSG:3857) and expects vector tile coordinates in that projection's tile grid. If your tile pipeline (for example, a custom Tippecanoe alternative or a hand-rolled PostGIS export) produced tiles in a different CRS, the source may load metadata but features land outside the visible world bounds — or the source fails to bind and no requests fire at all.
Diagnostic: fetch one tile directly in the console and decode it, or simpler, check the coordinates of a known feature. Web Mercator's valid range is roughly ±20037508.34 meters on both axes. If your source data carries coordinates that look like degrees (±180/±90) or a national grid (large values in a local range), it is not EPSG:3857.
// Rough sanity check: query rendered features after forcing a render
const feats = map.queryRenderedFeatures({ layers: ['my-layer'] });
console.log(feats.length, feats[0]?.geometry);
Also watch the console for CRS-related warnings during source initialization — Mapbox GL JS logs projection mismatches as warnings, not errors, so they are easy to miss among other output.
Check 3: Validate the whole style
Before deeper debugging, run your style JSON through the official Mapbox style validator (the @mapbox/mapbox-gl-style-spec package, usable from Node):
npm install @mapbox/mapbox-gl-style-spec
node -e "const v=require('@mapbox/mapbox-gl-style-spec');const s=require('./style.json');console.log(v.validate(s))"
Run this locally with Node installed; no special permissions needed. An empty array means the style is valid. Validation catches structural problems — a layer referencing a nonexistent source layer name, a wrong type field — that produce the same silent-no-render symptom as a CRS issue.
Fixes tied to findings
If the CRS is wrong: reproject the source data to EPSG:3857 and regenerate the tiles. With ogr2ogr, for example:
ogr2ogr -f GeoJSON -t_srs EPSG:3857 out.geojson in.shp
Then rebuild the tileset with your usual tool (Tippecanoe, planetiler, or a PostGIS/pg_tileserv pipeline, which serves Web Mercator tiles natively). Rebuilding tiles is the reliable fix; do not try to patch coordinates client-side.
If the URL template is wrong: correct the tiles array to include {z}/{x}/{y} and confirm your tile server actually serves that path scheme. Test one URL by hand — pick z=0, x=0, y=0 — and expect a binary protobuf response, not a 404 or an HTML error page.
If a style update orphaned the source: changing projection-related settings after map initialization requires a full style reload, not a partial setStyle diff. Re-add the source and layers after the reload, and verify with map.getStyle().sources that the source is present and referenced by at least one layer.
Verifying the fix
- Open the Network tab filtered to
.pbfand pan the map. You should see tile requests with 200 responses. - Run
map.queryRenderedFeatures({layers:['my-layer']})over a region with known data and confirm features return with plausible Web Mercator coordinates. - Check the attribution control: a bound vector source with an
attributionfield now displays it. Missing attribution alongside missing tiles is itself a signal the source never bound.
Limitations and escalation
This guide assumes Mapbox GL JS v2/v3 defaults. If you are on a heavily customized fork or an old v1 build, projection behavior may differ — check your version with mapboxgl.version. If tiles request fine, decode fine, and features still render offset, the problem is upstream in the tile generation pipeline, not the client; escalate to whoever owns the tile build with a sample tile and the expected feature coordinates.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.