Product
Developers
Docs Pricing Blog Startups FREE Star on GitHub22

Engineering

How we made logo loads about 4x faster in a real browser

Single requests looked fine. A real browser did not. What was slow, what we changed, and what we tried that did not help.

Alon Harpaz
Alon Harpaz, Founder of LogoAPI.sh Published · Updated · 6 min read
Contents

A logo API has one job on a page: get the image there before anyone notices it was missing. We measured that job with single requests, and the numbers looked fine. Then we loaded a real page in a real browser, and ours was the slow one. This is what we found, what we changed, and what we tried that did not help.

DisclosureThese are our own tests of our own service, run on Nov 5 and 6, 2026. Locations, sample sizes and what each number means are in How we measured. Your network will give different numbers.

Short answer

The bare logo URL, https://logoapi.sh/stripe.com, is now a plain static file. Before, a small program (a Cloudflare Worker) ran on almost every request to it.

In a real-browser test from a US runner, a page with 20 popular logos went from 281 ms to 73-75 ms at the median, in two separate runs after the change. At p99 it went from 471 ms to 247-303 ms. That is about 4x at the median. Nothing changes in your code. One header is gone: bare SVG responses no longer send X-Logo-Kind.

Page time for 20 logos, before and after US browser test. Median: 281 ms before; 75 and 73 ms in two after runs. p99: 471 ms before; 247 and 303 ms in two after runs. Lower is better. Median Before 281 ms After, run 1 75 ms After, run 2 73 ms p99 Before 471 ms After, run 1 247 ms After, run 2 303 ms 0 200 400 ms, lower is better
Heading render to last logo render, 20 popular logos, headless Chromium on a US GitHub Actions runner. Before: one run of 30 page loads, measured on 6 November 2026, just before the change went live. After: two separate runs of 15 page loads each, all completed, once the change was live.

Single requests looked fine

Our usual speed test is a script. It opens a new connection for each request and times the wait after the TLS handshake: from “secure connection ready” to the first byte of the logo. It sends 100 requests per URL, interleaved, so no URL gets a better minute than another.

From a US runner, the bare URL had a median of 54 ms and a p90 of 103 ms. The .svg URL for the same logo had 51 ms and 69 ms. Three ms apart at the median. The bare URL had a longer tail, about 35 ms at p90. We noted it and parked the fix. It did not look urgent.

A real browser disagreed

Then we built a test page the way a customer would. It shows a heading and 20 popular logos, each from a bare URL, cross-origin. Each load gets a fresh browser with the cache off. We time from the moment the heading paints to the moment the last logo paints. That is the wait a visitor sees.

On that page, our bare URL was about 3x slower than the other logo APIs in the same test.

Per logo, the gap was small. The median wait for the first byte was 68 ms from the US runner. Not alarming on its own. But a page is only as fast as its slowest logo, and our slowest logos waited about 240 ms. A single-request median hides that. A page of 20 logos finds it every time.

The lessonA median per request is not a page time. If your page shows many logos, the tail decides how fast it feels.

Why: code on almost every hit

The .svg URL was always a static file. The bare URL was not. It went to our Worker, which looks up the brand and decides what to send: the SVG, a PNG, the site's favicon, or a name tile. It also set the X-Logo-Kind header.

That logic is needed for a domain we don't have. For a catalog logo, the answer is a file we already have. Yet in our tests the Worker ran on most bare requests, even for the same popular logo again and again. Each run adds a little wait, and some runs add a lot. On a page of 20, one of those slow runs lands on the last logo.

We did not try to make the Worker faster. We took it out of the path for files it did not need to touch.

The fix: static files, no extension

For each catalog logo that has an SVG, the build now writes a second copy named only by the domain: stripe.com next to stripe.com.svg. Cloudflare's static layer serves that file before the Worker runs.

One problem: a file with no extension gets no content type, and a browser needs image/svg+xml to draw it. We set headers in the _headers file. One rule per file hits the rule cap. One catch-all rule would label our HTML pages as SVG. So the build writes one rule per domain ending:

/:a.com
  Content-Type: image/svg+xml
  Access-Control-Allow-Origin: *
  Timing-Allow-Origin: *
  X-Content-Type-Options: nosniff
  Cache-Control: public, max-age=86400, stale-while-revalidate=86400

