Learn how GridFS stores files bigger than 16 MB in MongoDB, how to upload and download them with mongofiles and Node.js, and when to use object storage.

Bishal Biswas
Self Learner
What you will learn
By the end of this lesson you will know:
- Why you can’t just put a big file inside a normal document
- How GridFS splits a file into chunks and stores it in two collections
- How to upload, list, download and delete files with the mongofiles tool
- How to do the same from a Node.js app with
GridFSBucket - When GridFS is a good idea and when a cloud storage service is better
The 16 MB limit
Every MongoDB document has a maximum size of 16 MB. That’s plenty for normal data: a product, a customer, an order with 200 items.
But what about a 40 MB product video, a 200 MB PDF catalogue, or a large backup file? They don’t fit in one document.
GridFS is MongoDB’s built-in way to store files of any size. It’s not a separate server or product. It’s a simple convention that every MongoDB driver understands: split the file into small pieces and store each piece as its own document.
Think of it like this: You can’t post a sofa in one envelope. So you take it apart, put each piece in a numbered box, and include a packing list. At the other end, someone uses the list to put the sofa back together. GridFS does exactly this with files.
How GridFS stores a file
GridFS uses two collections. By default they are called fs.files and fs.chunks. The fs part is the bucket name, which you can change.
shop (database)
├── fs.files (one document per file: the "packing list")
└── fs.chunks (many documents per file: the "numbered boxes")
fs.files: the packing list
Each uploaded file gets one document in fs.files that describes it:
{
_id: ObjectId("6718d4a1b2c3d4e5f6a7b8c9"),
length: 734003, // file size in bytes
chunkSize: 261120, // size of each piece (255 KB by default)
uploadDate: ISODate("2026-10-25T08:30:00Z"),
filename: "headphones.jpg",
metadata: { productName: "Headphones", uploadedBy: "Asha" } // your own extra info
}
fs.chunks: the pieces
The file’s bytes are split into chunks of 255 KB each (the last one can be smaller). Each chunk is a small document:
{
_id: ObjectId("..."),
files_id: ObjectId("6718d4a1b2c3d4e5f6a7b8c9"), // which file this piece belongs to
n: 0, // piece number: 0, 1, 2, ...
data: BinData(0, "...") // the raw bytes
}
Our 734,003-byte image needs three chunks: n: 0, n: 1 and n: 2. To read the file back, a driver finds all chunks with that files_id, sorts them by n, and joins them.
Note: GridFS works with any file size, even small ones. But it is only needed for files bigger than 16 MB. A 50 KB avatar fits fine in a normal document as binary data.
Trying GridFS with mongofiles
The quickest way to play with GridFS is mongofiles, a command-line tool that is part of the MongoDB Database Tools (the same package as mongodump and mongoimport, covered in the next lesson). If mongofiles --version prints a version, you’re ready.
Upload a file
Find any file on your computer, for example an image, and upload it into the shop database:
mongofiles --uri="mongodb://localhost:27017" --db=shop put headphones.jpg
2026-10-25T08:30:00.000+0530 connected to: mongodb://localhost:27017
2026-10-25T08:30:00.000+0530 adding gridFile: headphones.jpg
2026-10-25T08:30:00.000+0530 added gridFile: headphones.jpg
On Windows PowerShell the command is the same:
mongofiles --uri="mongodb://localhost:27017" --db=shop put .\headphones.jpg
List and search files
mongofiles --db=shop list
# headphones.jpg 734003
mongofiles --db=shop search head
# headphones.jpg 734003
Download a file
get saves the file back to disk. Use --local to choose the output name:
mongofiles --db=shop get headphones.jpg --local=headphones-copy.jpg
Delete a file
mongofiles --db=shop delete headphones.jpg
This removes both the fs.files document and all of its chunks.
Look inside with mongosh
After uploading, open mongosh and peek at what GridFS created:
use shop
db.fs.files.find({}, { filename: 1, length: 1, chunkSize: 1 })
// → { _id: ObjectId('...'), filename: 'headphones.jpg', length: 734003, chunkSize: 261120 }
db.fs.chunks.countDocuments()
// → 3
Warning: Don’t edit
fs.filesorfs.chunksby hand (except themetadatafield). If a chunk goes missing orlengthis wrong, the file becomes unreadable.
GridFS in a Node.js app
In a real app, files usually arrive from a user upload form. The Node.js driver gives you a class called GridFSBucket that handles all the chunking for you, using streams so even huge files never need to fit in memory.
Create a bucket
Let’s use a bucket named productImages. Its collections will be productImages.files and productImages.chunks:
import { MongoClient, GridFSBucket } from "mongodb";
const client = new MongoClient("mongodb://localhost:27017");
const db = client.db("shop");
const bucket = new GridFSBucket(db, { bucketName: "productImages" });
Upload a file
openUploadStream() gives you a stream to write the file into. We read a file from disk and pipe it in. The pipeline helper waits until the upload is finished and reports errors properly:
import fs from "node:fs";
import { pipeline } from "node:stream/promises";
const uploadStream = bucket.openUploadStream("headphones.jpg", {
metadata: { productName: "Headphones", uploadedBy: "Asha" }
});
await pipeline(fs.createReadStream("./headphones.jpg"), uploadStream);
console.log("Saved with id:", uploadStream.id);
// → Saved with id: new ObjectId('6718d4a1b2c3d4e5f6a7b8c9')
Save that id somewhere useful, for example on the product document:
await db.collection("products").updateOne(
{ name: "Headphones" },
{ $set: { imageFileId: uploadStream.id } }
);
Find files
bucket.find() searches the files collection with a normal filter:
const files = await bucket.find({ "metadata.productName": "Headphones" }).toArray();
for (const file of files) {
console.log(file.filename, file.length, file.uploadDate);
}
// → headphones.jpg 734003 2026-10-25T03:00:00.000Z
Download a file
Open a download stream by id (or by name) and pipe it wherever you want: a file on disk, or the HTTP response in a web server.
// By id
await pipeline(
bucket.openDownloadStream(uploadStream.id),
fs.createWriteStream("./downloaded.jpg")
);
// By name (gets the newest file with that name)
await pipeline(
bucket.openDownloadStreamByName("headphones.jpg"),
fs.createWriteStream("./downloaded-by-name.jpg")
);
Here’s how it looks in an Express route that sends a product image to the browser:
import { ObjectId } from "mongodb";
app.get("/images/:id", async (req, res) => {
try {
const id = new ObjectId(req.params.id);
res.set("Content-Type", "image/jpeg");
await pipeline(bucket.openDownloadStream(id), res);
} catch (err) {
res.status(404).send("Image not found");
}
});
Rename and delete
await bucket.rename(uploadStream.id, "headphones-black.jpg");
await bucket.delete(uploadStream.id); // removes the files entry and all its chunks
Streaming part of a file
Because the file is stored in numbered chunks, GridFS can read just part of it without loading the rest. That’s great for video players that jump to the middle:
// Read only bytes 1,000,000 to 1,999,999
const partial = bucket.openDownloadStream(videoId, { start: 1_000_000, end: 2_000_000 });
Should I use GridFS?
GridFS is handy, but it is not always the best home for files.
GridFS is a good fit when:
- Files are bigger than 16 MB and you want them in MongoDB
- You want files to be backed up and replicated together with the rest of your data
- You can’t (or don’t want to) add another service to your system
- You want to store metadata with each file and query it like normal data
Object storage is often better when:
- You serve lots of images or videos to the public. Services like Amazon S3, Google Cloud Storage, Azure Blob Storage or Cloudflare R2 are cheaper per GB and work with CDNs.
- Files are huge and many, so they would make your database (and its backups) very large
A common pattern is to put the file in object storage and save only its URL and details in MongoDB:
db.products.updateOne(
{ name: "Headphones" },
{ $set: { image: { url: "https://cdn.example.com/products/headphones.jpg", sizeBytes: 734003 } } }
)
| Need | Choose |
|---|---|
| Small binary (under a few MB), like a thumbnail | Normal document with a binary field |
| Big files, kept together with your data | GridFS |
| Lots of public media, served fast worldwide | Object storage + URL in MongoDB |
Tip: GridFS files are not “updated in place”. To change a file, upload the new version and delete the old one.
Common mistakes
- Using GridFS for tiny files. Storing 20 KB icons as GridFS files adds two collections and extra queries for no gain. A binary field in a normal document is simpler.
- Reading the whole file into memory. In Node.js, always use streams (
pipeline) instead of collecting the whole file into one bigBuffer. Big files will crash your app otherwise. - Deleting only from
fs.files. That leaves orphan chunks behind, taking up space. Usebucket.delete()ormongofiles delete, which clean up both collections. - Forgetting the bucket name. If you uploaded with
bucketName: "productImages", you must use the same bucket name to find or download the file. With mongofiles, add--prefix=productImages. - Assuming file names are unique. GridFS allows many files with the same
filename. Use the_idto point to a specific one.
Practice
- Using mongofiles, upload any PDF to the
shopdatabase, list the files, and then download it back with a new name. - In mongosh, find the uploaded file’s size and count how many chunks it has.
- In Node.js, write code that uploads
catalogue.pdfinto a bucket calleddocumentswithmetadata: { year: 2026 }, then lists every file in that bucket from 2026.
Show a possible answer
# 1
mongofiles --db=shop put catalogue.pdf
mongofiles --db=shop list
mongofiles --db=shop get catalogue.pdf --local=catalogue-copy.pdf
// 2 (mongosh)
use shop
const file = db.fs.files.findOne({ filename: "catalogue.pdf" })
file.length
db.fs.chunks.countDocuments({ files_id: file._id })
// 3 (Node.js)
import fs from "node:fs";
import { pipeline } from "node:stream/promises";
import { MongoClient, GridFSBucket } from "mongodb";
const client = new MongoClient("mongodb://localhost:27017");
const bucket = new GridFSBucket(client.db("shop"), { bucketName: "documents" });
await pipeline(
fs.createReadStream("./catalogue.pdf"),
bucket.openUploadStream("catalogue.pdf", { metadata: { year: 2026 } })
);
const files = await bucket.find({ "metadata.year": 2026 }).toArray();
console.log(files.map(f => f.filename));
await client.close();
Key takeaways
- A single MongoDB document can be at most 16 MB; GridFS lets you store bigger files.
- GridFS splits a file into 255 KB chunks in
fs.chunksand keeps one description document infs.files. - mongofiles (
put,list,search,get,delete) is the quickest way to try GridFS from the terminal. - In Node.js,
GridFSBucketwithopenUploadStream()andopenDownloadStream()handles files using streams. - For lots of public media, object storage plus a URL in MongoDB is often cheaper and faster than GridFS.
Next lesson: Import, Export and Data Migration Tools
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready