The early web ran on plain text. You typed a username or a comment into a form, hit submit, and a short string traveled to the server over HTTP. That simple request-response cycle defined the infrastructure. When people started wanting to share photos, documents, and videos, engineers had to figure out how to move raw binary data through a system built entirely for readable text.
If you try to drop a JPEG into a JSON object, you hit a fundamental wall. JSON is a text protocol. It expects valid Unicode characters, quotes, braces, and properly escaped strings. A binary file is just a long sequence of bytes, many of which have no printable representation. Jam those bytes into a JSON string and the parser breaks, escape sequences corrupt the payload, and the whole message becomes unreadable on the other side.
Base64 encoding emerged as the obvious workaround. It re-maps binary data into a limited set of sixty-four printable ASCII characters. Every three bytes of binary become four characters of text. The payload is now legal JSON, which means it will survive a trip through any standard API. But the cost is immediate. That re-encoding inflates the file size by roughly thirty-three percent. A three megabyte image becomes four megabytes on the wire. Both the client and the server burn extra CPU cycles translating the data back and forth. More importantly, many JSON server frameworks read the entire body into memory before parsing it. A handful of concurrent large uploads can swamp a modest server because each one is being held in RAM as an overweight text string before it even gets saved to disk. Base64 works in a pinch, but it was never designed to carry heavy files at production scale.
The better answer is multipart/form-data. This format treats a single HTTP request as a collection of separate parts, each divided by a unique boundary string. One part might hold a plain text field. The next part might hold a raw binary image, marked with its own Content-Type and Content-Disposition headers. The server reads the incoming stream sequentially, watching for the boundary markers, and hands each section off to the appropriate handler without ever needing to treat the entire payload as a single block of text.
In Node.js, this distinction is especially important. The express.json() middleware knows how to parse JSON bodies, but it does not handle file streams. To process multipart uploads, you need a streaming parser like Multer or Busboy. These tools attach to the raw request stream and read it chunk by chunk. Multer, for instance, lets you choose whether to write incoming files to a temporary folder on disk or hold smaller ones in memory. That configuration decision matters. If you keep everything in memory and your app suddenly receives several large files at once, your process can run out of heap space and crash. Writing to disk trades I/O for stability, but it introduces its own questions about cleanup and path security.
For a small application, saving files to a local folder like ./uploads feels natural and fast. The file lands on the same machine running your code, and serving it back is just a matter of pointing at the right path. This works right up until it does not.
The moment you place a load balancer in front of a second application server, local storage becomes a bug. A user uploads a profile picture. The load balancer routes the request to Server A, and the file is written to Server A's disk. Later, that user asks to view the image, but the load balancer sends the request to Server B. Server B checks its own filesystem and finds nothing. The file is effectively missing. You can implement sticky sessions to pin a user to the same machine, but that is a fragile fix. If Server A restarts, gets redeployed, or is replaced by an autoscaling instance, the data vanishes. In containerized environments, local disks are even more ephemeral. A Docker container’s filesystem is meant to be disposable. Treating it as permanent storage is a reliable way to lose user data.
The standard solution is to separate compute from storage. You keep your application servers stateless and send uploaded files to dedicated object storage like AWS S3 or Google Cloud Storage. These services are built for durability, geographic distribution, and massive concurrency. The application server handles the request, validates the metadata, and then hands the bytes off to infrastructure designed specifically to hold them.
Doch selbst dieses Muster erzeugt einen Flaschenhals, wenn man es unvorsichtig implementiert. Viele Teams beginnen damit, dass der Browser die Datei an das Backend hochlädt und das Backend dann jedes einzelne Byte an den Object Storage weiterleitet. Wenn ein Benutzer ein fünfhundert Megabyte großes Video hochlädt, wird Ihr Server zum Vermittler. Er verbraucht Bandbreite beim Herunterladen der Datei und verbraucht dann noch mehr Bandbreite beim Hochladen zu S3. Die Verbindung bleibt während der gesamten Dauer der Übertragung offen. Langsame Uploads von Benutzern mit schlechten Netzwerkbedingungen können Serververbindungen minutenlang blockieren. Die Speicherauslastung bleibt hoch, wenn der Server den Stream puffert, und wenn Sie Metered Hosting nutzen, zahlen Sie doppelt für denselben Datentransfer. Horizontale Skalierung löst dieses Problem nicht, da jeder zusätzliche Server, den Sie hinzufügen, immer noch damit beschäftigt ist, Bytes zu transportieren, die er gar nicht sehen muss.
Moderne Systeme lösen das Problem, indem sie das Backend vollständig aus dem Datenpfad entfernen. Anstatt die Datei entgegenzunehmen, akzeptiert das Backend lediglich eine Anfrage um Erlaubnis für den Upload. Der Ablauf sieht wie folgt aus:
- Der Browser bittet das Backend, einen Upload zu initiieren, wobei in der Regel nur der Dateiname, der Dateityp und der beabsichtigte Zweck gesendet werden.
- Das Backend authentifiziert den Benutzer, validiert die Anfrage anhand von Geschäftsregeln und verwendet ein SDK, um eine temporäre, vorsignierte URL vom Object-Storage-Anbieter zu generieren.
- Das Backend gibt diese URL an den Browser zurück. Die URL ist auf einen bestimmten Bucket und Key beschränkt, für ein kurzes Zeitfenster wie fünf oder fünfzehn Minuten gültig und mit einem Token signiert, das nur die exakt benötigten Berechtigungen gewährt.
- Der Browser lädt die Datei direkt per Standard-PUT oder -POST zu S3 oder GCS hoch. Die Bytes gelangen direkt vom Gerät des Benutzers zu
