Back to Blog

NestJS File Upload Failing on Vercel — The 4.5MB Body Limit Nobody Documents

Published: July 28, 2026
NestJS File Upload Failing on Vercel — The 4.5MB Body Limit Nobody Documents

NestJS file upload failing on Vercel past a certain size is almost always the same story: it works in local testing with small test files, someone uploads a real one in production, and the request just fails — no stack trace pointing at your code, no Multer error you can act on, nothing. Nine times out of ten it's not a bug in your upload handler. It's Vercel's serverless functions capping request bodies at 4.5MB, silently, with no config flag to raise it.

Short answer: Vercel serverless functions reject request bodies over 4.5MB before your code ever runs, and no setting changes that number. The fix isn't a bigger Multer limit or a longer timeout — it's not sending the file through your function at all. Generate a presigned upload URL from NestJS, and have the browser upload the file directly to S3 (or equivalent object storage). The file bytes never touch your Vercel function, so the limit stops being relevant.

Colorful measuring tape coiled up, representing the hard size limit Vercel silently applies to your upload}

Why NestJS File Upload Failing on Vercel Isn't a Multer Problem

The instinct when an upload fails is to start tuning Multer — raise limits.fileSize, double-check the FileInterceptor config, add better error handling around the parsing step. None of that helps here, because Vercel's documented function limitations cap the request body at 4.5MB at the platform level, upstream of anything your NestJS app controls. If the incoming request is bigger than that, Vercel can reject it before your handler — and therefore Multer — ever sees a single byte.

TypeScript
1// uploads.controller.ts — works under ~4MB, fails above it with no useful error
2import { Controller, Post, UseInterceptors, UploadedFile } from '@nestjs/common';
3import { FileInterceptor } from '@nestjs/platform-express';
4
5@Controller('uploads')
6export class UploadsController {
7  @Post()
8  @UseInterceptors(FileInterceptor('file'))
9  async uploadFile(@UploadedFile() file: Express.Multer.File) {
10    // Proxying the file through this function is the actual problem —
11    // not anything about how this handler processes it.
12    return { filename: file.originalname, size: file.size };
13  }
14}

This code is fine. It's also structurally the wrong shape for any file upload of meaningful size on Vercel, because it asks a single serverless invocation to receive the entire file as its request body.

