omeryanbas.com

Ömer Yanbaş

General Manager, Ticofab Yazılım

SecurityWeb

Self hosted fonts, privacy law and one request less

A hosted font stylesheet hands every visitor IP address to another company before your page renders. Self hosting removes the transfer and a round trip.

Two lines in the head of a page bring in a font family from a hosted service. It takes a minute, the typography looks right, and nobody thinks about it again. Those two lines also tell another company the IP address of every person who opens the page, along with the address of the page itself, before a single character of your own content has been drawn. In Europe and in Turkey that is processing personal data, and the site owner is the one who has to justify it.

What actually happens

A stylesheet link to another origin is a request that the browser makes on the visitor's behalf, and every HTTP request carries more than the file name. The other server receives the IP address, the user agent string, the accepted languages and, from a stylesheet reference, the address of the document that asked for it. That is enough to know which device looked at which page, at what time.

European data protection rules treat an IP address as personal data when it can be connected to a person, which the Court of Justice of the European Union settled in 2016 for dynamic addresses held by a site operator. Turkish law follows the same reading: an IP address is personal data, and sending it to a server abroad is a transfer with its own conditions, a regime that was reworked in 2024 to add standard contractual clauses as a route.

The part that makes this awkward is where in the page it happens. The font stylesheet is in the head, so the request fires while the HTML is still being parsed. A consent banner further down the page cannot help, because the transfer already happened before the banner existed. Consent that arrives after the request is decoration.

A German regional court made the point concrete in January 2022. It found that a website which embedded fonts from a hosted service had unlawfully passed the visitor's IP address to a third country, ordered the site to stop, and awarded the claimant one hundred euros. The sum is nothing. The reasoning is what travelled: the transfer was not necessary, because the same fonts could have been served from the site's own server. A wave of warning letters followed, some of them opportunistic, and plenty of teams moved their fonts in a hurry.

Alongside the legal problem there is a plain performance one. A hosted font costs you a DNS lookup, a TCP connection and a TLS handshake to a second origin, then frequently a third origin for the font files themselves because the stylesheet and the binaries live on different hosts. The stylesheet is render blocking, so nothing paints until it arrives, and the font files are not even discovered until it is parsed. A preconnect hint warms the connection and does nothing about the extra round trip for the CSS.

The counter argument used to be the shared cache: the visitor has been to other sites using the same family, so the font is already on their disk. That stopped being true when browsers partitioned the HTTP cache by top level site to close a tracking channel. Chrome shipped partitioning in 2020, Firefox in early 2021, and Safari had done it years before. Today a font is fetched again for your site no matter how many other sites use it. The shared cache argument survives only in code reviews.

How to see it

Start with the built output, because a font reference can hide in a stylesheet rather than in the HTML:

grep -rEoh 'https?://[a-zA-Z0-9.-]+' dist/*.html dist/*.css dist/**/*.css | sort -u
# https://example.com

Anything in that list other than your own origin is a company your visitors are talking to. Then confirm from the page itself, which also catches requests added at runtime:

performance.getEntriesByType('resource')
  .filter((r) => new URL(r.name).origin !== location.origin)
  .map((r) => [new URL(r.name).host, r.initiatorType, Math.round(r.duration)]);

The fix

Self hosting is four steps and an hour, most of which is reading a licence.

  1. Download the exact files you use, in woff2, and put the licence text next to them. Open font licences generally allow self hosting and require the notice and licence to travel with the files. Some reserve the family name, so a modified or subsetted copy may need a different name. Keep the licence in the repository and served alongside the fonts.
  2. Subset to the characters the site can actually render. For a Turkish and English site that means Latin plus the dotted and dotless i, plus the other Turkish letters, plus punctuation and digits.
  3. Declare the faces yourself, with a display strategy and the fallback metrics matched.
  4. Preload only what the first screen needs, and cache the files forever under a hashed name.
import subsetFont from 'subset-font';

