Guide

Map tile API errors explained

What 401, 403, 404 and 429 mean for map tiles, why OpenStreetMap tiles say “Access blocked”, why a map stays blank or grey, and the MapLibre, Leaflet and Google Maps messages that come with it. Each with the usual cause and the fix.

You seeUsuallyFix
“Access blocked” or “Referer is required” on OpenStreetMap tilesThe request breaks the tile usage policy of tile.openstreetmap.org.Send a Referer, cache tiles, or use another tile service.
401 UnauthorizedThe key or token is missing, wrong or deleted.Check the key in the URL that the browser requests.
403 Forbidden, “Invalid key”The key is not allowed from this website or for this service.Add the website to the allowed origins of the key.
429 Too Many RequestsThe monthly allowance or a per-minute limit is used up.Wait for Retry-After, cache tiles, or upgrade.
404 Not Found on tilesA zoom above the highest of the tile set, or a wrong URL template.Set maxzoom on the source; check {z}/{x}/{y}.
“Unable to parse the tile”, “Unimplemented type: 3”Gzip-compressed tiles sent without Content-Encoding: gzip.Send the header, or decompress on the server.
“has been blocked by CORS policy”No Access-Control-Allow-Origin header, often on an error answer.Look at the status code in the network tab.
Image “…” could not be loadedThe style names an icon that its sprite does not have.Use the sprite the style was made for, or add the image.
“Style is not done loading”Layers added before the style has loaded.Add them in map.on("load").
“You are using the same source for a hillshade layer and for 3D terrain”One raster-dem source for both.Use two sources with the same URL.
“Expected value to be of type number, but found null instead.”A style expression reads a field some features lack.Give a default with coalesce.
“This page can't load Google Maps correctly”An API key, billing or referer problem in Google Cloud.Read the error name in the console.
Leaflet: grey map, tiles in a jumble, “Map container is already initialized”Missing CSS, a hidden container, or a second L.map().Load leaflet.css, call invalidateSize(), call remove() first.
A blank map and no errorThe map element has no height, or style and tiles do not match.Give the element a height; use the style made for the tiles.

OpenStreetMap tiles: “Access blocked”

The tile servers at tile.openstreetmap.org are run by volunteers for the OpenStreetMap website and its editors. Other sites may use them within the tile usage policy. When a request breaks it, the server answers HTTP 403 with a picture of the message instead of the map, so the error shows on the map itself. The OpenStreetMap wiki lists the reasons on its page Blocked tiles:

  • “Referer is required”: the browser sent no Referer header. This happens when the page sets Referrer-Policy: no-referrer, when a privacy add-on or antivirus removes the header, and when the map runs from a local file. Set the policy of the page to strict-origin-when-cross-origin, the default of browsers, or to origin.
  • No valid User-Agent: apps outside a browser must send a User-Agent that names the app.
  • Bulk downloading, no caching or too many requests: downloading areas in advance for offline use, sending Cache-Control: no-cache, or loading tiles faster than a person pans a map.
  • “Blocked for not attributing”: the map does not show the © OpenStreetMap credit.

A block ends by itself once the app is fixed. For an app or website with steady traffic, the policy expects you to use a tile service of your own or a commercial one; the OpenStreetMap data stays free either way.

The settings of Helmet, Django, nginx, Apache and Caddy, and code for Leaflet, OpenLayers and MapLibre, are in the guide OpenStreetMap tiles: “Access blocked”.

401 Unauthorized

The tile service did not accept the key or token: it is missing, mistyped, deleted, or an old one after a key change.

  • Open the network tab of the browser and look at the tile or style URL that the browser requested. A placeholder such as YOUR_KEY or get_your_own_key left in a style URL, or an environment variable that was empty at build time, is the most common cause.
  • Mapbox answers {"message":"Not Authorized - No Token"} without a token, and 401 for a deleted token or for the old default public token after it was refreshed.
  • Stadia Maps answers 401 when the website is not registered for domain authentication and no API key is sent. On localhost it needs neither, with a low limit.
  • The style JSON often holds the key in several URLs (tiles, sprite, glyphs). If you copied a style, replace the key in all of them.

403 Forbidden and “Invalid key”

