Axios vs ky in 2026: HTTP Clients for Modern JavaScript
TL;DR
Axios for Node.js-heavy backends; ky for modern browser/edge environments. Axios (~50M weekly downloads) is the default HTTP client for most Node.js projects — reliable, widely documented, and works everywhere from Node 12 to the browser. ky (~4M downloads) is a ~5KB Fetch-based alternative that works natively in all modern environments including Cloudflare Workers, Deno, and Bun. If you're targeting edge runtimes or want a smaller browser bundle, ky is compelling. If you're on an existing Node.js backend or need maximum compatibility, Axios wins on ecosystem familiarity.
Key Takeaways
- Axios: ~50M weekly downloads — ky: ~4M (npm, March 2026)
- Axios is ~40KB gzipped — ky is ~5KB gzipped (8x smaller)
- Axios uses XMLHttpRequest in browsers — ky wraps the native Fetch API everywhere
- ky works in all edge runtimes — Cloudflare Workers, Vercel Edge, Deno, Bun natively
- ky has built-in retry with exponential backoff — Axios requires
axios-retryplugin - Both support interceptors — ky calls them hooks; Axios calls them interceptors
- Both have excellent TypeScript support — generics for typed response bodies
Bundle Size: Why It Matters for Browser Projects
Bundle size is the most concrete difference between these two libraries and the primary reason to consider ky for browser-side code. Axios at ~40KB gzipped is large for a single utility library. On slow connections or mobile networks, that's meaningful extra time to first interactive.
ky at ~5KB gzipped achieves this by doing one thing: wrapping the Fetch API with a better API surface. It doesn't implement HTTP from scratch, doesn't polyfill XMLHttpRequest, and doesn't carry the compatibility shims that Axios needs to work on older runtimes. If your users' browsers support fetch — which every browser released after 2017 does — ky has everything it needs.
For Next.js apps, this matters particularly for client components. If your client-side code calls an API, ky's 5KB vs Axios's 40KB directly affects your initial JavaScript budget. Server-side (in Server Components or API routes), the difference is less impactful since bundle size doesn't affect server startup meaningfully.
# Bundle size comparison (gzip)
axios: ~40KB (includes full HTTP implementation)
ky: ~5KB (wraps native Fetch)
node-fetch: ~8KB (Fetch polyfill for older Node)
native fetch: 0KB (built into the runtime)
Basic Usage: Familiar API Surface
Both libraries provide a similar API surface for common operations. If you know one, you can read the other.
// Axios — automatic JSON parsing, classic API
import axios from 'axios';
interface User {
id: number;
name: string;
email: string;
}
// GET with typed response
const { data } = await axios.get<User>('/api/users/123');
// data: User — fully typed
// POST with JSON body (automatic serialization)
const { data: newUser } = await axios.post<User>('/api/users', {
name: 'Alice',
email: 'alice@example.com',
});
// Setting headers
const { data: privateData } = await axios.get('/api/private', {
headers: { Authorization: `Bearer ${token}` },
});
// Error handling — throws on 4xx/5xx (unlike native fetch)
try {
await axios.get('/api/missing');
} catch (err) {
if (axios.isAxiosError(err)) {
console.log(err.response?.status); // 404
console.log(err.response?.data); // { error: "Not found" }
}
}
// ky — Fetch-based, chainable .json() for parsing
import ky from 'ky';
interface User {
id: number;
name: string;
email: string;
}
// GET with typed JSON response
const user = await ky.get('/api/users/123').json<User>();
// user: User — fully typed
// POST with JSON body
const newUser = await ky.post('/api/users', {
json: { name: 'Alice', email: 'alice@example.com' },
}).json<User>();
// Setting headers
const privateData = await ky.get('/api/private', {
headers: { Authorization: `Bearer ${token}` },
}).json();
// Error handling — ky throws HTTPError on 4xx/5xx
try {
await ky.get('/api/missing').json();
} catch (err) {
if (err instanceof ky.HTTPError) {
console.log(err.response.status); // 404
const body = await err.response.json(); // async — response body access
}
}
One subtle difference worth noting: ky.get('/api/missing') returns a Response object before .json() is called. The error is thrown when the response is awaited, but accessing the error response body is async with ky (because it's a Fetch Response). Axios provides the response body synchronously in the error object. This is a real ergonomic difference in error handling code.
Creating Instances and Interceptors
Both libraries support creating pre-configured instances — the pattern used in most real applications to share base URL, headers, and interceptor logic.
// Axios — instances with interceptors
import axios from 'axios';
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
});
// Request interceptor — add auth token
api.interceptors.request.use((config) => {
const token = getAccessToken();
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// Response interceptor — handle token refresh on 401
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 && !error.config._retry) {
error.config._retry = true;
await refreshAccessToken();
return api.request(error.config); // retry original request with new token
}
return Promise.reject(error);
}
);
export { api };
// ky — extended instances with hooks
import ky from 'ky';
const api = ky.create({
prefixUrl: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
hooks: {
beforeRequest: [
(request) => {
const token = getAccessToken();
if (token) request.headers.set('Authorization', `Bearer ${token}`);
},
],
afterResponse: [
async (request, options, response) => {
if (response.status === 401) {
await refreshAccessToken();
// Return a new ky request with updated auth
return ky(request.url, {
...options,
headers: { Authorization: `Bearer ${getAccessToken()}` },
});
}
},
],
},
});
export { api };
ky's hooks system is slightly more explicit about the request/response lifecycle than Axios's interceptors but requires familiarity with the Fetch API's Request and Response objects. Teams coming from Axios may find the initial friction worth it for the edge compatibility gains.
Built-In Retry Logic
This is one area where ky has a concrete advantage out of the box. Retry with exponential backoff is built into ky as a first-class feature. Axios requires the axios-retry package.
// ky — built-in retry, zero extra dependencies
const data = await ky.get('https://api.example.com/flaky-endpoint', {
retry: {
limit: 3,
methods: ['get', 'put'],
statusCodes: [408, 413, 429, 500, 502, 503, 504],
afterStatusCodes: [413, 429, 503], // use Retry-After header for these
backoffLimit: 3000, // max delay (ms) between retries
delay: (attemptCount) => 0.3 * (2 ** (attemptCount - 1)) * 1000, // exponential
},
}).json();
// ky also exposes retry events via beforeRetry hook:
const api = ky.create({
retry: { limit: 3 },
hooks: {
beforeRetry: [
({ request, error, retryCount }) => {
console.log(`Retry attempt ${retryCount} for ${request.url}: ${error.message}`);
},
],
},
});
// Axios — retry requires axios-retry package
import axios from 'axios';
import axiosRetry from 'axios-retry';
const client = axios.create({ baseURL: 'https://api.example.com' });
axiosRetry(client, {
retries: 3,
retryDelay: axiosRetry.exponentialDelay,
retryCondition: (error) =>
axiosRetry.isNetworkError(error) || error.response?.status === 503,
});
const { data } = await client.get('/flaky-endpoint');
axios-retry is a mature and popular package, so this isn't a major weakness for Axios — but it is one more dependency to install and configure, and ky's retry config is more expressive with options like afterStatusCodes for respecting Retry-After headers automatically.
Edge Runtime Compatibility
This is the most decisive factor if you're deploying to Cloudflare Workers, Vercel Edge Functions, or similar environments.
// ky — works anywhere Fetch is available
// Cloudflare Workers:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// ky works natively — Cloudflare Workers have Fetch built in
const data = await ky.get('https://api.external.com/data').json();
return Response.json(data);
},
};
// Vercel Edge Function:
export const config = { runtime: 'edge' };
export default async function handler(req: Request) {
const data = await ky.get('https://api.external.com/data').json();
return new Response(JSON.stringify(data));
}
// Deno:
import ky from 'npm:ky';
const data = await ky.get('https://api.example.com').json();
// Axios — has limited edge runtime support
// Cloudflare Workers: XMLHttpRequest not available → Axios doesn't work natively
// Solution: use axios with the fetch adapter (available in Axios 1.x):
import axios from 'axios';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Requires axios@1.x with explicit fetch adapter
const { data } = await axios.get('https://api.external.com/data', {
adapter: 'fetch', // opt in to Fetch adapter
});
return Response.json(data);
},
};
Axios 1.x added a fetch adapter to address edge runtime compatibility, but it's an opt-in configuration — you have to know to set it. ky simply works because it is Fetch. If your codebase has workers, edge functions, or Deno scripts, ky's compatibility story is significantly simpler.
The Native Fetch Alternative
It's worth acknowledging the elephant in the room: modern JavaScript has fetch built in, and it's quite capable. Neither Axios nor ky is strictly necessary for simple use cases.
// Native fetch — works everywhere modern runtimes exist
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Alice' }),
});
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
const user: User = await response.json();
The native Fetch API requires more boilerplate: you must check response.ok manually (it doesn't throw on 4xx/5xx), you must serialize JSON explicitly, and there's no built-in retry or timeout. ky fills these gaps with minimal bundle cost. Axios fills them with maximum ecosystem coverage and a stable, well-documented API that hasn't changed significantly in years.
For new browser-only or edge-only projects in 2026, the question is increasingly "native Fetch or ky?" rather than "Axios or ky?". For Node.js backends that need HTTP client functionality, Axios's 50M weekly download count reflects a library that's genuinely battle-hardened for that use case.
Package Health
| Package | Weekly Downloads | Bundle Size (gzip) | Last Release | Maintained |
|---|---|---|---|---|
| axios | ~50M | ~40KB | Active (v1.x) | Yes — axios org |
| ky | ~4M | ~5KB | Active (v1.x) | Yes — Sindre Sorhus |
Axios is one of the most downloaded npm packages of all time. ky's download count is lower but growing, particularly as edge and browser use cases expand. Both are actively maintained with recent releases.
When to Choose
Choose Axios when:
- Existing Node.js codebase with Axios already integrated — no reason to migrate
- Node.js 16 or below, where native Fetch isn't available
- You need IE11 or legacy browser support (ky requires native Fetch)
- The team values Axios's extensive documentation, Stack Overflow answers, and tutorials
- You're building a backend-heavy service where bundle size doesn't affect users
- Rich default features (automatic JSON handling, transformers, instance-level defaults) without any setup
Choose ky when:
- Targeting Cloudflare Workers, Vercel Edge, Deno, or Bun (ky is zero-config in these environments)
- Bundle size matters — client-side applications where 35KB+ savings improves Time to Interactive
- You want built-in retry with
Retry-Afterheader support and no extra dependency - Migrating a browser bundle from Axios to something leaner
- Node.js 18+ projects (native Fetch available) where ky adds convenience without Axios's weight
- Modern project where all target environments support Fetch natively
See the full Axios vs ky package comparison on PkgPulse for download trends and bundle size history over time.
For the database layer in Node.js services, see Drizzle vs Kysely for a similar TypeScript-first philosophy applied to SQL query building. If you're evaluating validation for API payloads that your HTTP client sends and receives, see Zod vs TypeBox.
Browse the Axios package details and ky package details on PkgPulse.
See the live comparison
View axios vs. ky on PkgPulse →