# How to Upload Files in Node.js Using Native Streams and the Fetch API

> Learn efficient methods to upload files in Node.js using native streams and the Fetch API. Avoid memory buffering with these advanced techniques for better performance.

- Repository: [Node.js/node](https://github.com/nodejs/node)
- Tags: how-to-guide
- Published: 2026-02-19

---

**The most efficient methods to upload files in Node.js applications are native HTTP streaming using `fs.createReadStream()` piped to `http.request()`, and the built-in `fetch` API with `FormData` for multipart uploads, both of which avoid buffering entire files in memory.**

Uploading files efficiently in Node.js requires streaming data directly from disk to the network to prevent memory exhaustion. The `nodejs/node` repository provides several built-in mechanisms for high-performance file uploads, ranging from low-level HTTP streams to the modern `fetch` API powered by Undici. This guide examines the most common and efficient methods to upload files in Node.js applications using only core modules and the built-in fetch implementation.

## Native HTTP Streaming with `http` and `https` Modules

### Architecture and Core Implementation

The native `http` and `https` modules provide the foundation for zero-copy file uploads in Node.js. The implementation resides in **[`lib/http.js`](https://github.com/nodejs/node/blob/main/lib/http.js)** and **[`lib/https.js`](https://github.com/nodejs/node/blob/main/lib/https.js)**, where the `request()` method returns a `ClientRequest` object that inherits from `Writable` stream (specifically `OutgoingMessage`).

This architecture allows you to pipe a file stream directly into the HTTP request without intermediate buffering. The kernel handles the data transfer from the file descriptor to the network socket, making this approach optimal for large files.

### Streaming Upload Implementation

To implement a streaming upload, create a readable file stream using `fs.createReadStream()` and pipe it to the request object:

```javascript
// upload-http.js
import { createReadStream } from 'fs';
import { request } from 'https'; // or 'http' for plain HTTP

const filePath = '/path/to/large-file.zip';
const options = {
  hostname: 'example.com',
  port: 443,
  path: '/api/upload',
  method: 'POST',
  headers: {
    'Content-Type': 'application/octet-stream',
    // Content-Length can be omitted; Node uses chunked transfer encoding
  },
};

const req = request(options, (res) => {
  console.log(`Server responded with ${res.statusCode}`);
  res.on('data', (chunk) => process.stdout.write(chunk));
});

req.on('error', (err) => console.error('Upload error:', err));

// Stream the file directly into the request
createReadStream(filePath).pipe(req);

```

For production reliability, use **`stream.pipeline`** from **[`lib/stream/promises.js`](https://github.com/nodejs/node/blob/main/lib/stream/promises.js)** to handle errors and ensure proper stream cleanup:

```javascript
import { pipeline } from 'stream';
import { createReadStream } from 'fs';
import { request } from 'https';

const req = request({ /* options */ });

pipeline(
  createReadStream(filePath),
  req,
  (err) => {
    if (err) console.error('Pipeline failed:', err);
    else console.log('Upload completed successfully');
  }
);

```

### Memory Efficiency and Back-Pressure

Native HTTP streaming provides **zero-copy** efficiency where data flows from the file system to the network without being buffered in the JavaScript heap. The pipe mechanism respects **back-pressure** through the `highWaterMark` property, automatically pausing the file read when the network buffer fills, preventing memory spikes during large uploads.

## Multipart Uploads Using the Built-in `fetch` API and Undici

### FormData and Automatic Boundary Handling

Since Node.js 18, the global `fetch` implementation is powered by **Undici**, located in **[`deps/undici/undici.js`](https://github.com/nodejs/node/blob/main/deps/undici/undici.js)**. For multipart uploads, Undici provides a native `FormData` implementation that automatically handles boundary generation and MIME type negotiation.

The multipart parsing logic resides in **[`deps/undici/src/lib/web/fetch/formdata-parser.js`](https://github.com/nodejs/node/blob/main/deps/undici/src/lib/web/fetch/formdata-parser.js)**, which serializes the form data into a streaming body suitable for HTTP transmission. This eliminates the need to manually construct `--boundary` strings or calculate content lengths.

### Streaming Multipart Implementation

To upload files using the modern fetch API, create a `FormData` instance and append file streams along with metadata:

```javascript
// upload-fetch.js
import { createReadStream } from 'fs';
import { FormData, fileFromPath } from 'node:fetch'; // native fetch globals

const form = new FormData();
form.append('metadata', JSON.stringify({ userId: 123 }));
form.append('file', createReadStream('/path/to/photo.jpg'), {
  filename: 'photo.jpg',
  contentType: 'image/jpeg',
});

const response = await fetch('https://example.com/api/upload', {
  method: 'POST',
  body: form,
});

if (!response.ok) {
  throw new Error(`Upload failed: ${response.status}`);
}
const result = await response.json();
console.log('Server response:', result);

```

This approach maintains streaming efficiency because Undici’s `FormData` implementation accepts `Readable` streams and emits them as part of the multipart body without buffering the entire payload in memory.

### Advantages Over Manual Multipart Construction

Using the built-in `fetch` API with `FormData` provides three critical advantages for uploading files in Node.js applications:

- **Automatic boundary generation** – The `Content-Type: multipart/form-data; boundary=…` header is generated correctly without manual string concatenation.
- **Native streaming support** – File streams are piped directly into the HTTP body, maintaining back-pressure throughout the upload.
- **Async/await compatibility** – The Promise-based API integrates cleanly with modern JavaScript patterns and error handling.

## Third-Party Libraries for Advanced Scenarios

While native methods provide the highest performance for uploading files in Node.js, third-party libraries offer convenience features for specific requirements:

| Library | Best For | Underlying Mechanism |
|---------|----------|---------------------|
| **`form-data`** | Explicit boundary control and legacy compatibility | Builds a `CombinedStream` that pipes to `http.request` |
| **`axios`** | Request interceptors, automatic JSON parsing, and progress events | Wraps native `http`/`https` with Promise-based ergonomics |
| **`got`** | Retry logic, timeout handling, and advanced stream composition | Uses native requests with `pipeline`-style streaming |
| **`multer`** / **`busboy`** | Server-side multipart parsing and file storage | Implements streaming parsers similar to Undici's `formdata-parser` |

Choose these libraries when you require **upload progress bars**, **automatic retries on network failure**, or **server-side file handling** beyond simple HTTP client functionality.

## Summary

- **Native HTTP streaming** via `fs.createReadStream()` piped to `http.request()` provides zero-copy efficiency and minimal memory footprint for large files.
- **Built-in `fetch` API** with `FormData` (Undici) automatically handles multipart boundaries while maintaining streaming performance for HTML-form-style uploads.
- **`stream.pipeline`** from [`lib/stream/promises.js`](https://github.com/nodejs/node/blob/main/lib/stream/promises.js) ensures proper error handling and resource cleanup when piping file streams to HTTP requests.
- **Third-party libraries** like `axios` or `got` add convenience features but use the same core streaming mechanisms under the hood.

## Frequently Asked Questions

### What is the most memory-efficient way to upload files in Node.js?

The most memory-efficient approach uses `fs.createReadStream()` piped directly to `http.request()` or `https.request()`. This method, implemented in [`lib/http.js`](https://github.com/nodejs/node/blob/main/lib/http.js) and [`lib/https.js`](https://github.com/nodejs/node/blob/main/lib/https.js), leverages zero-copy streaming where data flows from the file descriptor to the network socket without buffering in the JavaScript heap. For multipart uploads, the built-in `fetch` API with `FormData` (powered by Undici in [`deps/undici/undici.js`](https://github.com/nodejs/node/blob/main/deps/undici/undici.js)) also streams data efficiently without loading entire files into memory.

### Can I use the native fetch API to upload large files without buffering?

Yes, the native `fetch` API available since Node.js 18 supports streaming uploads through `FormData` and readable streams. When you append a `fs.createReadStream()` to a `FormData` instance, Undici's implementation (located in [`deps/undici/src/lib/web/fetch/formdata-parser.js`](https://github.com/nodejs/node/blob/main/deps/undici/src/lib/web/fetch/formdata-parser.js)) streams the file content directly into the HTTP body using chunked transfer encoding. This maintains back-pressure and prevents memory exhaustion even with multi-gigabyte files.

### When should I use third-party libraries like axios or multer instead of native methods?

Use third-party libraries when you need features beyond basic streaming. Choose `axios` or `got` when you require upload progress events, automatic retries, request interceptors, or simplified error handling. Use `multer` or `busboy` specifically for server-side applications where you need to parse incoming multipart/form-data requests and save uploaded files to disk or memory. These libraries ultimately rely on the same native `http` and stream modules but add ergonomic abstractions for complex workflows.

### How do I handle upload progress in native Node.js HTTP requests?

To track upload progress with native `http.request()`, attach a listener to the request's `socket` event and monitor the `socket.bytesWritten` property, or pipe the file stream through a custom transform stream that counts bytes. Alternatively, use the `data` event on the source read stream to calculate progress before it enters the request. For precise progress tracking with less boilerplate, third-party libraries like `axios` provide `onUploadProgress` callbacks that abstract these native socket events.