Dev.to · 7 min read

Reviewing My nginx and Cloudflare Settings Cut Cloud Run Data Transfer Costs by 96%

Reviewing My nginx and Cloudflare Settings Cut Cloud Run Data Transfer Costs by 96%

In my previous post, [2026-11-03-1], I reworked how I deploy to Cloud Run. This is a follow-up. As the title says, reviewing my nginx and Cloudflare settings cut my Cloud Run data transfer costs by 96%. Combined with the Artifact Registry savings from the previous post, my Google Cloud bill, which had been over ¥1,000 a month for about a year, came to ¥72 in October. Why I Looked at the Bill Following on from that work, I checked my Google Cloud bill. This blog is just nginx on Cloud Run serving static files, with Cloudflare in front as a CDN. Cloud Run data transfer costs depend on how much data Cloud Run sends out. I knew the bill had been creeping up, but I didn't know why. Looking at the breakdown, nearly all of it came from two items: Cloud Run Data Transfer (blue in the graph) and Artifact Registry Storage (orange). Artifact Registry accounted for almost half of the bill, but I knew it would go down because I'd set a cleanup policy in the previous post. That left Data Transfer, which was ¥556 in September. I took httpRequest.responseSize from the Cloud Run request logs (run.googleapis.com/requests) and totaled it by path for the last 24 hours: Path Transfer Share Requests / 0.20 GiB 6.1% 9,067 /index.xml 3.02 GiB 92.5% 1,522 Other ~0.04 GiB 1.4% ~3,200 Over 90% of the transfer was /index.xml, the feed for RSS readers. It contained the full text of every post and weighed 5.4 MB, adding up to about 3 GiB a day. nginx gzip Compression Was Turned Off Embarrassingly, I hadn't noticed that nginx gzip compression was disabled. I'd copied my nginx config from the official nginx Docker image years ago, and #gzip on; had been commented out ever since. Cloudflare was compressing responses to browsers, so nothing looked wrong from the outside. The uncompressed part was between Cloud Run and Cloudflare: Cloud Run (nginx) --(uncompressed)--> Cloudflare --(gzip)--> Browser That uncompressed part is what gets billed as Cloud Run Data Transfer. I enabled gzip compression like this: gzip on; gzip_comp_level 6; gzip_proxied any; gzip_vary on; gzip_types text/plain text/css text/xml application/javascript application/json image/svg+xml; I set gzip_proxied any so that responses are still compressed when a proxy along the way adds a Via header. By default, nginx doesn't compress responses to requests that have a Via header. /index.xml went from 5.4 MB to 1.27 MB. Later, for an unrelated reason, I limited /index.xml to the latest 100 posts. I'd been including every post out of old habit, but decided it wasn't necessary in the first place. Today /index.xml is about 1 MB, or about 245 KB with gzip. Incidentally, this reminded me of the 2001 scare over the possible shutdown of 2channel (2ch), a huge Japanese message board (the August crisis). In August 2001, growing data transfer made the site hard to keep running, and 2ch faced shutdown. Regulars on its UNIX board improved read.cgi to reduce the transfer, and the improvements included gzip compression. I never expected to still be talking about cutting data transfer with gzip 25 years later ( ´∀`) Caching HTML on Cloudflare Too Cloudflare's CDN had actually been enabled ever since I moved my DNS in [2024-04-13-1]. When Cloudflare has a cached copy, it can respond without going to Cloud Run every time, which reduces data transfer. However, only static files such as images were being cached. HTML and /index.xml were fetched from Cloud Run on every request. Cloud Run, the origin, doesn't return Cache-Control, so Cloudflare's default behavior applied before this change. Edge TTL is how long Cloudflare keeps the cache, and Browser TTL is how long the browser keeps it. Here's the before and after: Before (Cloudflare defaults) After (Cache Rules) Target Static files determined by file extension (images, CSS, JS, etc.) and robots.txt Everything HTML, /index.xml, /llms.txt, sitemap Not cached Cached Edge TTL Default for each status code (120 minutes for 200) 1 day Browser TTL 4 hours None (bypass, so browsers don't keep a copy) The whole cache gets purged on every deploy. Cache Rules are managed in Terraform with cloudflare_ruleset. resource "cloudflare_ruleset" "cache" { zone_id = data.cloudflare_zone.main.zone_id name = "default" kind = "zone" phase = "http_request_cache_settings" rules = [ { ref = "cache_everything" description = "Cache everything including HTML for 1 day. deploy.yml purges the cache on every deploy" # Exclude uptime monitoring so it checks the origin rather than the cache expression = "not (http.user_agent contains \"GoogleStackdriverMonitoring-UptimeChecks\" or http.user_agent contains \"UptimeRobot\")" action = "set_cache_settings" action_parameters = { cache = true edge_ttl = { mode = "override_origin" default = 86400 } # A purge can't clear browser caches, so don't let browsers keep a copy browser_ttl = { mode = "bypass" } } } ] } I set Edge TTL to 1 day. Since the cache is purged on every deploy, it could just as well be a week or a year. But if a purge fails, stale content stays up for the entire TTL. With 1 day, the worst case fixes itself by the next day. The shorter TTL means Cloudflare comes back to Cloud Run to revalidate. In the actual logs, revalidation after expiry returned 304 (Not Modified), so the data transfer was negligible. I bypassed Browser TTL because a purge can't clear browser caches. Browsers no longer keep images either, but those are served from Cloudflare, so Cloud Run's data transfer doesn't go up. I excluded uptime monitoring (Cloud Monitoring uptime checks and UptimeRobot) from caching by User-Agent. What I want to monitor is whether Cloud Run is alive, not the cache. For the purge, the Deploy workflow calls the Purge Everything API after deploying to Cloud Run. - name: Purge Cloudflare cache run: | curl -sSf -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/purge_cache" \ -H "Authorization: Bearer ${CLOUDFLARE_PURGE_TOKEN}" \ -H "Content-Type: application/json" \ --data '{"purge_everything":true}' \ | jq -e .success How Much Did It Go Down? The graph says it all. From over ¥1,000 a month down to ¥65 in October (excl. tax; ¥72 with tax). This post is about the blue Data Transfer; the orange Artifact Registry Storage went down thanks to the previous post. First, I compared daily data transfer using the Cloud Run request logs: Path Before After / 0.20 GiB 0.065 GiB /index.xml 3.02 GiB 0.015 GiB Other ~0.04 GiB ~0.039 GiB Total 3.26 GiB 0.119 GiB That's about a 96% reduction. Requests to /index.xml dropped from 1,522 to 30, with Cloudflare now answering most of them. / still shows up because uptime monitoring is excluded from caching. Even so, thanks to gzip, its data transfer fell to a third. Next, the bills for September and October: Item September October Cloud Run Data Transfer ¥556 (83.01 GiB) ¥24 (3.66 GiB) Artifact Registry Storage ¥442 (28.24 GiB/month) ¥41 (3.10 GiB/month) Cloud Storage ¥20 ¥0 Subtotal ¥1,018 ¥65 Tax ¥102 ¥7 Total ¥1,120 ¥72 October's figures also include a small effect from limiting the feed to 100 posts, but the log comparison above was made before that change. Of the ¥953 reduction, the ¥532 in Data Transfer came from the changes in this post. Of the rest, ¥401 was Artifact Registry, thanks to the cleanup policy from my previous post, and the remaining ¥20 was Cloud Storage. By the way, Cloudflare's CDN is on the Free plan, so that stays at ¥0. Conclusion nginx gzip compression I hadn't noticed was off, and a Cloudflare cache that only covered static files. Fixing these two things cut my Cloud Run data transfer costs by 96%. Together with the Artifact Registry cleanup from the previous post, my Google Cloud bill, which had been over ¥1,000 a month for about a year, is now ¥72. Both turned out to be simple once I understood them. If you're using Cloud Run, try totaling your request logs by path once. You might find something unexpected. References Module ngx_http_gzip_module Default cache behavior | Cloudflare Cache Rules | Cloudflare Purge everything | Cloudflare

This is a summary aggregated from Dev.to. Read the complete article on the original site:

Read full article at Dev.to

More Programming & Dev News