# Fetch (/runtime/networking/fetch)

<!-- agent-signals: reading_time_min: 15 · est_tokens: 5658 · updated: 2026-09-23 -->
Related: [DNS](/runtime/networking/dns.md), [TCP](/runtime/networking/tcp.md), [UDP](/runtime/networking/udp.md)

Bun implements the WHATWG `fetch` standard, with some extensions to meet the needs of server-side JavaScript.

Bun also implements `node:http`, but we generally recommend `fetch` instead.

## Sending an HTTP request [#sending-an-http-request]

To send an HTTP request, use `fetch`:

```ts
const response = await fetch("http://example.com");

console.log(response.status); // => 200

const text = await response.text(); // or response.json(), response.formData(), etc.
```

`fetch` also works with HTTPS URLs.

```ts
const response = await fetch("https://example.com");
```

You can also pass `fetch` a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object.

```ts
const request = new Request("http://example.com", {
  method: "POST",
  body: "Hello, world!",
});

const response = await fetch(request);
```

### Sending a POST request [#sending-a-post-request]

To send a POST request, pass an object with the `method` property set to `"POST"`.

```ts
const response = await fetch("http://example.com", {
  method: "POST",
  body: "Hello, world!",
});
```

`body` can be a string, a `FormData` object, an `ArrayBuffer`, a `Blob`, or another of the body types listed in the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#setting_a_body).

### Proxying requests [#proxying-requests]

To proxy a request, pass an object with the `proxy` property set to a URL string, a `URL` instance, or to an object whose `url` is a string or a `URL`:

```ts
const response = await fetch("http://example.com", {
  proxy: "http://proxy.com",
});
```

To send custom headers to the proxy server, pass an object instead:

```ts
const response = await fetch("http://example.com", {
  proxy: {
    url: "http://proxy.com",
    headers: {
      "Proxy-Authorization": "Bearer my-token",
      "X-Custom-Proxy-Header": "value",
    },
  },
});
```

Bun sends the `headers` directly to the proxy in `CONNECT` requests (for HTTPS targets) or in the proxy request (for HTTP targets). If you provide a `Proxy-Authorization` header, it overrides any credentials in the proxy URL.

Without a `proxy` option, Bun reads `HTTP_PROXY`, `HTTPS_PROXY` and `ALL_PROXY` (upper or lower case) each time `fetch` runs, so changes to `process.env` apply to the next request. Bun uses `ALL_PROXY`, for `http:` and `https:` URLs alike, when the scheme-specific variable is unset. It ignores a value that names another kind of proxy, such as `socks5://`. curl reads `ALL_PROXY` too; Node.js does not. A request with the `unix` option never uses the proxy environment.

`no_proxy` lists hosts that bypass the proxy. Bun reads `NO_PROXY` when `no_proxy` is unset or empty, as curl and Node.js do. Entries are separated by commas or whitespace.

| Entry                                          | Matches                          |
| ---------------------------------------------- | -------------------------------- |
| `*`                                            | every host                       |
| `example.com`, `.example.com`, `*.example.com` | `example.com` and its subdomains |
| `127.0.0.1`, `::1`, `[::1]`                    | that IP address                  |
| `10.0.0.0/8`, `fd00::/8`                       | every IP address in the block    |
| `example.com:8080`                             | the host, on that port only      |

`NO_PROXY` also applies to an explicit `proxy`. Set `respectNoProxy: false` to send the request through the proxy regardless, and `proxy: false` to connect directly regardless of the environment:

```ts
await fetch("https://example.com", {
  proxy: { url: "http://proxy.com", respectNoProxy: false },
});

await fetch("https://example.com", { proxy: false });
```

When the proxy answers `CONNECT` with anything but a 2xx status, `fetch` rejects with an `ERR_PROXY_TUNNEL` error. The error carries the proxy's `status`, `statusText` and `headers`.

```ts
try {
  await fetch("https://example.com", { proxy: "http://proxy.com" });
} catch (error) {
  if (error.code === "ERR_PROXY_TUNNEL") {
    console.log(error.status, error.headers.get("proxy-authenticate"));
  }
}
```

### Custom headers [#custom-headers]

To set custom headers, pass an object with the `headers` property set to an object.