:a matches one path segment. So /stripe.com gets the rule, and /brand/stripe.com and /icon/stripe.com do not. The build writes rules only for the most common domain endings, never for file endings such as .md or .html, and stays under the cap.

Everything else still goes to the Worker, as before:

  • Domains we don't have: you still get the site's favicon or a name tile.
  • Brands with only a PNG logo.
  • Catalog domains on a rare ending, with no header rule.
  • Other routes, such as /icon/, /dark/ and /ticker/.

Same URL, same file, same cache headers. Rollback is one revert.

What did not help, or was not worth it

Before testing, we set a bar: a change had to save at least 15 ms to ship. Most ideas did not clear it.

  • A separate static host (Cloudflare Pages). The same files were about 5 ms faster at the median from the US. Under the bar.
  • A longer cache lifetime. Our popular logos were already cache hits: 100 of 100 in the warm test, and 39 of 40 rarely requested brands. A longer lifetime only helps a miss. We had almost none.
  • Plain Cloudflare cache instead of the static layer. 8 to 10 ms faster at the median from the US (26 ms against 36 ms). Real, but under the bar, and it means a second place to store every file.
  • A second CDN in front of us. Warm, it was within a few ms of the best result in the same browser test, not clearly ahead. A cold edge added 190 to 250 ms on the first fetch of a logo it had not seen. We would not move our front door for a tie.

The large win was not a faster network. It was not running code.

The trade-off: X-Logo-Kind

A header rule is the same for every file on that ending. It cannot say “wordmark” for one brand and “icon” for the next. So bare SVG responses no longer send X-Logo-Kind. The .svg URL never sent it.

If your code reads that header, ask search instead. Every result has wordmark: true when the bare URL serves a wordmark.

curl "https://logoapi.sh/search?q=stripe"
# each result: "wordmark": true or false

A missing header does not mean iconIf you need a square mark every time, use /icon/{domain}. PNG-only brands still go through the Worker and still send X-Logo-Kind: raster.

How we measured

Method

Where

A GitHub Actions runner in the US, headless Chromium with a normal Chrome user agent. An earlier diagnostic run also used a cloud machine in the EU.

The page

20 popular logos from bare URLs, served from localhost, so every logo is cross-origin. Fresh browser context per load, cache off, first view only.

Page time

Heading render to last logo render, in ms. Median and p99 over the loads. This includes new connections, parallel fetches and the slowest logo. It is what a visitor waits for.

After-TLS time

Used for the single-request tests: first byte minus TLS done, one request per new connection, 100 per URL. It leaves out DNS, connect, TLS and download. It measures the server and the network, not the page.

Sample size

Before: one run of 30 page loads, all 30 completed, measured on 6 November 2026, just before the change went live, same runner type and same 20 logos. After: two separate runs of 15 page loads each, 15 of 15 completed in both: 75/247 and 73/303 ms median/p99. The second half of that run failed to load; we have not found the cause yet, so we report only the two clean runs. The earlier diagnostic runs used 30 page loads per variant.

Limits

One US location for the headline, all on one day. We give no numbers for other services here, so this post makes no ranking claim.

Our take

We trusted a number that measured the server, not the page. It was a good number. It answered the wrong question.

The cheapest code to run on a request is code that never runs.

After the change we are still slower than the other logo APIs in the same kind of test; the next step is the edge network itself.

We also rerun the browser test from the EU. We will publish those numbers here, good or bad. If you see a slow logo on your page, tell us the domain and where you are.

Questions developers ask

Do I need to change my code?

No. The URL, the SVG and the cache headers are the same. The only change: bare SVG responses no longer send the X-Logo-Kind header.

How do I tell a wordmark from an icon now?

Call https://logoapi.sh/search?q={name or domain} and read wordmark on the result: true means the bare URL serves a wordmark. A missing X-Logo-Kind header does not mean icon. If you always want a square mark, use /icon/{domain}.

Is it about 4x faster everywhere?

We do not know yet. The 281 to 73-75 ms result is from a US test runner. An earlier test from an EU cloud region showed the same slow pattern before the fix. We will publish an EU after number when we have one.

Why not put a bigger CDN in front?

We tested a second CDN. Warm, it was within a few ms of the best result in the same browser test, not clearly ahead, and a cold edge added 190 to 250 ms on a first fetch. The slow part was code running on each request, so we removed the code from the path instead.

Found this useful? Share it.