The key is valid but not allowed for this request. Usually the key is restricted to some websites, and the page that loads the map is not one of them.

  • Add every host the map runs on to the allowed websites of the key, with www. and without, plus staging hosts and localhost for development. Mapbox calls them URL restrictions, MapTiler “Allowed HTTP origins”.
  • A request without Origin or Referer fails as soon as a key has any restriction. This is why a restricted key works in the browser and fails in curl, Postman, QGIS or a mobile app. Use a separate key without website restriction there, and keep it out of web pages.
  • MapTiler shows “Invalid key” for a key that does not exist and for an origin that is not allowed.
  • Mapbox also answers 403 when the token lacks a scope, such as STYLES:READ for loading a style.
  • A page with Referrer-Policy: no-referrer sends no referer at all, so every website check fails.

429 Too Many Requests

The account has used its monthly allowance of tiles, or sent too many in a short time. Most free plans stop serving tiles until the next month once the allowance is used up.

  • Wait as long as the Retry-After header says. MapLibre retries tiles on the next pan or zoom by itself.
  • Estimate the tiles you need: a full-screen map loads about 15 to 30 tiles when it opens, and more with every pan and zoom. A map with 3D terrain loads terrain tiles as well.
  • Keep the cache of the browser on. Do not add cache-busting query strings to tile URLs and do not prefetch whole areas.
  • A map in a page that reloads every few seconds, such as a dashboard on a wall screen, uses up an allowance quickly. Reload the data and keep the map.

404 Not Found on tiles

  • The map asks for a zoom above the highest zoom of the tile set. Set maxzoom on the source to the highest zoom of the tiles; MapLibre then enlarges that zoom for closer views. With "url" pointing to a TileJSON file, MapLibre reads the zoom range from it.
  • The URL template is wrong: {z}/{x}/{y} in another order, .pbf instead of .mvt, or a missing path part. Open one tile URL with real numbers in the browser.
  • Tiles in the TMS scheme count rows from the south. Add "scheme": "tms" to the source, or tiles appear in the wrong hemisphere and many are missing.
  • Some servers answer 404 or 204 for tiles without data, such as open sea. A 204 is normal and MapLibre shows nothing there.

“Unable to parse the tile” and “Unimplemented type: 3”

Current versions of MapLibre GL JS report:

Unable to parse the tile at https://…/14/8580/5737.pbf, please make sure the data is not gzipped and that you have configured the relevant header in the server

Older MapLibre versions and Mapbox GL JS say Unimplemented type: 3, or another number. The decoder got bytes that are not a vector tile. Almost always these are gzip-compressed tiles from an MBTiles or PMTiles file, served without the Content-Encoding: gzip header, so the browser does not decompress them. Less often it is an HTML or JSON error page, or a PNG raster tile, given to a vector source.

curl -sI 'https://tiles.example.com/14/8580/5737.pbf' | grep -i -e content-type -e content-encoding
curl -s 'https://tiles.example.com/14/8580/5737.pbf' | head -c 2 | xxd   # 1f8b means gzip

Send Content-Encoding: gzip with gzip tiles (in nginx, add_header Content-Encoding gzip; on the tile location), or decompress them on the server. How to tell the causes apart, and tested settings for nginx, Apache, Caddy, S3 and PMTiles: MapLibre: “Unable to parse the tile”.

“has been blocked by CORS policy”

Access to fetch at 'https://tiles.example.com/…' from origin 'https://www.example.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

MapLibre loads styles, tiles, fonts and icons with fetch, so every one of these answers needs an Access-Control-Allow-Origin header. Tile services send it, but some send it only on successful answers. Then a 401, 403 or 429 shows up in the console as a CORS error and hides its cause. Look at the status code of the request in the network tab, then read the section for that code above. With your own tile server, add the header to every answer, errors included.

Image “…” could not be loaded

Image "cafe" could not be loaded. Please make sure you have added the image before it is needed with map.addImage(), resolved it with map.setMissingStyleImageResolver(), or included it in a "sprite" property in your style.

A layer of the style uses an icon that its sprite does not contain. It is only a warning, and the map works without that icon. It appears when a style is used with another sprite than its own, or when you add a layer with an icon-image of your own. Load your icon with map.loadImage() and map.addImage() before the layer needs it, or add a second sprite to the style (MapLibre accepts a list of sprites).

“Style is not done loading”

This error is thrown when map.addSource() or map.addLayer() runs before the style has loaded. Add your sources and layers in map.on("load", …). After map.setStyle(), the old layers are gone: add yours again in map.on("style.load", …).

“You are using the same source for a hillshade layer and for 3D terrain”

You are using the same source for a hillshade layer and for 3D terrain. Please consider using two separate sources to improve rendering quality.

