শুরুর দিকের ওয়েব চলত সাধারণ টেক্সটের ওপর। আপনি কোনো ফর্মে ইউজারনেম বা কমেন্ট লিখতেন, সাবমিট করতেন এবং একটি ছোট স্ট্রিং HTTP-এর মাধ্যমে সার্ভারে চলে যেত। সেই সাধারণ রিকোয়েস্ট-রেসপন্স সাইকেলটিই ছিল এর অবকাঠামোর ভিত্তি। যখন মানুষ ছবি, ডকুমেন্ট এবং ভিডিও শেয়ার করতে চাইল, তখন ইঞ্জিনিয়ারদের বুঝতে হলো কীভাবে সম্পূর্ণভাবে পাঠযোগ্য টেক্সটের জন্য তৈরি একটি সিস্টেমের মাধ্যমে র-বাইনারি ডেটা (raw binary data) পাঠানো যায়।
আপনি যদি একটি JSON অবজেক্টের মধ্যে একটি JPEG ঢোকানোর চেষ্টা করেন, তবে আপনি একটি মৌলিক বাধার সম্মুখীন হবেন। JSON হলো একটি টেক্সট প্রোটোকল। এটি বৈধ Unicode ক্যারেক্টার, কোটেশন, ব্র্যাকেট এবং সঠিকভাবে এস্কেপ করা স্ট্রিং আশা করে। একটি বাইনারি ফাইল হলো কেবল বাইটের একটি দীর্ঘ সিকোয়েন্স, যার অনেকগুলোরই কোনো প্রিন্টেবল রিপ্রেজেন্টেশন নেই। এই বাইটগুলোকে একটি JSON স্ট্রিং-এর মধ্যে জোর করে ঢুকিয়ে দিলে পার্সার ভেঙে যায়, এস্কেপ সিকোয়েন্সগুলো পেলোড (payload) নষ্ট করে দেয় এবং পুরো মেসেজটি অপর প্রান্তে অপাঠ্য হয়ে পড়ে।
Base64 এনকোডিং একটি স্পষ্ট সমাধান হিসেবে আত্মপ্রকাশ করে। এটি বাইনারি ডেটাকে পঁয়ষট্টিটি প্রিন্টেবল ASCII ক্যারেক্টারের একটি সীমিত সেটে পুনরায় ম্যাপ করে। বাইনারির প্রতি তিনটি বাইট টেক্সটের চারটি ক্যারেক্টারে পরিণত হয়। পেলোডটি এখন বৈধ JSON, যার অর্থ এটি যেকোনো স্ট্যান্ডার্ড API-এর মধ্য দিয়ে অনায়াসেই যেতে পারবে। কিন্তু এর একটি তাৎক্ষণিক মূল্য রয়েছে। এই রিকোড করার ফলে ফাইলের আকার প্রায় ৩৩ শতাংশ বেড়ে যায়। একটি তিন মেগাবাইট ইমেজ নেটওয়ার্কের মাধ্যমে পাঠানোর সময় চার মেগাবাইট হয়ে যায়। ডেটা আদান-প্রদানের সময় ক্লায়েন্ট এবং সার্ভার উভয়কেই অতিরিক্ত CPU সাইকেল খরচ করতে হয়। আরও গুরুত্বপূর্ণ বিষয় হলো, অনেক JSON সার্ভার ফ্রেমওয়ার্ক পার্স করার আগে পুরো বডি মেমোরিতে নিয়ে নেয়। কিছু সংখ্যক সমান্তরাল বড় আপলোড একটি সাধারণ সার্ভারকে অচল করে দিতে পারে, কারণ প্রতিটি ফাইল ডিস্কে সেভ হওয়ার আগে একটি অতিরিক্ত ওজনের টেক্সট স্ট্রিং হিসেবে RAM-এ জমা থাকে। Base64 জরুরি প্রয়োজনে কাজ করলেও, এটি কখনোই প্রোডাকশন স্কেলে ভারী ফাইল বহনের জন্য ডিজাইন করা হয়নি।
এর চেয়ে ভালো সমাধান হলো multipart/form-data। এই ফরম্যাটটি একটি একক HTTP রিকোয়েস্টকে আলাদা আলাদা অংশের সংগ্রহ হিসেবে বিবেচনা করে, যেখানে প্রতিটি অংশ একটি অনন্য বাউন্ডারি স্ট্রিং (boundary string) দ্বারা বিভক্ত থাকে। একটি অংশ হতে পারে একটি সাধারণ টেক্সট ফিল্ড। পরবর্তী অংশটি হতে পারে একটি র-বাইনারি ইমেজ, যা নিজস্ব Content-Type এবং Content-Disposition হেডার দ্বারা চিহ্নিত থাকে। সার্ভার ইনকামিং স্ট্রিমটি ক্রমানুসারে পড়ে, বাউন্ডারি মার্কারগুলোর জন্য অপেক্ষা করে এবং প্রতিটি সেকশনকে যথাযথ হ্যান্ডলারের কাছে পাঠিয়ে দেয়, যেখানে পুরো পেলোডটিকে একটি একক টেক্সট ব্লক হিসেবে দেখার প্রয়োজন হয় না।
Node.js-এর ক্ষেত্রে এই পার্থক্যটি অত্যন্ত গুরুত্বপূর্ণ। express.json() মিডলওয়্যার জানে কীভাবে JSON বডি পার্স করতে হয়, কিন্তু এটি ফাইল স্ট্রিম হ্যান্ডেল করতে পারে না। মাল্টিপার্ট আপলোড প্রসেস করার জন্য আপনার Multer বা Busboy-এর মতো একটি স্ট্রিমিং পার্সার প্রয়োজন। এই টুলগুলো র-রিকোয়েস্ট স্ট্রিমের সাথে যুক্ত হয় এবং এটি চঙ্ক (chunk) আকারে পড়ে। উদাহরণস্বরূপ, Multer আপনাকে ইনকামিং ফাইলগুলো ডিস্কের একটি টেম্পোরারি ফোল্ডারে লিখবেন নাকি ছোট ফাইলগুলো মেমোরিতে রাখবেন তা বেছে নিতে দেয়। এই কনফিগারেশন সিদ্ধান্তটি অত্যন্ত গুরুত্বপূর্ণ। আপনি যদি সবকিছু মেমোরিতে রাখেন এবং আপনার অ্যাপ হঠাৎ করে একসাথে বেশ কিছু বড় ফাইল গ্রহণ করে, তবে আপনার প্রসেস হিপ স্পেস (heap space) শেষ করে ক্র্যাশ করতে পারে। ডিস্কে লেখা মানে হলো স্থিতিশীলতার জন্য I/O-এর সাথে আপস করা, তবে এটি ক্লিনআপ এবং পাথ সিকিউরিটি নিয়ে নিজস্ব কিছু প্রশ্ন উত্থাপন করে।
একটি ছোট অ্যাপ্লিকেশনের জন্য, ./uploads-এর মতো লোকাল ফোল্ডারে ফাইল সেভ করা স্বাভাবিক এবং দ্রুত মনে হয়। ফাইলটি আপনার কোড চলা একই মেশিনে জমা হয় এবং এটি সার্ভ করা কেবল সঠিক পাথের দিকে নির্দেশ করার ব্যাপার মাত্র। এটি ততক্ষণ পর্যন্ত কাজ করে যতক্ষণ না কোনো সমস্যা দেখা দেয়।
যে মুহূর্তে আপনি একটি দ্বিতীয় অ্যাপ্লিকেশন সার্ভারের সামনে একটি লোড ব্যালেন্সার বসাবেন, লোকাল স্টোরেজ একটি ত্রুটিতে (bug) পরিণত হবে। একজন ব্যবহারকারী একটি প্রোফাইল পিকচার আপলোড করলেন। লোড ব্যালেন্সার রিকোয়েস্টটি Server A-তে পাঠালো এবং ফাইলটি Server A-এর ডিস্কে লেখা হলো। পরে, সেই ব্যবহারকারী ছবিটি দেখতে চাইলেন, কিন্তু লোড ব্যালেন্সার রিকোয়েস্টটি Server B-তে পাঠালো। Server B তার নিজস্ব ফাইল সিস্টেম চেক করল এবং কিছুই পেল না। ফাইলটি কার্যত হারিয়ে গেছে। আপনি ব্যবহারকারীকে একই মেশিনে আটকে রাখার জন্য sticky sessions ব্যবহার করতে পারেন, কিন্তু এটি একটি ভঙ্গুর সমাধান। যদি Server A রিস্টার্ট হয়, পুনরায় ডেপ্লয় করা হয় বা অটোস্কেলিং ইনস্ট্যান্স দ্বারা প্রতিস্থাপিত হয়, তবে ডেটা হারিয়ে যাবে। কন্টেইনারাইজড এনভায়রনমেন্টে লোকাল ডিস্ক আরও বেশি ক্ষণস্থায়ী (ephemeral)। একটি Docker কন্টেইনারের ফাইল সিস্টেম মূলত ডিসপোজেবল বা একবার ব্যবহারযোগ্য হওয়ার জন্য তৈরি। এটিকে স্থায়ী স্টোরেজ হিসেবে ব্যবহার করা ইউজার ডেটা হারানোর একটি নিশ্চিত উপায়।
এর আদর্শ সমাধান হলো কম্পিউট (compute) এবং স্টোরেজকে আলাদা করা। আপনি আপনার অ্যাপ্লিকেশন সার্ভারগুলোকে স্টেটলেস (stateless) রাখবেন এবং আপলোড করা ফাইলগুলো AWS S3 বা Google Cloud Storage-এর মতো ডেডিকেটেড অবজেক্ট স্টোরেজে পাঠিয়ে দেবেন। এই সার্ভিসগুলো স্থায়িত্ব (durability), ভৌগোলিক বিস্তৃতি এবং বিশাল কনকারেন্সি (concurrency)-র জন্য তৈরি করা হয়েছে। অ্যাপ্লিকেশন সার্ভার রিকোয়েস্টটি হ্যান্ডেল করে, মেটাডেটা যাচাই করে এবং তারপর বাইটগুলোকে বিশেষভাবে ডিজাইন করা ইনফ্রাস্ট্রাকচারের কাছে হস্তান্তর করে।
Yet even this pattern creates a bottleneck if you implement it carelessly. Many teams start by having the browser upload the file to the backend, and then having the backend forward every single byte to object storage. If a user uploads a five-hundred megabyte video, your server becomes a middleman. It consumes bandwidth pulling the file in, then consumes more bandwidth pushing it out to S3. The connection stays open for the entire duration of the transfer. Slow uploads from users with poor network conditions can tie up server connections for minutes. Memory usage stays elevated if the server buffers the stream, and if you are running on metered hosting, you are paying twice for the same data transfer. Horizontal scaling does not solve this, because every additional server you add still gets stuck ferrying bytes it does not need to see.
Modern systems solve the problem by removing the backend from the data path entirely. Instead of accepting the file, the backend only accepts a request for permission to upload. The flow looks like this:
- The browser asks the backend to initiate an upload, usually sending only the filename, file type, and intended purpose.
- The backend authenticates the user, validates the request against business rules, and uses an SDK to generate a temporary presigned URL from the object storage provider.
- The backend returns that URL to the browser. The URL is scoped to a specific bucket and key, valid for a short window such as five or fifteen minutes, and signed with a token that grants only the precise permissions needed.
- The browser uploads the file directly to S3 or GCS using a standard PUT or POST. The bytes travel straight from the user’s device to
