Low Level Design: Map Tiles Service

Tile Coordinate System

Map tiles use a Z/X/Y (zoom / column / row) scheme:

  • Zoom 0 = 1 tile covering the entire world
  • Zoom 1 = 4 tiles (2×2 grid)
  • Zoom N = 2^N x 2^N tiles
  • Zoom 18 = 68 billion tiles — only generated on demand
tile_count(zoom) = 2^zoom * 2^zoom
tile_url = /tiles/{z}/{x}/{y}.png

Tile Formats

  • Raster (PNG/JPEG) — pre-rendered images; simple to serve, large storage footprint
  • Vector (MVT/Protobuf) — raw geometry data; dynamic client-side styling, smaller payloads

Tile Generation Pipeline

OpenStreetMap data (PBF)
  -> osm2pgsql -> PostGIS database
  -> Tile Renderer
       Raster: Mapnik  -> PNG tiles
       Vector: Tegola  -> MVT tiles
  -> Object Storage (S3): tiles/{z}/{x}/{y}.png
  -> CDN (CloudFront / Fastly)
  -> Client

Storage Layout

s3://map-tiles-bucket/
  raster/
    0/0/0.png
    1/0/0.png
    ...
    18/x/y.png
  vector/
    0/0/0.mvt
    ...

-- Optional metadata table
CREATE TABLE tile_metadata (
  zoom         SMALLINT,
  x            INT,
  y            INT,
  generated_at TIMESTAMP,
  size_bytes   INT,
  PRIMARY KEY (zoom, x, y)
);

CDN Delivery

  • Tiles are served from CDN edge nodes; S3 is the origin
  • Tiles are immutable for a given zoom+coordinate once rendered
  • Set Cache-Control: public, max-age=31536000, immutable for indefinite CDN caching
  • Low origin hit rate after warmup — CDN absorbs >95% of traffic

Metatile Rendering

Render an 8×8 block (metatile = 64 tiles) in a single pass to amortize renderer startup cost and eliminate seam artifacts at tile borders. Split result into 64 individual tile files before uploading to S3.

metatile_render(z, meta_x, meta_y):
  image = render_8x8_block(z, meta_x*8, meta_y*8)
  for dx in 0..7:
    for dy in 0..7:
      tile = crop(image, dx*256, dy*256, 256, 256)
      s3_put('tiles/{z}/{meta_x*8+dx}/{meta_y*8+dy}.png', tile)

Pre-generation vs On-demand

  • Zoom 0-14: pre-generate all tiles offline during initial build and after map data updates
  • Zoom 15-18: render on-demand per request, cache result in S3/CDN immediately

Tile Invalidation

When map data is updated, re-render only tiles intersecting the changed geographic bounding box:

changed_bbox = (min_lng, min_lat, max_lng, max_lat)

for zoom in 0..14:
  x_min, y_min = lng_lat_to_tile(changed_bbox.min_lng, changed_bbox.max_lat, zoom)
  x_max, y_max = lng_lat_to_tile(changed_bbox.max_lng, changed_bbox.min_lat, zoom)
  enqueue_render_jobs(zoom, x_min..x_max, y_min..y_max)

Rate Limiting

Apply per-API-key rate limiting at the CDN or API gateway layer to prevent bulk tile scraping. Unauthenticated requests get a lower tier limit.

-- Nginx rate limit example
limit_req_zone  zone=tiles:10m rate=500r/m;
limit_req zone=tiles burst=100 nodelay;

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the Z/X/Y tile coordinate system?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Z/X/Y stands for zoom level, column, and row. At zoom 0, the entire world fits in one 256×256 pixel tile. Each additional zoom level doubles the tile count in each dimension, so zoom N has 2^N columns and 2^N rows. Tile URLs follow the pattern /tiles/{z}/{x}/{y}.png.”
}
},
{
“@type”: “Question”,
“name”: “What is the difference between raster and vector map tiles?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Raster tiles are pre-rendered PNG or JPEG images — simple to serve but large in storage and fixed in style. Vector tiles (MVT/Protobuf) contain raw geometry and attribute data, allowing the client to apply dynamic styles, support high-DPI displays, and enable interactive feature queries at much smaller file sizes.”
}
},
{
“@type”: “Question”,
“name”: “How does a metatile improve map tile rendering efficiency?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A metatile renders an 8×8 block of 64 tiles in a single pass, amortizing the fixed startup cost of the renderer and eliminating border artifacts that occur when adjacent tiles are rendered separately. The resulting image is cropped into 64 individual tile files and uploaded to object storage.”
}
},
{
“@type”: “Question”,
“name”: “How do you invalidate map tiles after a data update?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Compute the bounding box of the changed geographic area. Convert that bounding box to tile coordinates at each affected zoom level. Enqueue re-render jobs for all tiles intersecting the bounding box. Once re-rendered, new tiles overwrite the old files in S3 and CDN cache is purged for those tile paths.”
}
}
]
}

See also: Netflix Interview Guide 2026: Streaming Architecture, Recommendation Systems, and Engineering Excellence

See also: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering

See also: Airbnb Interview Guide 2026: Search Systems, Trust and Safety, and Full-Stack Engineering

Scroll to Top