(If you've been adding try/catch blocks around this handler hoping to at least surface a cleaner error message — you won't get one, because the failure happens before your try block is reachable. This one doesn't have a "handle it more gracefully" version. It has an "architecture" version.)

The Real Fix: Direct-to-S3 Upload With a Presigned URL

The fix is to stop proxying the file through your function entirely. Your NestJS API's only job becomes generating a short-lived, signed URL that lets the browser upload straight to object storage:

TypeScript
1// uploads.service.ts
2import { Injectable } from '@nestjs/common';
3import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
4import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
5
6@Injectable()
7export class UploadsService {
8  private readonly s3 = new S3Client({ region: process.env.AWS_REGION });
9
10  async getPresignedUploadUrl(key: string, contentType: string): Promise<string> {
11    const command = new PutObjectCommand({
12      Bucket: process.env.S3_BUCKET_NAME,
13      Key: key,
14      ContentType: contentType,
15    });
16
17    return getSignedUrl(this.s3, command, { expiresIn: 300 });
18  }
19}
TypeScript
1// uploads.controller.ts
2import { Controller, Post, Body } from '@nestjs/common';
3import { UploadsService } from './uploads.service';
4
5@Controller('uploads')
6export class UploadsController {
7  constructor(private readonly uploadsService: UploadsService) {}
8
9  @Post('presigned-url')
10  async getPresignedUrl(@Body() body: { filename: string; contentType: string }) {
11    const key = `uploads/${Date.now()}-${body.filename}`;
12    const url = await this.uploadsService.getPresignedUploadUrl(key, body.contentType);
13    return { url, key };
14  }
15}

A classic mechanical balance scale, representing the file size limit your architecture needs to route around instead of fight

The request this endpoint handles is tiny — a filename and a content type — comfortably under any body size limit no matter how large the actual file is. AWS's own presigned URL documentation covers the mechanism in full if you want the details beyond what's needed here.

Uploading Directly From the Browser

With the presigned URL in hand, the client uploads straight to S3 — the file bytes never route through Vercel at all:

TypeScript
1// Direct upload from the browser straight to S3 — never touches your Vercel function
2async function uploadFileDirectly(file: File) {
3  const { url, key } = await fetch('/api/uploads/presigned-url', {
4    method: 'POST',
5    headers: { 'Content-Type': 'application/json' },
6    body: JSON.stringify({ filename: file.name, contentType: file.type }),
7  }).then((res) => res.json());
8
9  await fetch(url, {
10    method: 'PUT',
11    headers: { 'Content-Type': file.type },
12    body: file,
13  });
14
15  return key;
16}

Data transfer complete message on a computer monitor, representing an upload that finished without ever hitting the body size limit

Two fetch calls, both small from your Vercel function's point of view: one to request the signed URL, one that goes directly to S3 and never touches your serverless function at all. NestJS's own file upload docs are worth reading if you still need Multer for smaller, genuinely function-sized uploads elsewhere in the app — the two patterns coexist fine.

The Opinion Part

Here's the position worth stating plainly, and it would have been true even without Vercel's 4.5MB limit forcing the issue: proxying file uploads through your own application server was already the wrong default, not just a Vercel-specific workaround. Every byte of that file gets received by your function, held in memory, and re-sent to storage — bandwidth and compute you're paying for twice, for work your API server was never the right tool to do. Direct-to-storage upload with presigned URLs is the correct architecture on any platform; Vercel just makes the cost of skipping it show up immediately instead of quietly on your infrastructure bill. Given that roughly 29% of cloud spend already goes to waste industry-wide (Flexera, 2024), a proxy-through-the-server upload pattern is exactly the kind of quietly expensive default worth killing early, not just when a platform limit forces your hand.

Conclusion

If NestJS file uploads keep failing on Vercel above a few megabytes, stop adjusting Multer's limits and stop reaching for a bigger maxDuration — neither touches the actual constraint. Generate a presigned URL, let the browser talk to S3 directly, and the 4.5MB body limit stops being something you're fighting, because your function was never carrying the file in the first place.

If you're building this upload flow for the first time rather than migrating an existing one, our full presigned URL upload architecture guide covers validation, virus scanning, and image processing on top of this same pattern. And if a background worker or a WebSocket gateway on the same Vercel deployment has also been behaving strangely, that's very likely the same root cause wearing a different face — or, if it's specifically a socket that won't stay open, this one covers that variant directly.

Ship the presigned URL, watch the large uploads go through clean, and cross one more "why does this only break in production" ticket off the list.

Frequently Asked Questions

4.5MB, and it's a hard platform limit — not a default you can raise with a config flag or a larger plan. It applies to the total request body size for a serverless function invocation, which includes the entire file when you're proxying an upload through your API route rather than sending it directly to storage.

It applies to the request body coming into the function — uploads, in other words. Response bodies (what your function sends back) have a separate, much larger limit. This is specifically about how much data a client can send to a single serverless function invocation in one request.

No. Unlike function timeout (maxDuration), the request body size limit isn't a configurable value in vercel.json or anywhere else — it's a fixed platform constraint on all serverless functions. The only way around it is to not send the file through the function at all.

No — Multer's limits.fileSize option controls what your own application chooses to accept; it does nothing about Vercel's platform-level cap upstream of your code. The request can fail before it ever reaches your NestJS handler or Multer's parsing logic, which is exactly why the error is so unhelpful: your app's error handling never gets a chance to run.

Upload directly from the browser to object storage (S3, Cloudflare R2, or similar) using a presigned URL your NestJS API generates. The file bytes never pass through your Vercel function at all — the function only handles the small request to generate the presigned URL, which comfortably fits under any body size limit regardless of how large the actual file is.

Portrait of Umar Farooq

About Umar Farooq

Umar Farooq is the founder and lead engineer of Codify SaaS. He builds B2B SaaS products and web applications on modern TypeScript stacks and enterprise Java, and writes code-first guides drawn from real production work — the schema decisions, the migrations that almost went wrong, and the performance fixes that actually moved the numbers. When he recommends an approach, he shows the code and explains the trade-offs.

Read full bio