MongoDB Time Series Collections: Storage, Bucketing, and Limitations

6 min readMongoDB

Sensor readings, stock ticks, server metrics, clickstream events: time series data is high-volume, append-mostly, and almost always queried by time range plus some identifier. Storing each measurement as a regular document works, but it is wasteful -- every document repeats the same field names, and a year of one-per-second readings is 31 million documents competing for cache.

Time series collections, introduced in MongoDB 5.0, are a specialized collection type that buckets measurements together behind the scenes. You read and write individual measurement documents as usual; MongoDB groups them into compressed internal buckets, which cuts storage and speeds up time-range scans. The trade-off is a substantial list of restrictions, several of which surprise people in production. This guide covers both sides.

Creating a time series collection

Unlike regular collections, time series collections must be created explicitly -- you cannot convert an existing collection in place, and the defining options are fixed at creation:

db.createCollection("weather", {
  timeseries: {
    timeField: "timestamp",   // required: BSON date in every document
    metaField: "metadata",    // optional but almost always wanted
    granularity: "minutes"    // seconds (default) | minutes | hours
  },
  expireAfterSeconds: 86400 * 30  // optional TTL: auto-delete after 30 days
})
  • timeField is required. Every inserted document must contain a BSON date in this field.
  • metaField identifies the source of the measurement: a sensor ID, a host name, a ticker symbol. It can be a scalar or a subdocument with multiple fields. MongoDB groups documents into buckets by metaField value, so a well-chosen metaField is the single biggest factor in storage and query efficiency.
  • timeField and metaField cannot be changed after creation. If you got them wrong, you create a new collection and move the data.

Inserting and querying look exactly like a normal collection:

db.weather.insertOne({
  timestamp: ISODate("2026-06-11T08:00:00Z"),
  metadata: { sensorId: 5578, type: "temperature" },
  temp: 12.1
})
 
db.weather.find({
  "metadata.sensorId": 5578,
  timestamp: { $gte: ISODate("2026-06-10T00:00:00Z") }
})

How bucketing works

Internally, a time series collection is a writable non-materialized view over a system collection of buckets. Each bucket holds many measurements that share a metaField value and fall within a time span, stored in a columnar, compressed layout. Field names are stored once per bucket instead of once per document, and consecutive values compress well.

Since MongoDB 6.3, creating a time series collection also creates a compound index on {metaField, timeField} automatically, which is what most queries want.

granularity

granularity tells MongoDB how far apart consecutive measurements from the same source typically are, which controls the bucket time span:

  • "seconds" (default) -- bucket spans up to 1 hour
  • "minutes" -- bucket spans up to 24 hours
  • "hours" -- bucket spans up to 30 days

Set it to the value closest to your actual ingest interval. If a sensor reports every 5 minutes and granularity is "seconds", buckets close while nearly empty and you lose most of the compression benefit. You can increase granularity later with collMod (5.0.1+), but never decrease it.

Custom bucket boundaries (6.3+)

If you query fixed windows -- say, 4-hour reporting periods starting at midnight -- the custom bucketing parameters give precise control:

db.createCollection("weather4h", {
  timeseries: {
    timeField: "timestamp",
    metaField: "metadata",
    bucketMaxSpanSeconds: 14400,    // max time between timestamps in a bucket
    bucketRoundingSeconds: 14400    // new bucket min time rounds down to this
  }
})

Both parameters must be set to the same value, and they replace granularity. With 14400 (4 hours), a document at 16:24 that needs a new bucket gets one spanning 16:00 to 19:59:59 -- aligned to your query windows, so a report query touches exactly the buckets it needs.

TTL expiry

expireAfterSeconds deletes measurements automatically once their timeField passes the age threshold. This is the supported way to age out old data, and it matters more than on regular collections because manual deletes are restricted (see below). You can add or change it later with collMod.

The restrictions that matter

This is where time series collections earn their reputation for surprises. As of MongoDB 8.0 (June 2026), the official limitations list includes:

Updates can only touch the metaField. Update commands must match on the metaField only, modify the metaField only, use multi: true, and cannot upsert. You cannot fix a mis-recorded temperature value with updateOne. The practical pattern for correcting measurements is insert-a-new-version or rewrite into a new collection. Deletes have loosened over time (MongoDB 7.0 removed most delete restrictions), but if your workload genuinely needs arbitrary updates, a time series collection is the wrong tool.

No change streams. Time series collections do not support change streams, Atlas Search, client-side field level encryption, schema validation rules, or database triggers. If you planned to stream measurements onward with a change stream, you will need to restructure (for example, write to a regular collection and $out periodically).

No writes in transactions. Reads inside transactions work; writes do not.

Aggregation gaps. $merge cannot write into a time series collection (use $out). The distinct command is unsupported -- use $group instead. Geospatial queries are limited to $geoNear against 2dsphere indexes; $near and $nearSphere do not work.

Document size cap of 4 MB instead of the usual 16 MB.

No default _id index. Unlike regular collections, no index is created on _id. Secondary indexes are supported on metaField and timeField (and measurement fields since 6.0), but reIndex and renameCollection are not.

Sharding caveats. Sharded time series collections require the shard key to use the metaField; shard keys containing the timeField are deprecated as of 8.0. Resharding requires 8.0.10+. Zone sharding is unsupported entirely.

Timestamps outside 1970-2038 (the 32-bit epoch range) degrade query optimization unless you create an index on the timeField.

When to use one

A time series collection is the right call when data is append-only or append-mostly, every document has a timestamp, queries are dominated by time ranges per source, and old data expires rather than being edited. Metrics, IoT telemetry, and event logs fit well.

Stick with a regular collection (plus a compound index on {source, timestamp} and possibly a TTL index) when you need to update measurements after the fact, run change streams, write inside transactions, or validate schemas. A regular collection does everything a time series collection does, just with more disk and slower range scans -- which may be a fair price for flexibility.

For high-volume insert-order logging without timestamps as the organizing principle, see also capped collections. For aggregating measurement data once stored, the aggregation pipeline guide and $group deep dive cover the query side, and date-operators covers $dateTrunc bucketing in queries.

Inspecting an existing collection

To check a collection's time series parameters:

db.runCommand({ listCollections: 1, filter: { name: "weather" } })

The output shows type: 'timeseries' and the configured timeField, metaField, granularity or custom bucketing values, and expireAfterSeconds.


Mako connects to MongoDB with AI-powered autocomplete for building queries against any collection type, time series included. Try it free at mako.ai.

Mako — open source

Skip the terminal. Use Mako.

Connect your database, write queries with AI assistance, and import/export data in clicks. Free to start.