```ts
const response = await fetch("http://example.com", {
  headers: {
    "X-Custom-Header": "value",
  },
});
```

You can also set headers using the [Headers](https://developer.mozilla.org/en-US/docs/Web/API/Headers) object.

```ts
const headers = new Headers();
headers.append("X-Custom-Header", "value");

const response = await fetch("http://example.com", {
  headers,
});
```

Bun sends credentials in the URL (`https://user:password@example.com/`) as a `Basic` `Authorization` header, unless the request sets an `Authorization` header itself. Node.js's `fetch` rejects such a URL instead. The `path` of a connection error leaves the credentials out.

### Response bodies [#response-bodies]

To read the response body, use one of the following methods:

* `response.text(): Promise<string>`: Returns a promise that resolves with the response body as a string.
* `response.json(): Promise<any>`: Returns a promise that resolves with the response body as a JSON object.
* `response.formData(): Promise<FormData>`: Returns a promise that resolves with the response body as a `FormData` object.
* `response.bytes(): Promise<Uint8Array>`: Returns a promise that resolves with the response body as a `Uint8Array`.
* `response.arrayBuffer(): Promise<ArrayBuffer>`: Returns a promise that resolves with the response body as an `ArrayBuffer`.
* `response.blob(): Promise<Blob>`: Returns a promise that resolves with the response body as a `Blob`.

#### Streaming response bodies [#streaming-response-bodies]

You can use async iterators to stream the response body.

```ts
const response = await fetch("http://example.com");

for await (const chunk of response.body) {
  console.log(chunk);
}
```

You can also access the `ReadableStream` directly.

```ts
const response = await fetch("http://example.com");

const stream = response.body;

const reader = stream.getReader();
const { value, done } = await reader.read();
```

### Streaming request bodies [#streaming-request-bodies]

You can also stream data in request bodies using a `ReadableStream`:

```ts
const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("Hello");
    controller.enqueue(" ");
    controller.enqueue("World");
    controller.close();
  },
});

const response = await fetch("http://example.com", {
  method: "POST",
  body: stream,
});
```

When using streams with HTTP(S):

* Bun streams the data directly to the network without buffering the entire body in memory
* If the connection is lost, Bun cancels the stream
* Bun sets the `Content-Length` header automatically only when the stream has a known size

When using streams with S3:

* For PUT/POST requests, Bun automatically uses multipart upload
* Bun consumes the stream in chunks and uploads the chunks in parallel
* You can monitor progress through the S3 options

### Fetching a URL with a timeout [#fetching-a-url-with-a-timeout]

To fetch a URL with a timeout, use `AbortSignal.timeout`:

```ts
const response = await fetch("http://example.com", {
  signal: AbortSignal.timeout(1000),
});
```

#### Canceling a request [#canceling-a-request]

To cancel a request, use an `AbortController`:

```ts
const controller = new AbortController();

const response = await fetch("http://example.com", {
  signal: controller.signal,
});

controller.abort();
```

### Unix domain sockets [#unix-domain-sockets]

To fetch a URL using a Unix domain socket, use the `unix: string` option:

```ts
const response = await fetch("https://hostname/a/path", {
  unix: "/var/run/path/to/unix.sock",
  method: "POST",
  body: JSON.stringify({ message: "Hello from Bun!" }),
  headers: {
    "Content-Type": "application/json",
  },
});
```

### TLS [#tls]

To use a client certificate, use the `tls` option:

```ts
await fetch("https://example.com", {
  tls: {
    key: Bun.file("/path/to/key.pem"),
    cert: Bun.file("/path/to/cert.pem"),
    // ca: [Bun.file("/path/to/ca.pem")],
  },
});
```

#### Custom TLS Validation [#custom-tls-validation]

To customize TLS validation, use the `checkServerIdentity` option in `tls`:

```ts
await fetch("https://example.com", {
  tls: {
    checkServerIdentity: (hostname, peerCertificate) => {
      // Return an Error if the certificate is invalid
    },
  },
});
```

This option is similar to the one in Node's `tls` module. Bun calls it after the certificate chain verifies. With `rejectUnauthorized: false` Bun still calls it and ignores what it returns.

A request that passes its own `checkServerIdentity` opens a connection of its own and closes it afterwards, so that another request's function never vouches for it. To verify once per connection and reuse the connection, put the function on a [`Bun.FetchSession`](#fetch-sessions).

#### Disable TLS validation [#disable-tls-validation]

To disable TLS validation, set `rejectUnauthorized` to `false`:

```ts
await fetch("https://example.com", {
  tls: {
    rejectUnauthorized: false,
  },
});
```

This avoids SSL errors with self-signed certificates, but it disables TLS validation, so use it with caution.

### Request options [#request-options]

In addition to the standard fetch options, Bun provides several extensions:

```ts
const response = await fetch("http://example.com", {
  // Control automatic response decompression (default: true)
  // Supports gzip, deflate, brotli (br), and zstd
  decompress: true,

  // Disable connection reuse for this request
  keepalive: false,

  // Debug logging level
  verbose: true, // or "curl" for more detailed output
});
```

### Protocol support [#protocol-support]

Beyond HTTP(S), Bun's fetch supports several additional protocols:

#### S3 URLs - `s3://` [#s3-urls---s3]

Bun supports fetching from S3 buckets directly.

```ts
// Using environment variables for credentials
const response = await fetch("s3://my-bucket/path/to/object");

// Or passing credentials explicitly
const response = await fetch("s3://my-bucket/path/to/object", {
  s3: {
    accessKeyId: "YOUR_ACCESS_KEY",
    secretAccessKey: "YOUR_SECRET_KEY",
    region: "us-east-1",
  },
});
```

Only PUT and POST methods support request bodies when using S3. For uploads, Bun automatically uses multipart upload for streaming bodies.

See the [S3](/runtime/s3) documentation.

#### File URLs - `file://` [#file-urls---file]

You can fetch local files using the `file:` protocol:

```ts
const response = await fetch("file:///path/to/file.txt");
const text = await response.text();
```

The URL's host must be empty or `localhost`. `fetch("file://other.host/path")` rejects with `ERR_INVALID_FILE_URL_HOST`.

On Windows, Bun normalizes paths automatically:

```ts
// Both work on Windows
const response = await fetch("file:///C:/path/to/file.txt");
const response2 = await fetch("file:///c:/path\\to/file.txt");
```

#### Data URLs - `data:` [#data-urls---data]

Bun supports the `data:` URL scheme:

```ts
const response = await fetch("data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==");
const text = await response.text(); // "Hello, World!"
```

#### Blob URLs - `blob:` [#blob-urls---blob]

You can fetch blobs using URLs created by `URL.createObjectURL()`:

```ts
const blob = new Blob(["Hello, World!"], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const response = await fetch(url);
```

### Error handling [#error-handling]

Bun's fetch implementation includes several specific error cases:

* Using a request body with GET/HEAD methods throws an error (which is expected for the fetch API)
* Using the `proxy` and `unix` options together throws an error
* Connection failures reject with a `TypeError` whose `code` is the socket error (`ECONNREFUSED`, `ECONNRESET`, `ETIMEDOUT`, `ENOTFOUND`, ...). The message starts with the code.
* TLS certificate validation failures when `rejectUnauthorized` is true (or undefined)
* S3 operations may throw specific errors related to authentication or permissions

### Content-Type handling [#content-type-handling]

Bun automatically sets the `Content-Type` header for request bodies when not explicitly provided:

* For `Blob` objects, uses the blob's `type`
* For `FormData`, sets appropriate multipart boundary

## Debugging [#debugging]

For debugging, pass `verbose: true` to `fetch`:

```ts
const response = await fetch("http://example.com", {
  verbose: true,
});
```

This prints the request and response headers to your terminal:

```sh
[fetch] > HTTP/1.1 GET http://example.com/
[fetch] > Connection: keep-alive
[fetch] > User-Agent: Bun/1.3.3
[fetch] > Accept: */*
[fetch] > Host: example.com
[fetch] > Accept-Encoding: gzip, deflate, br, zstd

[fetch] < 200 OK
[fetch] < Content-Encoding: gzip
[fetch] < Age: 201555
[fetch] < Cache-Control: max-age=604800
[fetch] < Content-Type: text/html; charset=UTF-8
[fetch] < Date: Sun, 21 Jul 2024 02:41:14 GMT
[fetch] < Etag: "3147526947+gzip"
[fetch] < Expires: Sun, 28 Jul 2024 02:41:14 GMT
[fetch] < Last-Modified: Thu, 17 Oct 2019 07:18:26 GMT
[fetch] < Server: ECAcc (sac/254F)
[fetch] < Vary: Accept-Encoding
[fetch] < X-Cache: HIT
[fetch] < Content-Length: 648
```

`verbose: boolean` is a Bun-specific extension, not part of the Web standard `fetch` API.

## Performance [#performance]

Before an HTTP request can be sent, Bun has to resolve DNS, connect the TCP socket, and sometimes complete a TLS handshake. Each step takes time, especially over a slow DNS server or a poor network connection. After the request completes, consuming the response body also takes time and memory.

Bun provides APIs to optimize each of these steps.

### DNS prefetching [#dns-prefetching]

Use `dns.prefetch` when you know you'll connect to a host soon and want to avoid the initial DNS lookup.

```ts
import { dns } from "bun";

dns.prefetch("bun.com");
```

#### DNS caching [#dns-caching]

By default, Bun caches and deduplicates DNS queries in-memory for up to 30 seconds. `dns.getCacheStats()` returns the cache stats.

See [DNS caching](/runtime/networking/dns).

### Preconnect to a host [#preconnect-to-a-host]

`fetch.preconnect` starts the DNS lookup, TCP socket connection, and TLS handshake for a host before you're ready to send a request to it.

```ts
import { fetch } from "bun";

fetch.preconnect("https://bun.com");
```

Calling `fetch` immediately after `fetch.preconnect` does not make your request faster. Preconnecting only helps when there's a gap between knowing the host and sending the request.

#### Preconnect at startup [#preconnect-at-startup]

To preconnect to a host at startup, pass `--fetch-preconnect`:

```sh
bun --fetch-preconnect https://bun.com ./my-script.ts
```

`--fetch-preconnect` is similar to `<link rel="preconnect">` in HTML. It is not implemented on Windows; if you need it there, file an issue.

### Connection pooling & HTTP keep-alive [#connection-pooling--http-keep-alive]

Bun automatically reuses connections to the same host. This is called **connection pooling**, and it can significantly reduce the time spent establishing connections.

#### Fetch sessions [#fetch-sessions]

A `Bun.FetchSession` holds connection settings and owns a keep-alive pool. `session.fetch` is `fetch` with that session, and it is bound, so it works wherever a `fetch` function is accepted (`new SomeClient({ fetch: session.fetch })`). It has no `preconnect`. `fetch(url, { session })` does the same through the global `fetch`. Bun never shares a connection between two sessions, or between a session and plain `fetch`.

```ts
const session = new Bun.FetchSession({
  tls: { ca: await Bun.file("corp-ca.pem").text() },
  proxy: { url: "http://proxy.internal:8080", respectNoProxy: false },
  keepAlive: { idleTimeout: 30, maxIdleSockets: 8 },
});

const response = await session.fetch("https://example.com");
session.close(); // closes the idle connections
```

| Option      | Description                                                                                                                                                                                                               |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tls`       | The same options as `fetch`'s `tls`. `checkServerIdentity` runs once per connection.                                                                                                                                      |
| `proxy`     | The same values as `fetch`'s `proxy`, including `false`.                                                                                                                                                                  |
| `keepAlive` | `false` closes each connection after its response. `idleTimeout` is in seconds (default 300). `maxIdleSockets` caps the idle connections per kind (plain, TLS, Unix socket). Neither limit applies to HTTP/3 connections. |
| `unix`      | A Unix socket path.                                                                                                                                                                                                       |

An option given on the request takes precedence over the session's, `proxy: false` included. A request's `tls` replaces the session's `tls` as a whole.

The constructor throws for a `proxy` or a `tls.checkServerIdentity` that it cannot use, such as `proxy: 8080`, a `proxy` object without a `url`, or a `checkServerIdentity` that is not a function. For `proxy`, only `undefined`, `null` and `""` mean no option. On a request, `fetch` ignores such a `proxy` (`true` throws), and the request keeps the proxy of its session or of the environment.

Bun rounds `idleTimeout` up to its socket timer, which moves in 4 second steps up to four minutes and in whole minutes beyond that. A connection is never closed before `idleTimeout`, and it can stay open for up to two steps longer. The longest `idleTimeout` is 238 minutes. A larger value means 238 minutes.

Without `maxIdleSockets` a session has no limit of its own. The idle connections of all sessions and of plain `fetch` share one bounded pool per kind. When that pool is full, Bun closes the longest-idle connection in it, whichever session it belongs to. A `maxIdleSockets` above the size of the pool has no effect.

#### Connecting to a specific address [#connecting-to-a-specific-address]

To send a request for a hostname to an address you resolved and checked yourself, put the address in the URL and name the origin in the `Host` header and in `tls.serverName`. Bun dials the address, sends `Host` (as `:authority` over HTTP/2), uses `serverName` for SNI, and verifies the certificate against `serverName`.

```ts
import { lookup } from "node:dns/promises";

const { address, family } = await lookup("example.com");
if (isPrivateAddress(address)) throw new Error(`refusing to connect to ${address}`);

const response = await fetch(`https://${family === 6 ? `[${address}]` : address}/webhook`, {
  headers: { Host: "example.com" },
  tls: { serverName: "example.com" },
  proxy: false,
  redirect: "manual",
});
```

* Pass `proxy: false`. Otherwise Bun judges the environment's proxy and `NO_PROXY` against the address in the URL, not against the hostname. Decide whether the hostname needs a proxy before you resolve it.
* Follow redirects yourself (`redirect: "manual"`): `Host` and `serverName` describe this origin only, and a relative `Location` resolves against the address.
* A name with several addresses has no fallback here. Try each address yourself.
* Bun sends `Host` as given. Include the port when it is not the scheme's default. A plain `http://` URL needs only `Host`.
* `tls.serverName` works with HTTP/1.1 and HTTP/2. `protocol: "http3"` rejects it.
* Each distinct `serverName` has its own TLS context and keep-alive pool. Bun caches up to 60 of them.

#### Simultaneous connection limit [#simultaneous-connection-limit]

By default, Bun limits the number of simultaneous `fetch` requests to 256, for two reasons:

* It improves overall system stability. Operating systems have an upper limit on the number of simultaneous open TCP sockets, usually in the low thousands. Nearing this limit causes your entire computer to behave strangely. Applications hang and crash.
* It encourages HTTP Keep-Alive connection reuse. For short-lived HTTP requests, the slowest step is often the initial connection setup. Reusing connections can save a lot of time.

When the limit is exceeded, Bun queues requests and sends them as soon as the next request ends.

To raise the limit, set the `BUN_CONFIG_MAX_HTTP_REQUESTS` environment variable:

```sh
BUN_CONFIG_MAX_HTTP_REQUESTS=512 bun ./my-script.ts
```

The max value for this limit is 65,535. The maximum port number is 65,535, so it's quite difficult for any one computer to exceed this limit.

### Response buffering [#response-buffering]

The fastest way to read the response body is to use one of these methods:

* `response.text(): Promise<string>`
* `response.json(): Promise<any>`
* `response.formData(): Promise<FormData>`
* `response.bytes(): Promise<Uint8Array>`
* `response.arrayBuffer(): Promise<ArrayBuffer>`
* `response.blob(): Promise<Blob>`

You can also use `Bun.write` to write the response body to a file on disk:

```ts
import { write } from "bun";

await write("output.txt", response);
```

### Implementation details [#implementation-details]

* Connection pooling is enabled by default. You can disable it per-request with `keepalive: false` or the `"Connection: close"` header.
* Bun optimizes large file uploads using the operating system's `sendfile` syscall under specific conditions:
  * The file must be larger than 32KB
  * The request must not be using a proxy or the `compress` option
  * On macOS, only regular files (not pipes, sockets, or devices) can use `sendfile`
  * When these conditions aren't met, or when using S3/streaming uploads, Bun falls back to reading the file into memory
  * Bun only uses this optimization for HTTP (not HTTPS) requests, where the file can be sent directly from the kernel to the network stack; Bun does not use it on Windows
* S3 operations automatically handle signing requests and merging authentication headers

Many of these features are Bun-specific extensions to the standard fetch API.
