A capped collection has a fixed byte size and, optionally, a fixed document count; when it is full the oldest document is overwritten. Nothing expires on a clock: you trade unbounded history for a guarantee that the collection will never fill the disk.
db.createCollection('audit', { capped: true, size: 4096, max: 5 });
for (let i = 1; i <= 9; i++) db.audit.insertOne({ n: i, msg: 'event ' + i });
const ns = () => db.audit.find({}, { _id: 0, n: 1 }).toArray().map(d => d.n).join(' ');
print('ns : ' + ns() + ' del : ' + db.audit.deleteOne({ n: 5 }).deletedCount);
print('grew: ' + db.audit.updateOne({ n: 9 }, { $set: { blob: 'x'.repeat(2000) } })
.modifiedCount + ' size=' + db.audit.stats().size);
for (let i = 10; i <= 12; i++) db.audit.insertOne({ n: i, msg: 'event ' + i });
print('now : ' + ns() + ' tail: ' + db.audit.find({}, { _id: 0, n: 1 })
.sort({ $natural: -1 }).limit(3).toArray().map(d => d.n).join(' '));ns : 5 6 7 8 9 del : 1 grew: 1 size=2195 now : 8 9 10 11 12 tail: 12 11 10
Nine inserts leave five documents because max: 5 caps the count; size caps the bytes, is required and is rounded up. Documents return in natural order, here insertion order, so sort({ $natural: -1 }) is the tail command and costs nothing. The delete and the size-increasing update both succeeded, which they would not have before server 5.0, though the manual still warns that a growing update evicts more than intended. A tailable cursor — db.audit.find().tailable({ awaitData: true }) — printed the last document and then blocked in hasNext() waiting for the next insert; that is how the oplog is read.
Build on neither. Capped collections cannot be sharded or written in a transaction, reject $out, are excluded from the Stable API, and serialize their writes. Prefer a TTL index for age-based cleanup, a time series collection for measurements, a change stream (Change Streams) to follow writes.