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;

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