Plate 41
ofetch vs ky: 4.1 KB vs 9.8 KB Gzip
Aditya Challa4 min read
I bundled the two common fetch wrappers on this box: ofetch 1.5.1 came out at 4,057 bytes gzip, while ky 2.1.0 came out at 9,779 bytes. Against a local JSON server, ofetch.create averaged about 122 µs per request versus about 263 µs for ky.create.
Short answer: pick ofetch when you want a smaller auto-parsing client (Nuxt / unjs stack). Keep ky when you want Response-first control, stricter TypeScript around HTTPError, and the default retry=2 policy on safe methods. No affiliate links in this post.
What I tested
- Machine: 8 vCPU Intel Xeon / 15 GB RAM shared Linux cloud box, tested 6 Oct 2026 about 02:15 IST.
- Versions: ofetch 1.5.1, ky 2.1.0, Node 22.20.0, esbuild 0.28.2 browser ESM minify + gzip -9.
- Inputs: local http.Server JSON payload (~32 object fields via 20 items). Speed: 800 sequential GETs, median of 9 rounds, two runs with library order reversed. Scripts in /workspace/bench-ofetch-ky/.
- Findings: ofetch 10,114/4,057 B vs ky 28,594/9,779 B; create wrappers 10,127/4,070 vs 28,611/9,787. Plain GET µs/req: ofetch 183.8 / 129.7 vs ky 179.8 / 203.5. create µs/req: ofetch 118.8 / 125.8 vs ky 300.4 / 225.7.
- One surprise: ofetch.FetchError on 404 already exposed parsed .data; ky.HTTPError needed a separate e.response.json() read, and my first attempt saw a null body after .json() had already been requested on the failing call.
- Not tested: browsers, interceptors/hooks under load, upload streams, AbortSignal races, or ofetch's native Node dispatcher path beyond undici-backed global fetch.
How much smaller is ofetch?
| Build (esbuild browser minify) | Minified | gzip -9 |
|---|---|---|
| ofetch | 10,114 B | 4,057 B |
| ky | 28,594 B | 9,779 B |
| ofetch.create wrapper | 10,127 B | 4,070 B |
| ky.create wrapper | 28,611 B | 9,787 B |
About 2.4× smaller gzip for ofetch. Track client cost the same way as size-limit vs bundlesize. If you are already comparing ky to axios, see ky vs axios. Cookie helpers that often sit next to API clients show up in cookie-es vs cookie.
Does plain GET speed matter?
| Call cost (800 GETs, median of 9, 2 order-reversed runs, µs/req) | ofetch | ky |
|---|---|---|
| Absolute URL + auto/manual JSON | 183.8 / 129.7 | 179.8 / 203.5 |
| create() + relative path | 118.8 / 125.8 | 300.4 / 225.7 |
Plain GETs were within noise across order-reversed runs. ofetch.create stayed clearly cheaper on this box (~2×). That gap is still tiny next to real network RTT. If your bottleneck is cloning response payloads instead of fetching them, compare klona vs structuredClone. Hashing cache keys sits next to ohash vs object-hash.
Do the APIs behave the same?
| Topic | ofetch 1.5.1 | ky 2.1.0 |
|---|---|---|
| Success JSON | auto-parsed object | Response; call .json() |
| Query string | query: { a, b } | searchParams: { a, b } |
| Client factory | ofetch.create({ baseURL }) | ky.create({ prefix }) |
| 404 error | FetchError with .status + .data | HTTPError with .response |
| Retry default | 1 | 2 on safe methods |
Query encoding matched on my echo fixture. Base-path clients both returned { a: 1 }. Error shapes did not. For URL joining before the request, see ufo vs URL. Soft JSON parse helpers show up in destr vs superjson.
Which one should you pick?
| Situation | My pick |
|---|---|
| Nuxt / unjs stack, want auto JSON | ofetch |
| Smallest browser client for JSON APIs | ofetch |
| Want Response / ReadableStream control | ky |
| Prefer retry=2 on GET by default | ky |
| Migrating from axios with interceptors | re-check ky first (ky vs axios) |
Bottom line: ofetch 1.5.1 was 4,057 B gzip versus 9,779 B for ky 2.1.0, and ofetch.create was about 2× cheaper per local request on this box. Who should not switch: codebases that treat every call as a Response object, or teams standardized on ky's HTTPError + retry defaults. Shell-out helpers in the same decision lane sit next to tinyexec vs execa.
How this was made: I installed both packages on the ShopperCove box, measured esbuild browser bundles, timed sequential GETs against a local http.Server in two order-reversed runs, and compared JSON/text/404/query/create behavior. The JSON results sit next to the scripts. The write-up was drafted with AI help and checked against that output.
Sources
- https://github.com/unjs/ofetch
- https://github.com/sindresorhus/ky
- https://www.npmjs.com/package/ofetch
- https://www.npmjs.com/package/ky
Related
- https://www.shoppercove.com/blog/ky-vs-axios
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/cookie-es-vs-cookie
- https://www.shoppercove.com/blog/ufo-vs-url
- https://www.shoppercove.com/blog/destr-vs-superjson
- https://www.shoppercove.com/blog/klona-vs-structuredclone
- https://www.shoppercove.com/blog/ohash-vs-object-hash
- https://www.shoppercove.com/blog/tinyexec-vs-execa
Lab evidence
What I found running this
Hands-on on ShopperCove box 6 Oct 2026 ~02:15 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 22.20.0). ofetch 1.5.1 vs ky 2.1.0, esbuild 0.28.2 browser ESM minify + gzip -9. Size: ofetch 10,114/4,057 B vs ky 28,594/9,779 B; create 10,127/4,070 vs 28,611/9,787. Speed (800 sequential local JSON GETs, 9-round median, order-reversed, µs/req): plain ofetch 183.8 / 129.7 vs ky 179.8 / 203.5; create ofetch 118.8 / 125.8 vs ky 300.4 / 225.7. Behavior: ofetch auto-JSON + FetchError.data on 404; ky Response/.json() + HTTPError; query/searchParams matched; create baseURL vs prefix both worked. Not tested: browsers, hooks under load, upload streams, AbortSignal races. No affiliate.