GridFS — Storing Large Files
MongoDB documents are capped at 16 MB. GridFS is the built-in convention for storing files larger than that limit by splitting them into chunks and spreading them across two collections. It's a MongoDB-native option, but it is not always the right one — know when to reach for it and when to reach for object storage instead.
When to Use GridFS
Files larger than 16 MB that must be stored inside MongoDB itself (e.g. no separate object storage service available).
You need to query file metadata (uploader, tags, content type) alongside the binary data using MongoDB queries.
You want file reads/writes to participate in the same replication and backup story as the rest of your data.
Range queries into part of a file matter (e.g. seeking into a video) without loading the whole file into memory.
When NOT to Use GridFS
High-traffic public file serving — a CDN in front of object storage will always outperform serving files through your database.
Files that change frequently — GridFS has no in-place update; a changed file is a new set of chunks.
Cost-sensitive storage of very large volumes — dedicated object storage is significantly cheaper per GB than database storage/replication overhead.
The fs.files and fs.chunks Collections
GridFS splits a file into chunks (255 KB by default) stored in fs.chunks, with one metadata document per file in fs.files.
fs.files — one document per uploaded file
{
_id: ObjectId("..."),
length: 4823221,
chunkSize: 261120,
uploadDate: ISODate("2026-07-10T12:00:00Z"),
filename: "report.pdf",
metadata: { contentType: "application/pdf", uploadedBy: "user123" }
}fs.chunks — the actual binary data, split up
{
_id: ObjectId("..."),
files_id: ObjectId("..."), // references the fs.files document
n: 0, // chunk sequence number
data: BinData(0, "...") // up to chunkSize bytes
}
// A 4.8MB file with a 255KB chunk size becomes ~19 chunk documentsUploading a File (Node.js Driver)
import { GridFSBucket } from 'mongodb'
import fs from 'fs'
const bucket = new GridFSBucket(db, { bucketName: 'uploads' })
fs.createReadStream('./report.pdf')
.pipe(bucket.openUploadStream('report.pdf', {
metadata: { contentType: 'application/pdf', uploadedBy: 'user123' }
}))
.on('finish', () => console.log('Upload complete'))Downloading a File
const downloadStream = bucket.openDownloadStreamByName('report.pdf')
downloadStream
.pipe(fs.createWriteStream('./downloaded-report.pdf'))
.on('finish', () => console.log('Download complete'))
// Or stream directly to an HTTP response in an API route
app.get('/files/:id', (req, res) => {
bucket.openDownloadStream(new ObjectId(req.params.id)).pipe(res)
})Deleting a File
await bucket.delete(new ObjectId(fileId)) // Removes both the fs.files metadata document and every associated chunk
GridFS vs Storing a File Path / URL
GridFS | Store Path/URL to S3 etc. | |
|---|---|---|
File larger than 16MB | Handled natively | N/A — file lives outside Mongo entirely |
Query file metadata via MongoDB | Yes, in fs.files | Yes, if you store metadata in your own collection |
Serving via CDN | Not built-in | Native — S3/CloudFront, GCS/Cloud CDN, etc. |
Storage cost at scale | Higher (DB storage + replication) | Lower — object storage is built for this |
Operational complexity | One less external service | One more service to configure/manage |
Best for | Small deployments, files tied tightly to DB records | Most production apps serving user files at scale |
Chunk Size
The default chunk size is 255 KB, which works well for most files. You can tune it per bucket — larger chunks reduce the number of chunk documents (less overhead per read), while smaller chunks make partial/range reads cheaper.
const bucket = new GridFSBucket(db, {
bucketName: 'videos',
chunkSizeBytes: 1024 * 1024 // 1MB chunks — fewer chunk docs for large video files
})fs.files.filename and any metadata fields you query on — GridFS creates a compound index on { files_id, n } for fs.chunks automatically, but does not index your custom metadata fields.Summary
GridFS splits files larger than 16MB across fs.files (metadata) and fs.chunks (binary data).
Use it when files must live inside MongoDB and be queried alongside other data — otherwise prefer S3/GCS/Azure Blob with a URL/path stored in your documents.
The driver exposes upload/download as Node.js streams, so large files never need to be fully loaded into memory.
Tune chunkSizeBytes for your access pattern — larger chunks for sequential full-file reads, smaller for range/seek-heavy access.