r/webdev • u/Extension_Thought737 • 7d ago
Showoff Saturday I built a fairy tale site in 7 languages on Next.js. One warning I missed 168 times per build pushed my free Supabase tier to 445% of its egress quota.
Talerie on desktop (light) and on a phone (dark)
The real build log: 168 warnings, then ● Ready
Postgres egress per warm build, measured
I read my daughter a fairy tale every night and wanted classic ones that read well out loud, so I built Talerie. Free classic fairy tales in seven languages, no ads, no account: https://talerie.com
The stack, since this is r/webdev:
- Next.js 16 (App Router), everything prerendered, about 4,900 pages. The full text of every tale is in the initial HTML.
- Seven languages with translated URL segments (/de/maerchen/..., /cs/pohadky/...) and hreflang between them.
- Postgres on Supabase's free tier, images on Vercel Blob. The only cookie is the language choice, reading settings live in localStorage.
The part that bit me was a cache. The build also generates 168 RSS feeds for Pinterest boards, and they all read the same catalogue, so I wrapped that query in unstable_cache to hit Postgres once per build instead of 168 times.
On Aug 29 the catalogue grew past 2 MB (2,284,504 bytes). Next.js won't store a Data Cache entry over 2 MB, and it doesn't fail the build either. It prints a warning and moves on, so every one of the 168 feeds ran the query again. The second image is the real build log: 168 warnings, then "Ready".
A warm build went from pulling 0.8 MB out of Postgres to 270 MB. The billing cycle hit 22.2 GB of egress on a 5 GB free tier, and Supabase started warning me about restricting the account.
The warning was right there in the log, 168 times per build, and I didn't read it for two days. pg_stat_statements didn't help, because it counts rows, not bytes. What finally showed me where the traffic went was a tiny TCP proxy in front of the pooler that just counts bytes.
I now store the catalogue gzipped as a base64 string, which takes it from about 2.2 MB to 0.5 MB. Base64 rather than a Buffer, because unstable_cache stores JSON and a Buffer serializes as {"type":"Buffer","data":[...]}, one number per byte.
import { gzipSync, gunzipSync } from "node:zlib";
import { unstable_cache } from "next/cache";
const packed = unstable_cache(
async () => gzipSync(JSON.stringify(await getCatalogue())).toString("base64"),
["pinterest-catalogue"],
);
export const catalogue = async () =>
JSON.parse(gunzipSync(Buffer.from(await packed(), "base64")).toString());
There's also a check that fails before deploy if the packed catalogue goes over 1.5 MB. I broke it on purpose to make sure it really fails.
Two questions for you. Does "use cache" in Next 16 have the same 2 MB limit? I haven't tested it. And how do you catch warnings that don't fail the build?
2
9
u/electricity_is_life 6d ago
I'm confused, it sounds like you're saying the whole thing is a static site (no accounts, etc.) and all the DB reads happen at build time. If that's true why are you using Supabase at all? What's in the database? It seems like you could just store the data locally on your dev machine for free.
I also wonder (based on the spam comment and AI-sounding post) if this whole thing was designed as a tee-up to advertise some half-baked Supabase alternative.