A Font Rendering Bug Made Our Prices Look Free for Six Hours
The first support ticket just said “is this a mistake” with a screenshot attached, and the screenshot showed our checkout page with a…
A Font Rendering Bug Made Our Prices Look Free for Six Hours
The first support ticket just said “is this a mistake” with a screenshot attached, and the screenshot showed our checkout page with a product listed at what looked, unmistakably, like a price of dollar sign, period, nothing.
Not $0.00.
Just “$.” sitting there on the page, like the number itself had been quietly deleted, and the checkout button underneath it cheerfully said “Complete Purchase” like nothing was wrong at all.

I remember staring at that screenshot for a solid ten seconds trying to figure out if it was a joke before I actually went and loaded the page myself.
It wasn’t a joke.
For six hours, an unknown number of visitors to our site had been looking at prices that, depending on the exact number involved, ranged from slightly wrong to looking completely, entirely free.
Nobody touched our pricing logic.
Nobody touched our database.
The actual number “9.99” was sitting there in our system the entire time, correct, untouched, exactly where it was supposed to be.
The bug wasn’t in the price.
It was in a single missing character inside a font file, and once I finally understood what had actually happened, I genuinely couldn’t believe how far downstream a font subsetting build step was from the checkout page it ended up breaking.
The Redesign That Shipped the Night Before
We’d pushed a marketing site refresh the previous evening: new layout, new typography, a nice custom webfont replacing the generic system font we’d been using for years.
Standard stuff.
The kind of change that gets reviewed for how it looks, not really scrutinized character by character, because why would it be?
It’s a font.
It just makes letters look nicer.
Somewhere in the build pipeline for that new font sat a step called font subsetting, and this is the part that turned out to matter enormously despite being something almost nobody on the team had ever thought about before that night.
Why Anyone Subsets a Font in the First Place
A full webfont file, every character it supports, every weight, every style, can genuinely be enormous.
Sometimes hundreds of kilobytes for something that, in most cases, only needs to render a small fraction of the total character set a font technically contains.
Subsetting is a completely standard, genuinely sensible performance optimization:
- Scan whatever text actually appears on the site
- Figure out exactly which characters get used
- Strip the font file down to include only those characters
- Dramatically reduce font size
- Improve page load times for every visitor
full font file: every letter, every digit, every symbol,
every language variant the typeface supports
~340KB
subsetting build step scans: all static text found on the site
at build time
subsetted font file: only the characters actually detected
~38KB
Genuinely reasonable.
Genuinely standard.
Used across a huge fraction of production websites without incident.
Precisely because most sites’ text doesn’t change in ways the subsetting scan can’t see coming.
Ours did, in one very specific, very unlucky way.
The Scan That Couldn’t See What It Needed to See
Here’s the actual mechanism, and it’s the kind of thing that only becomes obvious in hindsight.
The subsetting tool scanned our static marketing copy, headlines, body text, footer links, all the HTML that exists at build time, and correctly identified every character appearing anywhere in that text.
But our prices don’t live in static HTML at all.
They get pulled from a pricing API and injected into the page dynamically, client-side, after the page has already loaded, specifically so pricing can update without requiring a full site rebuild every time a number changes.
Which meant the subsetting scan, running entirely against build-time static content, had no way of ever seeing the digits that would later get inserted dynamically by JavaScript.
If a digit happened to appear somewhere in our static marketing copy too, purely by coincidence, say a “2024” in a footer copyright line, that digit made it into the subsetted font safely, just by accident.
But the specific digit “9” never once appeared anywhere in our static text.
Not in a headline.
Not in a footer.
Not in any button label.
Nowhere at all.
The scan looked everywhere it was capable of looking, found zero instances of the character “9”, and, doing exactly what it was built to do, quietly excluded that glyph from the final subsetted font file to save a few more bytes.
What the Subsetting Scan Could and Couldn’t See
static HTML at build time dynamic content, injected later
(headlines, footer, buttons) (prices, from the pricing API)
| |
v v
scanned successfully invisible to the build-time scan
| |
digits found:
0,1,2,3,4,5,6,7,8 digit "9" never detected
| |
included in subsetted font EXCLUDED from font
(nothing referenced it)
Every price on the entire site that happened to contain the digit nine, which, given how relentlessly common .99 pricing is, turned out to be almost every single price we had, was now trying to render a character that simply didn’t exist anywhere in the font file being served to the browser.
What Actually Happens When a Font Is Missing a Character
This is the part I had to go learn properly, because I’d genuinely never thought about it before that night.
When a browser tries to render a character that isn’t present in the specified font, it doesn’t just throw an error.
And it doesn’t necessarily fall back cleanly to a different font for that one character either.
That behavior actually depends on the specific font-fallback configuration in the site’s CSS.
In our case, the fallback chain wasn’t configured the way you’d want for something this critical, and the missing glyph rendered as effectively nothing.
An empty space exactly where a properly rendered “9” should have been.
With no visual indication anything had failed at all.
No broken-image icon.
No obvious error.
No little “tofu” box the way some systems display genuinely unsupported characters.
Just blank space.
Silently.
Seamlessly.
Sitting inside a price that otherwise looked completely normal.
intended: $9.99
rendered: $.99
(the 9 simply isn't there)
intended: $19.99
rendered: $1.99
(looks like a real, much lower price)
intended: $29.99
rendered: $2.99
(also looks like a real, much lower price)
intended: $99.99
rendered: $.
(both nines gone, looks completely broken or free)
That last one is the screenshot that landed in my inbox.
A ninety-nine dollar product, both nines silently erased by a missing glyph, leaving behind a dollar sign, a period, and nothing else.
Sitting next to a fully functional checkout button that had absolutely no idea anything upstream had gone wrong.
Because as far as the checkout logic was concerned, the actual price value was still, correctly, 99.99 the entire time.
The number was never broken.
Only its visual representation was.
And only for one specific digit.
In one specific font.
On one specific page.
Why Six Hours, and Why It Wasn’t Caught Immediately
The genuinely unsettling part of this story is how quiet the failure was.
Nothing crashed.
No error logs fired.
Because from the server’s perspective, and even from the browser’s rendering engine’s perspective in a purely technical sense, nothing had actually gone wrong.
A font was simply asked to display a character it didn’t have, and it displayed nothing, which is completely valid, if disastrous, behavior.
Our automated monitoring watches for:
- Failed requests
- Elevated error rates
- Slow response times
None of which had moved even slightly.
Every single request involved was returning a perfectly successful response with a completely valid price value inside it.
The bug lived entirely in the rendering layer.
A place almost none of our automated tooling was actually looking.
We shipped the redesign late at night, deliberately, to catch a quiet traffic window in case anything needed a quick rollback.
Which meant the first several hours after deploy had relatively few visitors passing through.
And the ones who did see a suspiciously blank-looking price mostly seem to have:
- Shrugged it off
- Assumed it was a display glitch on their end
- Or simply not noticed at all if the missing digit landed somewhere less visually obvious
It took traffic actually picking up the next morning, a genuine wave of people hitting pricing pages during normal business hours, before the sheer volume of “is this a mistake” screenshots became impossible to miss.
The Fix, and the More Uncomfortable Fix Underneath It
The immediate fix was almost embarrassingly small once we found the actual cause:
Force the digit “9”, along with the rest of the full numeral set, into the subsetting scan explicitly, rather than relying on the scan to discover it organically from static content that structurally could never contain it.
subsetting config, before:
scan: [static HTML content only]
subsetting config, after:
scan: [static HTML content,
explicit safelist: 0-9,
currency symbols,
common punctuation]
One line, essentially.
Guaranteeing every digit and currency-relevant character makes it into the font regardless of whether the build-time scanner happens to encounter it anywhere in the static content it can actually see.
The more uncomfortable fix, the one that actually took longer and mattered more, was accepting that any build process which infers its own scope by scanning content, rather than being told explicitly what it needs to support, is structurally blind to anything injected outside of what it’s scanning.
And that blind spot doesn’t announce itself anywhere in a build log.
The subsetting build completed successfully.
Every single time.
With zero warnings.
Zero errors.
A smaller file size than before.
Exactly as intended.
Nothing about that build process had any way of knowing it had just silently produced a font that was mathematically guaranteed to break roughly ninety percent of our pricing.
What Actually Stuck With Me About This One
I think about how many layers of the stack this bug traveled through completely undetected.
It started as a build-time optimization step, moved through a font file, into a browser’s rendering engine, and only became visible as a business-critical problem the moment a human eye landed on a checkout page and saw something that looked, unmistakably, like a free product.
Every single layer in between did exactly what it was built to do, correctly, according to its own narrow definition of correct.
- The build tool correctly subsetted based on what it could see.
- The browser correctly rendered a font that genuinely didn’t contain the requested glyph.
- The checkout logic correctly processed a price value that had never actually changed.
Nothing lied.
Nothing threw an error.
The truth just quietly failed to make it all the way to the pixel a person was actually looking at.
I look at font subsetting completely differently now.
And honestly, I look at any build step that infers its own scope from a scan, rather than being told explicitly what it needs to cover, with a lot more suspicion than I used to.
A missing character doesn’t look like a bug when you’re staring at a build log full of green checkmarks.
It just looks like a slightly smaller file, shipped a little faster, sitting quietly in production until the one specific digit it happened to exclude turns out to be the exact digit holding your prices together.
Final Thought
A green build proves only that the checks you wrote passed.
It does not prove that users are seeing what you think they’re seeing.
Sometimes the most expensive production bugs aren’t logic bugs, database bugs, or infrastructure bugs.
Sometimes they’re just a missing character in a font file.
I write about the quiet failures hiding in the gap between a green build and a page that actually works. If this made you want to go check your own font subsetting config, follow for more.
메타데이터
- post_id
- a8522c41a809
- slug
- font-rendering-bug-made-prices-look-free-a8522c41a809
- url
- https://medium.com/@the_atomic_architect/font-rendering-bug-made-prices-look-free-a8522c41a809
- canonical_url
- https://medium.com/@the_atomic_architect/font-rendering-bug-made-prices-look-free-a8522c41a809
- author_url
- https://medium.com/@the_atomic_architect
- status
- ok
- fetched_at
- 2026-08-25 05:19:14