Tirup Mehta
›Writing›Your Build Needs Someone Else's Server to Succeed

Your Build Needs Someone Else's Server to Succeed

Build Log/01.Nov.2026/5 min read
#build-log#reliability#frontend
← All Articles
TL;DR

My deploy failed one morning and the code hadn't changed. The build was downloading fonts from Google before it compiled anything. I moved two font files into the repo instead, and now the build finishes with the network adapter switched off, which is the only test that proves it.

The deploy failed at 9:41am and I'd pushed nothing overnight. I opened the log expecting a type error and found a timeout on a URL with the word font in it. My build wasn't broken, it was waiting on a server I don't own.

I never counted that as a dependency, because it never showed up in package.json. But a build that reaches out to the network is betting that a server I don't control answers quickly, every time, for everyone who ever runs it, and that bet can lose on a morning when nothing in the repo has moved.

What the compiler was doing with a font

Next.js ships a font loader. You give it a family name, and while the build runs it fetches a stylesheet from Google, follows the font file linked inside that stylesheet, then inlines the bytes into the output. It happens as part of the build, before any page renders, which is why a slow stylesheet turns into a slow deploy instead of a slow page.

Why would a build need the internet at all? Compiling a font would mean parsing outlines and subsetting them, and that isn't what happens. The loader downloads a file and rewrites the CSS around it, so the network ends up inside the build pipeline whether you meant it to be there or not.

Think of a chef who has to phone the supplier before the stove gets lit. Every ingredient is already in the fridge, but one unanswered call means dinner doesn't happen.

There's no local copy to fall back on. If that request fails, the build fails, and nothing in the manifest suggests the network was ever involved.

Why the runner failed and my laptop didn't

It isn't reproducible, which is the annoying part. The same commit builds on my laptop and times out on the runner, because the two machines reach Google across different networks. One build trains me to trust the commit. The other tells me the commit doesn't matter.

And the response depends on who asks. A browser gets a compressed woff2 file. A script with an older user agent gets an uncompressed TTF. Same URL, different bytes. I ran into that later when an image renderer needed the same fonts and refused the woff2 file I handed it, so the fix there was to stop pretending to be a browser and ask for the format it could actually read.

What bothers me about this failure isn't that it happened. It's where it showed up. A font problem arrived dressed as an infrastructure problem, and I spent a while reading the wrong part of the log before I looked at the URL inside the timeout.

The fix was two files and fifteen lines

Both families are variable, so one file covers every weight I use. I kept the Latin subset only, since the site doesn't render anything else, which leaves them at 27KB and 22KB.

app/fonts.ts The Fix
import localFont from "next/font/local"

export const sans = localFont({
  src: "../public/fonts/plus-jakarta-sans-latin.woff2",
  variable: "--font-sans",
  display: "swap",
  weight: "200 800",
})

export const display = localFont({
  src: "../public/fonts/space-grotesk-latin.woff2",
  variable: "--font-display",
  display: "swap",
  weight: "300 700",
})

That's the whole change. The weight range tells the loader the file already covers 200 to 800, so a request for 600 reuses it instead of pulling a second file. Swap paints the text immediately in a system font and replaces it when the real one arrives, so nothing flashes invisible while it loads. The tradeoff is a small reflow on first paint, which I decided was worth it.

The paths are relative and the files sit in the repo, so nothing resolves over the network, at build time or at run time.

How I know it's fixed

I switched the network adapter off and ran the build again. It finished. That's the only test I trust here, because every other check passes whether or not the dependency is still there.

Then I compared what I'd given up. Same two families, same weights, same look. The files together are smaller than a screenshot, and visitors were downloading them from Google anyway, so the only thing that changed is who serves them.

I left a comment beside the config saying why the files are there. In six months they'll look like clutter next to a one line import, and someone will want to tidy them away.

The remote import survives in so many projects for a boring reason. It's shorter, and it looks cleaner in a diff. Nobody objects during review, because it works. It keeps working until the morning the network doesn't, and by then the failure reads as an infrastructure incident rather than a dependency somebody added a year earlier.

The Takeaway
  • A build that reaches the network can fail for reasons you can't see.
  • Two files in the repo beat one convenient import.
  • Test with the network off, or you're guessing.