# @fastify/multipart: streaming file uploads for Fastify

> The @fastify/multipart plugin parses multipart requests for Fastify on top of a dedicated streaming parser, giving you a file stream rather than a buffer, plus limits that the route level bodyLimit does not enforce. Its documentation is unusually explicit about the ways an upload handler can hang or overflow, and that is where most of its value is.

**fastify/fastify-multipart** — Multipart support for Fastify

- Repository: https://github.com/fastify/fastify-multipart
- Website: https://npmjs.com/package/@fastify/multipart
- Stars: 539 · Forks: 125
- Language: JavaScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/fastify-fastify-multipart

## What the plugin gives you, and what it is built on

The @fastify/multipart plugin is a Fastify plugin to parse the multipart content-type, and the feature list in the README is the API surface in brief: async and await support, an async iterator for handling multiple parts, stream and disk mode, accumulating the entire file in memory, a mode to attach all fields to the request body, and a claim that it is tested across Linux, Mac and Windows.

Under the hood it uses @fastify/busboy, and that detail explains most of the plugin's behaviour, including the sharp edges. This is a streaming parser, so the request is consumed as it arrives rather than being buffered and parsed at the end, and everything about field ordering and stream lifecycle follows from that.

The runtime dependencies in package.json are small and tell the same story. There is @fastify/busboy at 3.0.0 or later for the parsing, @fastify/deepmerge for combining option layers, @fastify/error for the error types, fastify-plugin at 6.0.0 or later for the plugin wrapper that gives the plugin access to the Fastify decorators, and secure-json-parse for safe handling of JSON content types.

The version is 10.1.4 and the package is published under the @fastify scope, with the npm package page as the homepage. The licence is MIT, the last push was on 2026-10-22, and the recent releases are v10.1.4 on 2026-10-15, v10.1.3 on 2026-10-08 and v10.1.2 on 2026-09-22, so the release line moves quickly.

The topic list is just fastify and fastify-plugin, which is accurate for something that sits inside one framework rather than trying to be a general multipart parser.

## The first upload handler, and the two ways it can go wrong

The usage example shows the shape of a handler, and the comments in it are as instructive as the code.

The install is a single command:

```sh
npm i @fastify/multipart
```

Then the plugin is registered and a route awaits req.file(), which returns the first file part along with the other parsed parts:

```js
fastify.post('/', async function (req, reply) {
  // process a single file
  // also, consider that if you allow to upload multiple files
  // you must consume all files otherwise the promise will never fulfill
  const data = await req.file()

  data.file // stream
  data.fields // other parsed parts
  data.fieldname
  data.filename
  data.encoding
  data.mimetype

  await pipeline(data.file, fs.createWriteStream(data.filename))

  reply.send()
})
```

The first failure mode is the one the comment names twice. If you allow multiple files, you must consume all of them, otherwise the promise will never fulfil. The request simply hangs. This is inherited from the underlying parser, and the README repeats it later as a separate note: if the file stream provided by data.file is not consumed, the promise will not be fulfilled at the end of the multipart processing.

The second failure mode is the accumulation one, and the example calls it out with a warning. There is a commented-out line awaiting data.toBuffer() to get the file as a Buffer, with the comment to accumulate the file in memory and be careful. That is a whole file in the server's heap, on the request thread, for every concurrent upload. It is convenient for a small file and a denial of service for a large one.

The rest of the example is the sensible shape. The stream is piped to a file, and the comment warns to be careful of permission issues on disk and not to overwrite sensitive files that could cause security risks. That is a real concern: data.filename comes from the client, so writing it directly to disk is a path traversal waiting to happen unless you sanitise it.

## Field ordering, which is the part that surprises people

The README has a note about data.fields that is worth reading carefully, because it describes a consequence of streaming rather than a bug.

Because the underlying parser consumes the multipart in serial order, the order of form fields is very important to how the plugin can display the fields. The recommendation is to place the value fields first before any of the file fields, which ensures the fields are accessible before it starts consuming any files.

If you cannot control the order of the placed fields, you have to read data.fields after consuming the stream, or it will only contain the fields parsed at that moment.

That is a specific and non-obvious contract. A form that posts a file and a description field can arrive with the description in data.fields if it was sent first, and not arrive at all if the file came first and you read the fields before consuming the stream. The fix is either to control the client, which you often can, or to read the fields afterwards, which you always can.

The same serial-ordering logic explains why the plugin also offers a mode to attach all fields to the request body, which is the attachFieldsToBody option mentioned in the feature list. In that mode the fields are attached to the request body rather than being read from the file part, and the route options then take a multipartOptions key:

```js
fastify.post('/', {
  config: {
      multipartOptions: {
        limits: { fileSize: 1000 }
      }
    }
}, async function (req, reply) {
  const buffer = req.body.file.toBuffer();
  reply.send()
})
```

Per-request options are also supported directly on the call, with the same limits object passed to req.file, req.files, req.saveRequestFiles or req.parts.

## Limits, and the gap the documentation is blunt about

Limits are set by passing options to the underlying parser at registration, and the example enumerates every one of them:

```js
fastify.register(require('@fastify/multipart'), {
  limits: {
    fieldNameSize: 100, // Max field name size in bytes
    fieldSize: 100,     // Max field value size in bytes
    fields: 10,         // Max number of non-file fields
    fileSize: 1000000,  // For multipart forms, the max file size in bytes
    files: 1,           // Max number of file fields
    headerPairs: 2000,  // Max number of header key=>value pairs
    parts: 1000         // For multipart forms, the max number of parts (fields + files)
  }
});
```

The defaults are parts at 1000 and fileSize at the Fastify bodyLimit, which is 1 MiB by default.

Then comes the note that is the single most important sentence in this README for anyone running an upload endpoint in production. Multipart requests are parsed by a dedicated streaming parser and do not go through Fastify's bodyLimit enforcement. bodyLimit is only used to compute the per-file fileSize default, and it is not enforced as an aggregate limit across the whole multipart payload.

The consequence is stated plainly. A single request can contain more data than bodyLimit once spread across many fields and files, and the route level bodyLimit is not applied to the multipart parser. If you need to bound the total request size, you have to set explicit fieldSize, fields, files, parts and fileSize limits, and or enforce a request size limit upstream.

In other words, the bodyLimit that protects every other Fastify route does not protect this one. That is an easy assumption to carry over from the rest of the framework, and the documentation is unusually clear that it is wrong here.

Detecting that a file hit the size limit has three documented routes. You can listen to the limit event on the file stream, check the truncated property at the end of the stream, or call toBuffer and wait for the error to be thrown:

```js
const data = await req.file()
await pipeline(data.file, fs.createWriteStream(data.filename))
if (data.file.truncated) {
  // you may need to delete the part of the file that has been saved on disk
  // before the `limits.fileSize` has been reached
  reply.send(new fastify.multipartErrors.FilesLimitError());
}
```

The comment about deleting the partially saved file is worth noting on its own, because a truncated upload leaves a partial file on disk, and the cleanup is your responsibility.

## Multiple files, fields, temporary files, and how the project is tested

Beyond a single file, there are three more ways to consume a multipart request.

To handle multiple file streams, use the files accessor and iterate:

```js
fastify.post('/', async function (req, reply) {
  const parts = req.files()
  for await (const part of parts) {
    await pipeline(part.file, fs.createWriteStream(part.filename))
  }
  reply.send()
})
```

To handle files and fields together, use the parts accessor and branch on the part type:

```js
fastify.post('/upload/raw/any', async function (req, reply) {
  const parts = req.parts()
  for await (const part of parts) {
    if (part.type === 'file') {
      await pipeline(part.file, fs.createWriteStream(part.filename))
    } else {
      // part.type === 'field
      console.log(part)
    }
  }
  reply.send()
})
```

And to accumulate a file in memory rather than streaming it, await toBuffer:

```js
fastify.post('/upload/raw/any', async function (req, reply) {
  const data = await req.file()
  const buffer = await data.toBuffer()
  // upload to S3
  reply.send()
})
```

There is also a saveRequestFiles helper, which stores all files in the operating system's default temporary directory and removes them as soon as the response ends, returning the files and the parsed values.

The testing setup tells you how seriously the project takes its own edge cases. The unit test script runs with coverage enforced at 100 percent, using the Node built-in test runner, and there is a separate TypeScript test run with tstyche for the type definitions. The development dependencies include a benchmark directory, a form-data client for constructing multipart requests in tests, a pump and readable-stream and concat-stream for stream testing, h2url for HTTP/2, and climem to serve a sample app on a fixed port for the examples.

The examples directory shows the plugin used in anger, with a plain example, one with Swagger integration, one with Zod validation, and one doing validation, which are the combinations you are most likely to need in a real service. The repository also has a benchmark directory and a types directory with the TypeScript definitions, which matters because the plugin ships types as part of the package rather than leaving them to a separate DefinitelyTyped package.

## Conclusion

Adopt @fastify/multipart if you are accepting file uploads in a Fastify service and want a stream you can pipe to disk rather than a buffer you have to hold in memory, and if you set explicit limits. Do not rely on the route level bodyLimit to bound an upload, because the multipart parser does not go through that enforcement, so set fieldSize, fields, files, parts and fileSize yourself or enforce a request size limit upstream. Before you deploy an upload route, read the note about consuming every file, because an unconsumed stream means the promise never fulfils and the request hangs.

## FAQ

### Does Fastify's bodyLimit apply to multipart uploads?

No. Multipart requests are parsed by a dedicated streaming parser and do not go through bodyLimit enforcement. bodyLimit is only used to compute the per-file fileSize default, so a single request can exceed it across many fields and files, and the documentation says to set explicit fieldSize, fields, files, parts and fileSize limits or enforce a request size limit upstream.

### Why does my Fastify upload request hang?

Usually because the file stream was not consumed. The README states that if you allow multiple files you must consume all of them, otherwise the promise will never fulfil, and that this behaviour is inherited from the underlying busboy parser.

### How do I limit upload size in @fastify/multipart?

Pass limits to the plugin at registration, with fieldNameSize, fieldSize, fields, fileSize, files, headerPairs and parts. The defaults are parts at 1000 and fileSize at the Fastify bodyLimit of 1 MiB by default, and per-request or per-route limits can also be given.

### How do I know a file exceeded the size limit?

Three ways are documented: listen for the limit event on the file stream, check the truncated property at the end of the stream, or call toBuffer and wait for the error to be thrown. You may also need to delete the partially written file from disk yourself.

### How do I read form fields alongside a file upload in @fastify/multipart?

The parser consumes the multipart in serial order, so value fields should be placed before any file fields, and otherwise you need to read data.fields after consuming the stream. You can also use attachFieldsToBody with a multipartOptions key in the route config.

## Sources

- [fastify/fastify-multipart on GitHub](https://github.com/fastify/fastify-multipart)
- [License: MIT](https://github.com/fastify/fastify-multipart/blob/main/LICENSE)
- [Project website](https://npmjs.com/package/@fastify/multipart)
- [README](https://github.com/fastify/fastify-multipart/blob/main/README.md)
- [Releases](https://github.com/fastify/fastify-multipart/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/fastify-fastify-multipart
