Skip to content

How to Upload Files with Netlify Functions | Limits, Large Media, and Workarounds

Category: Netlify / Deployment
This article is currently available in Japanese only. We are working on translations.

When implementing file uploads with Netlify Functions (AWS Lambda-based serverless functions), request bodies have a size limit. Many developers encounter issues without knowing about this 6MB default limit. This article explains Netlify Functions' body size limit, configuration in <code>netlify.toml</code>, parsing methods for <code>multipart/form-data</code>, and alternative approaches for handling files beyond the limit.

Functions 6 MB request body 10s timeout Background Fn 6 MB request body 15min timeout Edge Functions 20 MB request body streaming OK
Diagram: Netlify body size limits per function type

Netlify Functions body size limit

Netlify Functions is based on AWS Lambda and has a fixed upper limit on request body size.

Types of Functions Body size limit Changeable or not Remarks
Netlify Functions (synchronous) 6 MB Not possible AWS Lambda Limitations
Netlify Functions(Background) 6 MB Not possible Processing time is maximum 15 minutes
Netlify Edge Functions With Restrictions Not possible Deno-based Runtime

Sending requests exceeding 6MB to Netlify Functions results in HTTP 413 or function timeout. While slightly larger than Vercel (4.5MB), large files like images or videos still cannot be received directly.

Configuration in netlify.toml

In <code>netlify.toml</code>, you can configure Functions runtime, timeout, memory, redirects, headers, and more. Body size itself cannot be changed, but understanding related settings is important.

# netlify.toml

[build]
  command = "npm run build"
  publish = ".next"
  functions = "netlify/functions"  # Functions のディレクトリ

# Node.js バンドラーの設定
[functions]
  node_bundler = "esbuild"

# 特定の Function の設定
[functions."upload"]
  included_files = ["uploads/**"]

# リダイレクト設定(Next.js など SPA との組み合わせ)
[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/:splat"
  status = 200

# ヘッダーの設定(CORS など)
[[headers]]
  for = "/.netlify/functions/*"
  [headers.values]
    Access-Control-Allow-Origin = "*"
    Access-Control-Allow-Methods = "GET, POST, PUT, DELETE, OPTIONS"
    Access-Control-Allow-Headers = "Content-Type, Authorization"

# 環境変数(本番用。機密情報は Netlify UI で設定すること)
[context.production.environment]
  NODE_ENV = "production"

[context.deploy-preview.environment]
  NODE_ENV = "development"

Parsing implementation of multipart/form-data

In Netlify Functions, request bodies may be Base64-encoded by default. We explain how to parse multipart data using <code>busboy</code> or <code>formidable</code>.

// netlify/functions/upload.js(CommonJS)
const busboy = require('busboy');

exports.handler = async (event) => {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }

  return new Promise((resolve, reject) => {
    const contentType = event.headers['content-type'] || event.headers['Content-Type'];

    const bb = busboy({ headers: { 'content-type': contentType } });
    const files = [];
    const fields = {};

    bb.on('file', (name, file, info) => {
      const { filename, encoding, mimeType } = info;
      const chunks = [];

      file.on('data', (data) => chunks.push(data));
      file.on('end', () => {
        const buffer = Buffer.concat(chunks);

        // ファイルサイズの検証(6MB 上限の手前で確認)
        const MAX_SIZE = 5 * 1024 * 1024; // 5MB(余裕を持たせる)
        if (buffer.length > MAX_SIZE) {
          resolve({
            statusCode: 413,
            body: JSON.stringify({ error: 'ファイルが大きすぎます(上限5MB)。' }),
          });
          return;
        }

        files.push({ name, filename, mimeType, buffer, size: buffer.length });
      });
    });

    bb.on('field', (name, value) => {
      fields[name] = value;
    });

    bb.on('finish', () => {
      if (files.length === 0) {
        resolve({
          statusCode: 400,
          body: JSON.stringify({ error: 'ファイルが見つかりません。' }),
        });
        return;
      }

      const file = files[0];
      // ここでファイルを S3 などに保存する処理を行う

      resolve({
        statusCode: 200,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          success: true,
          filename: file.filename,
          size: file.size,
          mimeType: file.mimeType,
          fields,
        }),
      });
    });

    bb.on('error', (err) => {
      resolve({
        statusCode: 500,
        body: JSON.stringify({ error: err.message }),
      });
    });

    // Netlify Functions ではボディが Base64 エンコードされる場合がある
    const body = event.isBase64Encoded
      ? Buffer.from(event.body, 'base64')
      : event.body;

    bb.end(body);
  });
};
// netlify/functions/upload-s3.js(S3 に転送する例)
const busboy = require('busboy');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');

const s3 = new S3Client({ region: process.env.AWS_REGION });