Create two raster-dem sources with the same URL, one for the hillshade layer and one for terrain. The browser fetches each tile once and serves the second copy from its cache.

“Expected value to be of type number, but found null instead.”

A style expression reads a field that some features lack, for example ["get", "height"] for buildings without a height. Give it a default, ["coalesce", ["get", "height"], 10], or filter the layer to features that have it: ["has", "height"]. Many such warnings at once often mean that the style was made for another tile schema (see blank map).

Google Maps: “This page can't load Google Maps correctly”

The map turns dark with “For development purposes only” written across it. The console of the browser gives the reason as one of these errors.

  • InvalidKeyMapError or MissingKeyMapError: the key parameter is wrong or missing.
  • RefererNotAllowedMapError: the website is not among the HTTP referrers allowed for the key.
  • ApiNotActivatedMapError: the Maps JavaScript API is not enabled in the Google Cloud project of the key.
  • BillingNotEnabledMapError: the project has no billing account. Google requires one even within the free monthly amount.
  • ExpiredKeyMapError: the key was deleted or replaced.

All of them are fixed in the Google Cloud console under APIs & Services. Every code with its fix, website restrictions that work, and what a map costs with and without Google: Google Maps: “This page can't load Google Maps correctly”.

Leaflet: grey map, tiles in a jumble, “Map container is already initialized”

  • Tiles appear as a column of images, with gaps, or out of place: leaflet.css is not loaded. Add it before the map is created.
  • The map is grey until the window is resized: it was created inside a hidden element, such as a tab or a dialog. Call map.invalidateSize() when the element becomes visible.
  • Map container is already initialized.: L.map() ran twice on the same element, often after a re-render in React or Vue or a hot reload. Call map.remove() before creating it again, or create it once.
  • Map container not found.: the script runs before the element exists. Move the script after it, or wait for DOMContentLoaded.
  • Leaflet shows raster tiles (PNG). For vector tiles it needs a plugin such as maplibre-gl-leaflet.

A blank map and no error

  • The map element has no height. A div without content is 0 pixels high, so the map is there but cannot be seen. Give it one: #map { height: 480px; }. This is the most common cause of all.
  • The style was made for another tile schema. OpenMapTiles, Shortbread, Protomaps and Mapbox Streets use different layer names and fields, so a style for one shows only its background on the others. Use the style that belongs to the tiles.
  • MapLibre needs WebGL. On remote desktops, in some virtual machines and in browsers with hardware acceleration turned off it may be missing; the console then reports a WebGL error.
  • The map is centered on [latitude, longitude] instead of [longitude, latitude]. MapLibre takes the longitude first, Leaflet the latitude.

Checking a tile by hand

Request one tile with the headers a browser sends, and look at the status, the type and the first bytes:

curl -s -o tile.mvt -w '%{http_code} %{content_type}\n' \
  -H 'Accept-Encoding: gzip' -H 'Referer: https://www.example.com/' \
  'https://api.overpass.ch/v1/YOUR_KEY/maps/vector/14/8580/5737.mvt'
head -c 300 tile.mvt | xxd | head   # an error is readable text, a tile is binary

On overpass.ch an error is always JSON with a code and a message, and it carries the CORS headers, so the browser shows it as it is:

{"error":"forbidden_referer","message":"…"}
StatusCodeMeaning on overpass.ch
401unauthorizedThe key in the URL is missing or unknown.
403forbidden_refererThe website is not among the allowed referers of the key.
403key_scopeA “Maps only” key was used for Overpass or elevation.
404not_foundA zoom above the tile set; maps enlarge the highest zoom instead.
204–A tile without data, such as open sea. Free.
429rate_limited, quota_exceededThe per-minute limit of a free plan, or the monthly tiles; see Retry-After.
503server_busy, maps_unavailableRetry after the seconds in Retry-After.

All codes are in the documentation.

Moving to another tile service

With MapLibre GL JS, a tile service is one style URL. To move, replace the URL, keep your own sources and layers, and add them again after setStyle(). Layer names change with the tile schema, so layers of yours that refer to layers of the old style (for example beforeId) need new names.

Map tiles from overpass.ch

Vector tiles of the whole world for MapLibre in nine styles, among them outdoor with graded trails and marine with depths and sea marks, labels in 41 languages and 3D terrain. 100'000 tiles a month are free, keys can be limited to your websites, and errors come as JSON with Retry-After.