Production PostGIS Vector Tiles: Caching

Paul Ramsey
PostGIS Crunchy Bridge

Building maps that use dynamic tiles from the database is a lot of fun: you get the freshest data, you don't have to think about generating a static tile set, and you can do it with very minimal middleware, using pg_tileserv.

However, the day comes when it's time to move your application from development to production, what kinds of things should you be thinking about?

Let's start with load. A public-facing site has potentially unconstrained load. PostGIS is fast at generating vector tiles.

68747470733a2f2f646f63732e676f6f676c652e636f6d2f64726177696e67732f642f652f32504143582d317654686b35784b357a45666d6970555a4e6b314a48413374706436363759524375646d4c3571544e734b505a59335251734973772d7665476d324a5231503366593270317252495472634c36

One way to deal with load is to scale out horizontally, using something like the postgres operator to control auto-scaling. But that is kind of a blunt instrument.

68747470733a2f2f646f63732e676f6f676c652e636f6d2f64726177696e67732f642f652f32504143582d3176546b70614b317273676246516346596d50645f7468587769666b776c6d4942483241564a4f5f754c5973536f38664f484c304a67477776697854684e75774364645a7244583033734c7759

A far better way to insulate from load is with a caching layer. While building tiles from the database offers access to the freshest data, applications rarely need completely live data. One minute, five minutes, even thirty minutes or a day old data can be suitable depending on the use case.

68747470733a2f2f646f63732e676f6f676c652e636f6d2f64726177696e67732f642f652f32504143582d3176526a7042454b6b7077324631426778546b33344d7154396f62664c566d71397868392d6b414f646a78504737496f6c4f636c5930536b665065625a7347726f4850644c775a4c5553486d69

A simple, standard HTTP proxy cache is the simplest solution. Here's an example using just containers and docker compose that places a proxy cache between a dynamic tile service and the public web.

I used Docker Compose to hook together the community pg_tileserv container with the community varnish container to create a cached tile service, here's the annotated file.

First some boiler plate and a definition for the internal network the two containers will communicate over.

version: '3.3'

networks:
webapp:

The services section has two entries. The first entry configures the varnish service, accepting connections on port 80 for tiles and 6081 for admin requests.

Note the "time to live" for cache entries is set to 600 seconds, or five minutes. The "backend" points to the "tileserv" service, on the usual unsecured port.

services:
web:
image: eeacms/varnish
ports:
- "80:6081"
environment:
BACKENDS_PROBE_INTERVAL: "15s"
BACKENDS_PROBE_TIMEOUT: "5s"
BACKENDS_PROBE_WINDOW: "3"
BACKENDS: "tileserv:7800"
DNS_ENABLED: "false"
DASHBOARD_USER: "admin"
DASHBOARD_PASSWORD: "admin1234"
DASHBOARD_SERVERS: "web"
PARAM_VALUE: "-p default_ttl=600"
networks:
- webapp
depends_on:
- tileserv

The second service entry is for the tile server, it's got one port range, and binds to the same network as the cache. Because pg_tileserv is set up with out-of-the-box defaults, we only need to provide a DATABASE_URL to hook it up to the source database, which in this case is an instance on the Crunchy Bridge DBaaS.

  tileserv:
image: pramsey/pg_tileserv
ports:
- "7800:7800"
networks:
- webapp
environment:
- DATABASE_URL=postgres://postgres:password@p.uniquehosthash.db.postgresbridge.com:5432/postgres

Does it work? Yes, it does. Point your browser at the cache and simultaneously watch the logs on your tile server. After a quick run of populating the common tiles, you'll find your tile server gets quiet, as the cache takes over the load.

 

 

If your scale is too high for a single cache server like varnish, consider adding yet another caching layer, by putting a content delivery network (CDN) in front of your services.

Newsletter