const used = new Set([...pagesText, 'çğıİöşüÇĞÖŞÜ', '0123456789', '.,:;!?()[]{}'].join(''));
const woff2 = await subsetFont(await readFile(source), [...used].join(''), {
  targetFormat: 'woff2',
});
@font-face {
  font-family: "Site Sans";
  src: url("/f/site-sans.b91c4e.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0100-017F, U+2000-206F;
  size-adjust: 104%;
  ascent-override: 92%;
}
<link rel="preload" href="/f/site-sans.b91c4e.woff2" as="font" type="font/woff2" crossorigin>

The crossorigin attribute on the preload is not optional. Fonts are fetched in CORS mode even from your own origin, and a preload without it is treated as a different request, so the browser downloads the file twice and the preload makes the page slower.

On the server side, a hashed file name lets you cache hard and never think about it again:

location /f/ {
  add_header Cache-Control "public, max-age=31536000, immutable";
  add_header X-Content-Type-Options "nosniff";
  types { font/woff2 woff2; }
}

If you run a strict content security policy, this is also the moment font-src 'self' becomes true rather than aspirational, which is one directive fewer to argue about when you are hashing inline blocks for the rest of the page.

How to check it worked

The first check is the one that matters legally: no other origin appears at all.

grep -rEoh 'https?://[a-zA-Z0-9.-]+' dist/*.html dist/*.css | sort -u | grep -v example.com
# (no output)

curl -sI https://example.com/f/site-sans.b91c4e.woff2 | grep -i 'cache-control\|content-type'
# cache-control: public, max-age=31536000, immutable
# content-type: font/woff2

Then load the page with an empty cache and look at the connection count in the network panel. Where there used to be three origins there is one, and the font request starts in the first few milliseconds because it is preloaded rather than discovered inside a stylesheet that had to arrive first. If your fallback metrics are matched, the swap happens without the line breaks moving, which is the same measurement described in the post on subsetting and layout shift.

What to watch out for

  • Check the licence before the download, not after. Most open families are fine to self host, a few commercial ones are licensed only through the service that hosts them, and the answer is in the licence file rather than in a blog post.
  • Subset carefully. User names, quoted text, currency symbols and a single word in another language can all fall outside the subset and silently render in a fallback font. Keep a wider face for those ranges or accept the fallback knowingly.
  • A variable font is not automatically smaller. One variable file can beat four static weights and can also lose to two subsetted static ones. Build both and compare the bytes.
  • Preloading everything is the same mistake as preloading nothing. Two files for the first screen is usually right, and every extra preload competes with the HTML and the critical CSS.
  • The cache headers have to survive the server config, and a header added inside one location block can quietly drop the ones inherited from the parent, which is its own trap.

Moving fonts onto your own server is the rare change where the legal argument, the privacy argument and the performance argument all point the same way, and the work is an afternoon that never has to be repeated. It is also a useful test of a habit: for every third party reference in the head of your pages, someone should be able to say what it sends, to whom, and what would break without it. Most of the time the honest answer is that it sends more than anyone intended, and that a copy on your own server would have done.

Questions and answers

Is using a third party font service against data protection rules?
It is a transfer of personal data that needs a basis, not an automatic violation. The visitor's IP address goes to a company that did not have to be involved, usually in another country, before the page has rendered and before any consent could be given. Since a self hosted copy is available and costs almost nothing, the argument that the transfer is necessary is hard to make.
Does self hosting fonts actually make the page faster?
Usually yes, and predictably so. You remove a DNS lookup, a TCP connection and a TLS handshake to another origin, plus a render blocking stylesheet that has to arrive before the font files are even discovered. On a slow mobile connection that is commonly several hundred milliseconds before the first character can be drawn.
Is the shared cache argument for public font CDNs still valid?
No. Browsers now key the HTTP cache by the top level site, so a font downloaded on one site is not reused on another. Chrome shipped that change in 2020, Firefox in early 2021, and Safari had partitioned its cache long before. Every visitor downloads your font from your server, or from someone else's server, but they download it either way.
What do I have to keep when I self host an open source font?
Read the licence before you copy the files. Open font licences generally allow self hosting and redistribution, and require that the licence text and the copyright notice travel with the font files. Some also reserve the family name, which means a modified or subsetted file may have to be renamed. Keep the licence file in the repository and next to the served files.