Protomaps Tile Hosting
This is another in a series of articles about map tile hosting. Pinball Map has now been around for like 16 years, and so has tried a few things. I think the overall rundown is: Google Maps, Mapbox, Maplibre using self-hosted mbtiles from OpenMapTiles, then back to Mapbox, and now self-hosted pmtiles from Protomaps. This post is about our experience switching from Mapbox to Protomaps. It covers the overall steps for self-hosting on Cloudflare, and our first month’s bill (it was very small).
Background
We like the Mapbox tiles and service. We do not like Mapbox’s usage-based dynamic pricing. We cannot afford to spend a lot on map tiles, and it is very risky to use a service where costs can escalate very quickly. For the web map SDK, Mapbox provides 50,000 free map loads per month (one load is counted, roughly, as one user initializing the map a day). An additional 10,000 loads will rocket your bill to $50. It’s notable to me that for Mapbox, it’s not free for them to operate their service for our first 50k loads, yet they don’t charge us anything; and also, the massive increase in price for the 50,001 load does not reflect a sudden increase in their costs when that threshold is exceeded. Their prices are based on some marketing calculation.
Mapbox has been very kind enough to offer us a discount (we are open source, not for profit). We used to always skate around 48-50k loads per month. Then at the beginning of this year we were on Hacker News. This resulted in a surge in loads and would have been a large bill, except that we asked for and were granted the discount mentioned above (granted for a limited time). Since then every month is like 56-60k loads. With the discount, we pay a modest $20 or so a month. As an example of the risk that getting attention brings, Hoodmaps once went viral and were hit with an $11k bill from Mapbox.
Many years ago, when we were consistently under the 50k views per month, we offered to pay them $20/month if they could just take us off this dynamic pricing and instead give us fixed pricing. They would have made money off of us for years! And it would have given us stability and peace of mind. But they didn’t, and so here we are, leaving Mapbox.
Since our bill every month is not fixed, we can kind of guess at what it will be during the last 10 days of the month. If we exceed the 50k closer to the 20th day of the month, then we know we’re in trouble. If we exceed the 50k closer to the 28th or so, then it will be a modest bill. I’m trying to give you an idea of the anxiety that can arise from these uncertainties. In September, Pinball Map was mentioned in an article on The Verge, and we hit the 50k on the 20th. This would have for sure resulted in our biggest bill yet. Fortunately, just a day or so later, someone posted about OpenFreeMap on HN, which is a server someone set up with free (accepting donations) map tiles. So we switched to it that day as a way to stop the bleeding from Mapbox!
But the discussion on HN reminded us of Protomaps, unveiled earlier this year on HN. We had briefly attempted to switch to it earlier in the year, but hit some snags and gave up. In that attempt, a friend was hosting the pmtiles, and the issue we were hitting seemed like a CORS issue that was tough to troubleshoot without direct file access. So now, although OpenFreeMap was cool, we thought it would be good to not be dependent on someone else’s service and to try Protomaps again.
In short, Protomaps worked, we’ve been using it for over a month, and the first month cost us $1.67 and this next month will likely be $0. In this post we’ll show how we set it up, and how our usage/costs on Mapbox translate to usage/costs on Cloudflare.
Setup Protomaps
The overall stack here is:
- pmtile file stored on Cloudflare R2
- maplibre gl as the javascript
Steps:
Step 1: Download a pmtiles build.
It’s like 115-120gb.
Step 2: Create a Cloudflare account. Upload the pmtiles (and two other asset folders) to an R2 bucket.
Since the file is so large, you have to upload using a multipart flag. Like:
rclone copy 20240923.pmtiles pmtiles:pbm-protomaps/ --s3-upload-cutoff=200M --s3-chunk-size=200M
This upload took 24 hours from my home network.
Also! Upload glyph (font) and sprite assets to R2. Obtain a zip from here and then use rclone to upload the fonts and sprites folders. Here is what our final Cloudflare R2 bucket looks like:
Step 3: Set a CORS policy, like:
"AllowedOrigins": [
"https://yourdomain.com",
],
Step 4: Set a domain
For setup and testing you can use R2.dev subdomain rather than your own domain. But for production they recommend setting up your own domain or subdomain. We registered a new domain, connected it to Cloudflare, and then “connected the domain” to the bucket.
That’s it. That’s the entire Cloudflare set up. Very easy. No server stuff!
Step 5: Serve the tiles on your site
Here is the initial commit with the switch to Protomaps. At first we included the style.json in our main javascript file, which is why the diff is so giant. We later moved that to its own json file.
Protomaps instructions. I found this step in the instructions to not be intuitive. Here they are showing you how to “install” the protocol, but this alone won’t suffice for our purposes. We then need to jump to how to render a basemap.
Rendering it involves adding in the glyphs and sprites and a layer json style. The layer style can be obtained from here. And the json style can easily be customized here. We eventually pulled out the colors and stored them as variables, so they are easier to look at and customize. We also minimized the json, which reduced the file size from like 500kb to 50kb. We also added a dark theme, and look for that preference here.
For us, it basically looks like this:
let protocol = new pmtiles.Protocol();
maplibregl.addProtocol("pmtiles", protocol.tile);
map = new maplibregl.Map({
container: 'map_canvas',
style:
"version": 8,
"glyphs": "<%= ENV['PMTILES_GLYPHS'] %>",
"sprite": "<%= ENV['PMTILES_SPRITE'] %>",
"sources": {
"protomaps": {
"type": "vector",
"url": "<%= ENV['PMTILES_URL'] %>",
"attribution": "<a href=\"https://protomaps.com\">Protomaps</a> © <a href=\"https://openstreetmap.org\">OpenStreetMap</a>"
}
},
"layers": [ a large amount of json ]
});
That’s a clean-up version of the code (the paths to the R2 “folders” are stored as environment variables). It will vary depending on how you are serving your map!
Anyway, that’s it. Updating the tileset will likely entail uploading a new pmtile file and then changing the environment variable to point to it.
Costs
Cloudflare R2 pricing. They categorize requests as either Class A or Class B operations, and they are further categorized as standard or infrequent. Class A are more expensive ($4.50 per million requests). From our first month of use, it seems like all requests for map tiles from users are being classified as Class B. Class B are 36 cents per million. But the first 10 million are within the free tier.
Since it’s hard to compare Mapbox map loads vs Cloudflare requests, here is a broad, hazy comparison.
We do not know how many requests we had in the first month. Our bill shows 0. However, from casually looking at it throughout the month, it seems like we got around 1 million Class B requests in the month.
As of this writing on Nov 5, we are at 140.61k from Nov 1 - 5. Roughly 140k in 5 days = like 840k in 30 days.
The main takeaway here is that with our quantity of users/activity (50-60k loads on Mapbox per month) we are using about 1/10 of the Cloudflare free tier, and even if we exceeded the Cloudflare free tier, the cost would be 34 cents per million requests. Very cheap for us.
The pmtiles upload process used Class A operations. But it was fewer than 1,000, which is well below the 1 million request free tier for Class A.
Our bill for the month was entirely for storage. The 111gb we are storing cost us $1.67.
Add another dollar per month for the domain we bought.