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.

Tuy nhiên, ngay cả mô hình này cũng tạo ra nút thắt cổ chai nếu bạn triển khai một cách thiếu cẩn trọng. Nhiều đội ngũ bắt đầu bằng việc để trình duyệt tải tệp lên backend, sau đó backend sẽ chuyển tiếp từng byte một đến kho lưu trữ đối tượng. Nếu một người dùng tải lên một video nặng 500 megabyte, máy chủ của bạn sẽ trở thành một bên trung gian. Nó tiêu tốn băng thông để kéo tệp vào, sau đó lại tiêu tốn thêm băng thông để đẩy tệp ra S3. Kết nối sẽ duy trì mở trong suốt toàn bộ quá trình truyền tải. Việc tải lên chậm từ những người dùng có điều kiện mạng kém có thể chiếm dụng các kết nối máy chủ trong nhiều phút. Mức sử dụng bộ nhớ sẽ duy trì ở mức cao nếu máy chủ đệm (buffer) luồng dữ liệu, và nếu bạn đang chạy trên dịch vụ lưu trữ tính phí theo lưu lượng (metered hosting), bạn đang phải trả tiền gấp đôi cho cùng một lần truyền dữ liệu. Mở rộng quy mô theo chiều ngang (horizontal scaling) không giải quyết được vấn đề này, bởi vì mỗi máy chủ bổ sung mà bạn thêm vào vẫn sẽ bị kẹt trong việc vận chuyển những byte dữ liệu mà nó không cần phải xem.

Các hệ thống hiện đại giải quyết vấn đề này bằng cách loại bỏ hoàn toàn backend khỏi đường truyền dữ liệu (data path). Thay vì tiếp nhận tệp, backend chỉ tiếp nhận một yêu cầu xin quyền tải lên. Quy trình diễn ra như sau:

  • Trình duyệt yêu cầu backend khởi tạo một lượt tải lên, thường chỉ gửi tên tệp, loại tệp và mục đích dự kiến.
  • Backend xác thực người dùng, kiểm tra yêu cầu dựa trên các quy tắc nghiệp vụ, và sử dụng một SDK để tạo một URL được ký trước (presigned URL) tạm thời từ nhà cung cấp kho lưu trữ đối tượng.
  • Backend trả URL đó về cho trình duyệt. URL này được giới hạn trong một bucket và key cụ thể, có hiệu lực trong một khoảng thời gian ngắn như 5 hoặc 15 phút, và được ký bằng một token chỉ cấp các quyền chính xác cần thiết.
  • Trình duyệt tải tệp trực tiếp lên S3 hoặc GCS bằng phương thức PUT hoặc POST tiêu chuẩn. Các byte dữ liệu sẽ đi thẳng từ thiết bị của người dùng đến