Few errors are as reliably confusing as 413 Request Entity Too Large on a self-hosted Supabase instance. Your upload code works perfectly in development, then fails in production for any file bigger than a few megabytes — and the error might be coming from any one of four different layers between your client and the Storage API. Was it nginx? Cloudflare? The gateway? The FILE_SIZE_LIMIT variable you half-remember seeing in the .env file?
On Supabase Cloud there's a dashboard slider for this. When you deploy Supabase on your own server, you own the whole request path, which means you own every limit along it. This guide maps out exactly where upload limits live in a self-hosted stack, how to configure each one, and how to fix the notorious "can't upload more than 6MB from Studio" problem.
Where Upload Limits Actually Live
A file uploaded to self-hosted Supabase Storage passes through up to four checkpoints, and the request fails at the first one it exceeds:
| Layer | Default limit | Where it's configured |
|---|---|---|
| CDN / tunnel (Cloudflare proxied) | 100 MB on the Free plan | Cloudflare plan level |
| Reverse proxy (nginx, Caddy, Traefik) | nginx: 1 MB; Caddy/Traefik: none | Proxy config |
| API gateway (Kong, or Envoy since Aug 2026) | None in the stock Supabase config | kong.yml / Envoy config |
| Storage API | 50 MiB (FILE_SIZE_LIMIT=52428800) | .env in the Docker stack |
| Individual bucket | None (inherits global) | storage.buckets table |
This ordering matters for debugging. If uploads fail at exactly 1 MB, that's nginx's infamous default. Failures at ~100 MB behind an orange-clouded DNS record point at Cloudflare. Failures at 50 MiB with a JSON error body ("The object exceeded the maximum allowed size") come from the Storage API itself — that one is actually Supabase's limit, and the only one with a first-party knob.
A useful diagnostic: a 413 with an nginx or Cloudflare HTML error page means the request never reached Supabase. A 413 with a JSON body means it did, and you need to raise FILE_SIZE_LIMIT or a bucket limit.
Raising the Storage API Global Limit
In the official Docker Compose stack, the Storage service reads FILE_SIZE_LIMIT from your .env file. The default is 50 MiB, expressed in bytes:
# .env — global upload cap for the Storage API FILE_SIZE_LIMIT=524288000 # 500 MiB
Then recreate the storage container so it picks up the change:
docker compose up -d storage
Two things worth knowing before you set this to something enormous:
- This is a global ceiling, not a per-bucket setting. Every bucket can be further restricted, but none can exceed it.
- Bigger limits mean bigger buffers. Standard (non-resumable) uploads are held in the storage container while they stream through, and if you're running on a small VPS, one 2 GB upload can pressure the same memory your database needs. If your instance is memory-constrained, our guide to running self-hosted Supabase on low-memory servers covers how to budget for this. For genuinely large files, resumable uploads (below) are the right tool, not a giant global limit.
This is one of a few dozen variables in the stack's .env that quietly control production behavior — the complete environment variables guide catalogs the rest.
Per-Bucket Limits and MIME Type Restrictions
The global limit is a ceiling; buckets are where your actual policy should live. An avatars bucket has no business accepting 500 MB videos just because your exports bucket needs to.
Set limits when creating a bucket:
await supabase.storage.createBucket('avatars', {
public: false,
fileSizeLimit: '2MB',
allowedMimeTypes: ['image/png', 'image/jpeg', 'image/webp'],
});
Or update an existing bucket directly in Postgres:
update storage.buckets
set file_size_limit = 2097152, -- 2 MiB, in bytes
allowed_mime_types = array['image/png', 'image/jpeg', 'image/webp']
where id = 'avatars';
allowed_mime_types is doing security work here, not just tidiness: it stops users uploading HTML or SVG files with embedded scripts into a bucket your app later serves. Pair it with proper storage RLS policies — size limits control how much gets uploaded; RLS controls who can upload it. Neither substitutes for the other.
Fixing the 413 at the Proxy Layer
If the error page is HTML rather than JSON, your reverse proxy rejected the request before Supabase ever saw it.
nginx ships with a 1 MB default, which is why so many self-hosted setups break the first time someone uploads a photo:
server {
# ...
client_max_body_size 500M;
proxy_request_buffering off; # stream uploads instead of buffering to disk
}
Caddy imposes no default limit, but if you've added one, it lives in request_body:
supabase.example.com {
request_body {
max_size 500MB
}
reverse_proxy localhost:8000
}
Traefik also has no default body limit — unless you've enabled the buffering middleware, in which case check maxRequestBodyBytes.
Whichever proxy you run, keep its limit at or slightly above your FILE_SIZE_LIMIT so rejections happen at the Supabase layer, where clients get a structured JSON error they can actually handle. Our reverse proxy setup guide for nginx, Traefik, and Caddy covers the full configuration for each.
Two more spots people forget:
- Cloudflare: proxied (orange-cloud) DNS records cap request bodies at 100 MB on the Free and Pro plans. You can't raise it without upgrading — but resumable uploads sidestep it entirely, because each chunk is only 6 MB.
- The gateway: the stock Supabase
kong.ymldoesn't impose a body-size limit, and neither does the new Envoy configuration that replaced Kong as the default gateway in August 2026. If you customized your gateway config, though, audit it for a request-size plugin before blaming the layers above.
The "Can't Upload More Than 6MB in Studio" Problem
This one has bitten enough people to earn its own GitHub issue: everything works through supabase-js, but uploading anything over 6 MB through the self-hosted Studio dashboard fails.
The cause is undocumented behavior: Studio silently switches to resumable (TUS) uploads for files larger than 6 MB. Resumable uploads hit a different route — /storage/v1/upload/resumable — and older self-hosted gateway configs didn't expose it. The request dies at the gateway, and Studio surfaces an unhelpful generic error.
The fix checklist:
- Update your stack. Current versions of the official Docker Compose route
upload/resumablecorrectly — if you're running a config from 2023–2024, this alone is your answer. Follow the version upgrade guide rather than hand-editing, since gateway configs are exactly the kind of thing that drifts. - Verify the route exists in your (possibly customized) gateway config. If you maintain your own
kong.ymlor Envoy config, confirm the storage service definition includes the resumable path. - Check
TUS_URL_PATHif you've overridden storage service variables — it must match what the gateway forwards.
Once the route works, resumable uploads are something you should lean into rather than merely tolerate: they survive connection drops, they chunk files into 6 MB pieces (which is what lets them slip under Cloudflare's cap), and they're the only sane way to accept multi-gigabyte files. The resumable uploads guide walks through TUS end-to-end, including the client-side setup with tus-js-client.
Don't Forget What Happens After the Upload
Raising limits has a downstream cost that's easy to miss: disk. A 500 MB global limit on a public-facing app means users can now fill your VPS at 500 MB per request. Before raising limits, check that:
- Your storage backend has headroom — or better, move file storage off the local disk entirely to an S3-compatible backend like MinIO or Cloudflare R2, where capacity is elastic and your VPS disk stays boring.
- Your backups cover the files, not just the database.
pg_dumpcaptures thestorage.objectsmetadata but not the file contents — a restored database full of pointers to files you no longer have is a special kind of disaster. Storage backup is the forgotten piece of most self-hosted backup strategies. - Monitoring will warn you before the disk fills, because Postgres handles a full disk very badly.
This is the part Supascale was built for: it manages the full stack around your Supabase projects, including automated backups that cover both database and storage files with S3 offload and one-click restore, so raising an upload limit doesn't quietly outgrow your backup strategy. It's a one-time license from $99 with no per-project fees — see pricing for the tiers.
Conclusion
When an upload fails on self-hosted Supabase, work through the layers in order:
- Read the 413 response body. HTML → proxy or CDN; JSON → Supabase itself.
- Set
FILE_SIZE_LIMITin.envas your global ceiling, and restart the storage container. - Enforce real policy per bucket with
file_size_limitandallowed_mime_types. - Align the proxy (
client_max_body_sizeet al.) at or above the Supabase limit. - Use resumable uploads for anything over 6 MB — and make sure the
upload/resumableroute is exposed through your gateway, or Studio uploads will mysteriously fail.
The limits themselves are simple; the difficulty is that they're spread across four config files owned by three different systems. Map them once, keep them intentional, and 413s become a two-minute diagnosis instead of an afternoon of grepping proxy logs.
