The server limit was increased from 10 MB to 100 MB, but the upload still failed with a 413 error. The problem was nginx, which was still limiting request bodies to 1 MB before they reached the app.
This is one of the first surprises you run into when you put file upload APIs into production: several layers can limit an upload, and changing the limit in one place doesn’t automatically change the others.
To upload a file to an API, you choose a transport (multipart/form-data for forms, raw binary PUT for single files, or chunked multipart for large payloads), attach auth (bearer tokens or signed URLs), set the correct Content-Type, and respect size limits at every hop: client, proxy, gateway, and server. If you use Filestack, its upload API supports files up to 5 GB through an authenticated request.
This article walks through each part of that negotiation: which transport fits which shape of upload, how auth actually gets checked, and where a request can die before it reaches your code.
Key Takeaways
multipart/form-dataneeds a boundary parameter. SettingContent-Typeby hand without one is the most common self-inflicted 400.- Raw binary PUT, using the file’s own
Content-Type, is simpler and faster for a single file with no other form fields. - Size limits stack across the request path. Client timeout, proxy body cap, gateway cap, and server parser limit all apply, and the smallest one decides the outcome.
- A 413 means some hop rejected the size. A 415 means it rejected the type. They get diagnosed at different layers.
- Signed URLs move auth into the URL itself, with an expiry, which lets a client upload straight to storage without ever holding an API key.
The Three Transports
Most file upload API questions start with the same decision: how should the file be sent? In most cases, you have three options.
multipart/form-data works well for forms that include a file and other fields, such as a title or description. The request is split into separate parts, and each part has its own headers.
Raw binary PUT is simpler. The file is sent directly as the request body, with Content-Type set to the file’s actual type. It’s a good choice when you’re sending one file without any extra form fields.
Chunked uploads are better for large files. The file is split into smaller parts, and each part is uploaded separately. The server then puts the parts back together after all of them arrive. This is more reliable for large uploads because a dropped connection doesn’t have to restart the entire file.
import requests
FILE_PATH = "photo.jpg"
API_URL = "<https://api.example.com/upload>"
HEADERS = {"Authorization": "Bearer YOUR_TOKEN"}
# 1. multipart/form-data, for a file plus other fields
with open(FILE_PATH, "rb") as f:
requests.post(API_URL, headers=HEADERS,
files={"file": f}, data={"title": "Vacation photo"})
# 2. raw binary PUT, for a single file with no other fields
with open(FILE_PATH, "rb") as f:
requests.put(f"{API_URL}/photo.jpg", headers={**HEADERS, "Content-Type": "image/jpeg"},
data=f)
# 3. chunked multipart, for large files sent in parts
CHUNK_SIZE = 5 * 1024 * 1024
upload_id = requests.post(f"{API_URL}/multipart/start", headers=HEADERS).json()["id"]
with open(FILE_PATH, "rb") as f:
part_number = 1
while chunk := f.read(CHUNK_SIZE):
requests.put(f"{API_URL}/multipart/{upload_id}/{part_number}",
headers=HEADERS, data=chunk)
part_number += 1
requests.post(f"{API_URL}/multipart/{upload_id}/complete", headers=HEADERS)
Choosing the right upload method gets the file moving. But before the upload can start, the API needs to check whether the request is allowed. That’s where authentication comes in.
Auth Patterns
Three common authentication methods cover most file upload APIs. Each one offers a different balance between simplicity and security.
Bearer tokens are the simplest option. The client sends a token in the Authorization header, and the server checks it before accepting the upload. This works well when the user is already signed in and has a valid token.
HMAC signatures add another layer of protection. The client signs the request with a shared secret, and the server checks the signature before accepting the file. This can help detect changes to the request while it’s being sent.
Expiring signed URLs let the client upload directly to storage. The backend creates a URL with a signature and expiration time, then gives it to the client. The client can use that URL to upload without receiving a long-term API key. Once the URL expires, it can no longer be used.
If you’re comparing file upload providers, it’s also worth checking their security and compliance information, such as SOC 2 and GDPR support. These details can change, so check each provider’s current documentation instead of assuming they all offer the same protections.
Authentication controls who can upload. Next, Content-Type helps determine what they’re allowed to upload.
Content Types and the Boundary
One of the most common 400 errors with multipart uploads happens when you set the Content-Type header yourself. multipart/form-data needs a special boundary that tells the server where each part begins and ends. The HTTP client creates this boundary automatically.
If you set the header manually without the correct boundary, the server may not be able to read the request. It’s better to let your HTTP client set the Content-Type header for you.
On the server, don’t treat the Content-Type or file extension as proof of what the file actually is. They can be wrong or changed. A safer approach is to check the file’s actual bytes and detect its real format. If it doesn’t match what was declared, you can reject or convert the file.
This is also a key part of building a reliable REST API file upload flow: treat the declared file type as a hint, check the actual content, and handle mismatches safely.
Once transport, authentication, and file type are handled, the next concern is what happens between the client and your server. That’s where size limits come in.
Size Limits at Every Hop
A file upload doesn’t go straight from the client to your application. It can pass through several hops, and each hop may have its own size limit.
The client or browser can have its own timeout or upload limit. A reverse proxy like nginx can also limit request size. Its default client_max_body_size is often 1 MB, which can be much smaller than what your application expects.
An API gateway may add another limit, and your framework or request parser can have its own rules too.
That’s why increasing the limit in your application isn’t always enough. Every hop between the client and your server needs to allow the file size you want to support.
| Hop | Typical default | Error if exceeded |
| Client/browser | Varies by client, often no hard cap | Request never sends, or times out |
| Reverse proxy (nginx) | 1MB body cap by default | 413 Payload Too Large |
| API gateway | Provider-specific, commonly 10MB or less | 413 Payload Too Large |
| Server/framework parser | Framework-specific, often configurable | 413 or 400 depending on framework |
This is still part of the bigger file upload API problem, but now we’re looking at the infrastructure instead of the code.
Increasing the limit in your app won’t help if the proxy in front of it rejects the request first. Check every hop, not just the one you control in your code.
Keeping track of all four hops takes time, and the work doesn’t stop after the first setup. Any of those limits can change later.
There is another option that avoids having to manage all of these limits yourself.
The Managed Route, One Hop That Says Yes
If you don’t want to manage and check every hop yourself, a managed file upload API can simplify things. You get one documented upload limit, one authentication setup, and support for files up to 5 GB.
Instead of dealing with several different limits that may not match, you have one clear limit to work with. This makes the upload setup easier to manage and maintain.
When choosing a reliable file upload service, look for a few important things: a clear file size limit, a simple authentication setup, and built-in chunked uploads. You shouldn’t have to build and maintain all of that yourself.
For a startup team, the easiest approach is often to choose a service that already supports the file sizes and upload methods you need. That way, you spend less time managing multiple infrastructure limits.
For a framework-specific example, the FastAPI companion post covers how to handle file uploads with FastAPI. You can also check the Filestack API auth docs for more details on authentication and signed URLs.
That’s the managed approach. Here’s a quick summary of the key points either way.
Conclusion: Know Your Hops
Choosing a transport depends on what you’re uploading: a file with other form fields, a single file, or a large file that needs to be split into chunks.
Authentication is about deciding where trust should live. You might use a server-side token, a signed request, or a URL that lets the client upload directly.
But neither matters if one of the hops in between rejects the request before it reaches your app.
Try sending a 100 MB test file through the full upload path today, from the client to the server. If it fails, find the first hop that rejects it. That’s the limit that actually controls your uploads, no matter what your app’s configuration says.
Frequently Asked Questions
Why does my upload return 413?
Some hops’ body-size cap is smaller than the file. Check the proxy and gateway limits before assuming the problem is in your application code.
multipart/form-data or raw binary?
Multipart for forms with mixed fields. Raw binary PUT for a single file with no other data attached.
Should the client set the multipart Content-Type?
No. Let the HTTP client generate it, since it needs to include the boundary parameter, and a hand-set header usually leaves that out.
Shefali Jangid is a web developer, technical writer, and content creator with a love for building intuitive tools and resources for developers.
She writes about web development, shares practical coding tips on her blog shefali.dev, and creates projects that make developers’ lives easier.
Read More →
