---
title: "File uploads"
description: "Sending files through a server action without encoding them."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.rsc-kit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# File uploads

A `File` is a value a server action can take. React's Flight format carries it
as binary, so there is nothing to encode on the way out and nothing to decode
on the way in.

## One file

```tsx title="src/components/AvatarUpload.tsx"
"use client";

import { useState } from 'react';
import { uploadAvatar } from '../actions';

export function AvatarUpload() {
  const [url, setUrl] = useState<string | null>(null);

  async function submit(formData: FormData) {
const file = formData.get('avatar') as File;

if (!file || file.size === 0) return;

setUrl(await uploadAvatar(file));
  }

  return (
<form action={submit}>
  <input type="file" name="avatar" accept="image/*" />
  <button type="submit">Upload</button>
  {url && <img src={url} alt="" />}
</form>
  );
}
```

The action receives the `File` itself — name, type, size and all:

```ts title="src/actions.ts"
'use server'

import { writeFile } from 'node:fs/promises';

export async function uploadAvatar(file: File): Promise<string> {
  const bytes = new Uint8Array(await file.arrayBuffer());

  await writeFile(`./public/avatars/${file.name}`, bytes);

  return `/avatars/${file.name}`;
}
```

Nothing about that is upload-specific: it is the same call, the same
serialisation and the same round trip as [any other server
action](/guides/server-actions).

## The whole form at once

An action can take the `FormData` instead of individual arguments, which is
usually simpler when the form mixes files and fields:

```tsx
"use client";

import { createPost } from '../actions';

export function NewPost() {
  return (
<form action={createPost}>
  <input name="title" />
  <textarea name="body" />
  <input type="file" name="cover" accept="image/*" />
  <button type="submit">Create</button>
</form>
  );
}
```

```ts title="src/actions.ts"
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const cover = formData.get('cover') as File;

  // …
}
```

## Several files

`formData.getAll` gives every file from a `multiple` input, and an array of
`File` serialises as readily as one:

```tsx
const files = formData.getAll('photos') as File[];

await uploadPhotos(files);
```

```ts title="src/actions.ts"
'use server'

export async function uploadPhotos(files: File[]) {
  for (const file of files) {
// …
  }
}
```

## Size

The whole body is buffered before the action runs — it is one `POST`, not a
stream — so an upload occupies memory for as long as it takes. The server
refuses a body over **8 MB** with a 413 before it holds a byte of it, and
the form reports that as a failed submission. Raise the ceiling when the app
genuinely uploads more:

```ts title="vite.config.ts"
rscKit({ maxActionBody: 32 * 1024 * 1024 })
```

Two consequences worth planning for:

- Keep the ceiling — and any limit a proxy in front imposes — as low as the app actually needs.
- For genuinely large files, do not send them through an action at all. Have the action mint a pre-signed URL and let the browser upload straight to storage; the bytes never touch your server.

Source: https://docs.rsc-kit.dev/guides/file-uploads/index.mdx