exports.handler = async (event) => {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }

  return new Promise((resolve) => {
    const bb = busboy({
      headers: { 'content-type': event.headers['content-type'] },
      limits: { fileSize: 5 * 1024 * 1024 }, // 5MB で切り捨て
    });

    bb.on('file', async (fieldName, stream, { filename, mimeType }) => {
      const chunks = [];
      let truncated = false;

      stream.on('data', (chunk) => chunks.push(chunk));
      stream.on('limit', () => { truncated = true; });

      stream.on('end', async () => {
        if (truncated) {
          return resolve({
            statusCode: 413,
            body: JSON.stringify({ error: 'ファイルが5MBを超えています。' }),
          });
        }

        const buffer = Buffer.concat(chunks);
        const key = `uploads/${Date.now()}-${filename}`;

        try {
          await s3.send(new PutObjectCommand({
            Bucket: process.env.AWS_S3_BUCKET,
            Key: key,
            Body: buffer,
            ContentType: mimeType,
          }));

          resolve({
            statusCode: 200,
            body: JSON.stringify({
              success: true,
              key,
              url: `https://${process.env.AWS_S3_BUCKET}.s3.amazonaws.com/${key}`,
            }),
          });
        } catch (err) {
          resolve({ statusCode: 500, body: JSON.stringify({ error: err.message }) });
        }
      });
    });

    const body = event.isBase64Encoded
      ? Buffer.from(event.body, 'base64')
      : event.body;

    bb.end(body);
  });
};

Netlify Large Media (deprecated) and alternatives

Netlify Large Media was a service for managing large files based on Git LFS, but new signups are now restricted and migration to alternative solutions is recommended.

# Netlify Large Media(非推奨・新規利用不可)
# .lfsconfig
[lfs]
    url = https://large-media.netlify.com/<repo-id>

The recommended alternative configuration is as follows.

// Presigned URL 経由で S3 に直接アップロードする Netlify Function
// netlify/functions/get-upload-url.js
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

const s3 = new S3Client({ region: process.env.AWS_REGION });

exports.handler = async (event) => {
  if (event.httpMethod !== 'POST') {
    return { statusCode: 405, body: 'Method Not Allowed' };
  }

  const { filename, contentType, size } = JSON.parse(event.body);

  // サーバー側でのバリデーション
  const MAX_SIZE = 100 * 1024 * 1024; // 100MB
  if (size > MAX_SIZE) {
    return { statusCode: 413, body: JSON.stringify({ error: 'ファイルが大きすぎます。' }) };
  }

  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
  if (!allowedTypes.includes(contentType)) {
    return { statusCode: 415, body: JSON.stringify({ error: '許可されていない形式です。' }) };
  }

  const key = `uploads/${Date.now()}-${filename}`;

  const command = new PutObjectCommand({
    Bucket: process.env.AWS_S3_BUCKET,
    Key: key,
    ContentType: contentType,
    ContentLength: size,
  });

  // 署名付き URL を生成(このリクエストはファイル本体を含まないため制限外)
  const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 900 }); // 15分

  return {
    statusCode: 200,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ presignedUrl, key }),
  };
};
// クライアント側:Netlify Function から Presigned URL を取得して S3 に直接アップロード
async function uploadFile(file) {
  // 1. Netlify Function から Presigned URL を取得
  const res = await fetch('/.netlify/functions/get-upload-url', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      filename: file.name,
      contentType: file.type,
      size: file.size,
    }),
  });

  const { presignedUrl, key } = await res.json();

  // 2. S3 に直接 PUT(Netlify Function を経由しないため容量制限なし)
  const uploadRes = await fetch(presignedUrl, {
    method: 'PUT',
    headers: { 'Content-Type': file.type },
    body: file,
  });

  if (!uploadRes.ok) throw new Error('アップロードに失敗しました');

  return key;
}

Test files for this article (free)

  • → <a href="/ja/files/images/png/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">PNG Test Image (1MB)</a> — For verifying upload behavior via Netlify Functions
  • → <a href="/ja/files/pdf/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">PDF Test File (1MB)</a> — for testing file uploads within 6MB limit
  • → <a href="/ja/files/zip/1mb/" class="text-primary-600 dark:text-primary-400 hover:underline">ZIP test file (1MB)</a> — For S3 direct upload testing via Presigned URL

Related articles

  • → <a href="/ja/blog/vercel-upload-config/" class="text-primary-600 dark:text-primary-400 hover:underline">Vercel File Upload Configuration and Limits | Vercel Blob, API Restrictions, Workarounds</a>
  • → <a href="/ja/blog/render-upload-config/" class="text-primary-600 dark:text-primary-400 hover:underline">How to Configure File Upload on Render with PHP/Node.js | Disk, Environment Variables, S3 Integration</a>
  • → <a href="/ja/blog/s3-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">Summary of File Upload Limits for AWS S3 and CloudFront</a>