MongoDB documents have a BSON size limit of sixteen mebibytes. GridFS provides a supported file-storage format that separates file bytes into chunks and records file information in another collection. It also makes smaller files accessible through streaming APIs. This guide uses MongoDB 8.0, Node.js 24, and the official mongodb driver 6.20.0. In a disposable project, run npm install mongodb@6.20.0 and save each complete example as an .mjs file. Set MONGODB_URI for your teaching database connection. The fallback URI deliberately names the isolated local test port 27181. The examples use tiny strings and unusually small chunks to make the storage behavior visible, not to suggest a production chunk size.
Understand files, chunks, and identifiers
A GridFS bucket uses two collections, conventionally bucketName.files and bucketName.chunks. The files collection stores metadata such as identifier, length, chunk size, upload date, filename, and application metadata. Chunk documents associate sequential pieces with that file identifier. The driver manages the format and required indexes; applications should use its GridFS API rather than inventing their own chunk numbering conventions.
The upload stream exposes an id that identifies the new file. Filenames are labels and need not be unique. Two uploads called report.txt can coexist, so a download that must retrieve an exact version should use its file ID. Store that ID in the application record that owns the file. Avoid treating a user-supplied filename as proof of access permission or ownership. A filename-based revision lookup is useful only when its documented ordering and revision contract match the product behavior. The fixture downloads by ID to eliminate that ambiguity.
Wait for upload completion before publication
Writing some bytes to an upload stream does not mean the full file is available. Await a promise-based pipeline from the source into the GridFS upload stream. A successful pipeline resolves after the stream completes; a failure rejects and must be handled. Capture the ID, finish the upload, and only then publish an application pointer that lets readers discover that file.
GridFS does not support multi-document transactions as a way to make an entire file replacement atomic. A versioned replacement pattern can upload a new file first, then atomically update a separate ordinary document's active file ID. Readers resolve that pointer once and download that immutable file version. The old version can be removed later according to policy. A crash after upload but before pointer publication may leave an unreferenced file, so cleanup requires a retention or reconciliation rule. Do not delete the previous active file before the new upload and pointer update have succeeded.
Keep streams bounded and errors visible
Node's pipeline propagates stream errors and coordinates backpressure. That matters when an HTTP upload or filesystem source can produce more data than the database can accept immediately. An upload endpoint should also impose a byte limit and an authorization policy outside GridFS. The database format does not automatically enforce a application's maximum file size or validate a claimed media type.
For the tiny download demonstration, an async iterator collects chunks into an array and concatenates them. That is intentionally convenient only because the file is thirteen bytes. A large production download should pipe directly to an appropriate destination instead of buffering the entire file in memory. Keep both source and destination errors visible, handle client cancellation, and close the MongoClient when its owning application shuts down. A shared long-lived client is preferable to opening a new database connection for each uploaded chunk. The driver internally performs the chunk operations.
Choose integrity and cleanup policies explicitly
A successful download reconstructs the stored byte sequence, but application integrity requirements can call for an independently computed digest. Compute a modern hash over the exact source bytes and store or verify it under an application-controlled contract. Do not treat a filename, MIME label, or byte length as a cryptographic integrity proof. Avoid relying on historical GridFS MD5 behavior as a security mechanism.
Deletion should use the bucket's delete method with the file ID so file metadata and associated chunks are handled under the supported API. Failed or interrupted uploads need an explicit cleanup strategy; successful fixture runs do not simulate every failure boundary. If a file's content must change, create a new version rather than mutating chunk documents behind readers. Compare GridFS with filesystem or object-storage alternatives based on access patterns, backup policy, and operational requirements. The sixteen-mebibyte document limit explains one use case but does not require every small attachment to be stored in GridFS.
MongoDB documentation and version notes
MongoDB GridFS File Streaming Examples
Each example shows the code, the output it produces and what each part does.
Uploading and downloading a chunked GridFS file
This complete program resets only its disposable bucket collections, uploads hello GridFS! with a five-byte chunk size, and awaits pipeline completion. Thirteen bytes require three chunks. It then downloads the precise upload ID and confirms the stored length and reconstructed text. The metadata query happens after upload completion, so it observes a completed file. No authentication secret or external file is required for this local fixture. When adapting it to an endpoint, replace the in-memory source and destination while retaining bounded streaming and error handling.
import { MongoClient, GridFSBucket } from 'mongodb';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
const client = new MongoClient(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27181/?directConnection=true');
try {
await client.connect();
const demo = client.db('codevra_ref_gridfs');
await demo.collection('demo.files').deleteMany({});
await demo.collection('demo.chunks').deleteMany({});
const bucket = new GridFSBucket(demo, { bucketName: 'demo', chunkSizeBytes: 5 });
const upload = bucket.openUploadStream('greeting.txt', { metadata: { owner: 'learner' } });
const id = upload.id;
await pipeline(Readable.from([Buffer.from('hello GridFS!')]), upload);
const parts = [];
for await (const part of bucket.openDownloadStream(id)) parts.push(part);
const file = await demo.collection('demo.files').findOne({ _id: id });
console.log('text: ' + Buffer.concat(parts).toString('utf8'));
console.log('length: ' + file.length);
console.log('chunks: ' + await demo.collection('demo.chunks').countDocuments({ files_id: id }));
} finally { await client.close(); }text: hello GridFS!
length: 13
chunks: 3MongoDB GridFS File Streaming in Real Projects
Publish a new GridFS file version behind a pointer
Publish a second version only after it finishes uploading, and demonstrate that the first version remains independently readable. The solution uses a dedicated practice bucket and one ordinary pointers document. An async helper uploads and returns the completed file ID. The application pointer is first set to version one and then changed to version two with an atomic update. The final downloads resolve the active ID and the retained old ID separately. A production replacement may add a version predicate to prevent concurrent editors from overwriting one another's pointer updates; that concurrency policy is separate from GridFS's byte storage.
import { MongoClient, GridFSBucket } from 'mongodb';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
const client = new MongoClient(process.env.MONGODB_URI || 'mongodb://127.0.0.1:27181/?directConnection=true');
try {
await client.connect();
const demo = client.db('codevra_ref_gridfs');
for (const name of ['practice.files', 'practice.chunks', 'pointers']) await demo.collection(name).deleteMany({});
const bucket = new GridFSBucket(demo, { bucketName: 'practice' });
async function upload(text) {
const stream = bucket.openUploadStream('notes.txt');
await pipeline(Readable.from([Buffer.from(text)]), stream);
return stream.id;
}
async function download(id) {
const parts = [];
for await (const part of bucket.openDownloadStream(id)) parts.push(part);
return Buffer.concat(parts).toString('utf8');
}
const oldId = await upload('version one');
await demo.collection('pointers').insertOne({ _id: 'notes', fileId: oldId });
const newId = await upload('version two');
await demo.collection('pointers').updateOne({ _id: 'notes' }, { $set: { fileId: newId } });
const active = await demo.collection('pointers').findOne({ _id: 'notes' });
console.log('active: ' + await download(active.fileId));
console.log('retained: ' + await download(oldId));
} finally { await client.close(); }active: version two
retained: version oneMongoDB GridFS File Streaming FAQ
Is a filename a unique file key?
No. GridFS can store several uploads with the same filename. Use the file ID when the reader needs an exact version and store that identifier in the owning application record. A filename lookup can deliberately select a revision, but its documented rule must match the application contract. Do not let duplicate names silently choose which user's file is returned.
Why use a five-byte chunk size here?
It makes the three-chunk calculation visible with a thirteen-byte string. The default GridFS chunk size is much larger, and a real workload should choose settings from streaming and storage requirements. Tiny production chunks create excessive document and index overhead. A teaching fixture's small size makes inspection easy but supplies no useful benchmark for large-file throughput.
Can a transaction atomically replace all file bytes?
GridFS does not support multi-document transactions for that operation. Upload a new file version to completion, then atomically update an ordinary application pointer. That gives readers a stable version selection while preserving cleanup choices for old and abandoned versions. Add concurrency checks to the pointer update when multiple writers can publish replacements for the same logical file.
Should downloads always use Buffer.concat?
Only when the file is deliberately small and within a known memory budget. The fixture uses a thirteen-byte string, so collecting it makes the result easy to verify. Large downloads should stream to a destination with backpressure and propagate errors and cancellation. GridFS enables streaming; gathering the entire file into one Buffer would surrender that memory advantage.
MongoDB GridFS File Streaming Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
After the practice publishes version two, what does downloading oldId return?
MongoDB GridFS File Streaming Quiz
2 questions to check what you learned.
When should an application publish a new upload ID to readers?
- After the upload pipeline completes successfully
- Immediately after openUploadStream allocates the ID
- After the first source chunk is written
How many five-byte chunks hold the thirteen-byte main fixture?
- Three, with a shorter final chunk
- Two, because the final three bytes stay only in metadata
- Thirteen, because each character always becomes a separate chunk