- A time series collection stores measurements in buckets: documents with an identical metaField value and nearby timeField values are grouped and compressed together in a columnar format.
- timeField is required and must hold a BSON date; metaField should identify the series (site, sensor) and rarely change; both are fixed when the collection is created.
- granularity (seconds, minutes, hours) or the custom pair bucketMaxSpanSeconds and bucketRoundingSeconds set the bucket span; match it to the interval between readings from one source.
- expireAfterSeconds removes whole buckets once every measurement in them has expired; deletion is not immediate.
- The manual lists real limits: no change streams, schema validation or unique indexes, restricted updates, no writes in transactions, a 4 MB document limit and sharding rules.
What MongoDB time series collections are, and who this is for
Since MongoDB 5.0, a collection can be created as a time series collection. The manual describes the idea simply: writes are organised so that data from the same source is stored alongside other data points from a similar point in time. Each document is a measurement with three parts: a time (the timeField), metadata that identifies the series and rarely changes (the metaField), and the metric values. Compared with a regular collection, the manual says this improves query efficiency and reduces disk usage for the data and its secondary indexes.
This guide is for teams whose applications already run on MongoDB (a CAFM portal, a mobile maintenance app, an IoT platform) and who want to keep sensor readings in the same database rather than add a separate time-series engine. It focuses on the MongoDB time series design choices and limits. For a head-to-head with a SQL option, see MongoDB Time Series vs TimescaleDB; for general background, see the time-series database overview. On 27 October 2026 the release notes listed MongoDB 9.0 as the current stable release series.
How bucketing works
Internally, MongoDB groups measurements into buckets. A document goes into an open bucket only if its metaField value is identical and its timeField falls inside the bucket's time span; otherwise a new bucket is created. A bucket is closed when time moves past its span, when it reaches 1,000 measurements or 125 KB (the manual's defaults), when the set of open buckets no longer fits in cache, or on restart and some conflicting operations. Closed buckets remain queryable. Inside a bucket, data is stored column by column and compressed.
- Storage format. Time series collections use zstd compression (the global default is snappy) plus column compression with delta and delta-of-delta encoding, object and array compression, and run-length encoding. The 9.0 manual states: "Starting in 9.0, MongoDB stores time series collections in single namespaces that contain the data compressed into organized groups, or buckets." When you query, MongoDB converts the bucketed data to one document per measurement. The 9.0 release notes add that time series collections "are no longer writable non-materialized views"; earlier releases behave differently, so check the release notes for the version you run.
- Index. From 6.3, MongoDB creates a compound index on
metaFieldandtimeFieldautomatically for new time series collections. There is no index on_id. - Cardinality. The number of buckets depends on the number of distinct
metaFieldvalues. Fine-grained or changing metadata creates many sparsely filled, short-lived buckets, which reduces storage and query efficiency.
Choosing timeField, metaField and granularity for building telemetry
timeField is required and must contain a BSON date. Use the time the reading was taken at the device or gateway, not the time it reached the database.
metaField should identify one series and be used in filters: for building data, an object such as { site: "HQ", pointId: "AHU-01.SAT" }. The manual advises choosing fields that rarely or never change, preferring stable identifiers used in filter expressions, and keeping fields that are not used for filtering out of the metaField (store them as measurements, or better in an asset collection). Do not put the reading value, a status that changes every minute or a unique message ID in the metaField, or each reading gets its own bucket. Query the metaField by its scalar sub-fields ("meta.site", "meta.pointId"), because MongoDB may reorder the fields of a metaField object.
granularity is "seconds" (default, one-hour buckets), "minutes" (24-hour buckets) or "hours" (30-day buckets). Set it to the value closest to the interval between readings from one source: "seconds" for 1 to 10-second BMS points, "minutes" for one-minute or five-minute points, "hours" for hourly meter reads. Consider typical queries too: the manual suggests "minutes" when queries fetch a day at a time. From 6.3 you can instead set bucketMaxSpanSeconds and bucketRoundingSeconds to the same value (for example 14,400 for four-hour reporting windows) when queries follow fixed intervals. Granularity or the custom values can later be increased with collMod, but never decreased.
db.createCollection("bms_readings", {
timeseries: {
timeField: "ts",
metaField: "meta",
granularity: "minutes"
},
expireAfterSeconds: 34214400 // 396 days
});
// one insertMany per batch, grouped by series, unordered
db.bms_readings.insertMany([
{ meta: { site: "HQ", pointId: "AHU-01.SAT" }, ts: ISODate("2026-10-27T08:00:00Z"), value: 13.8, quality: 0 },
{ meta: { site: "HQ", pointId: "AHU-01.SAT" }, ts: ISODate("2026-10-27T08:01:00Z"), value: 13.9, quality: 0 },
{ meta: { site: "HQ", pointId: "L3-Z04.CO2" }, ts: ISODate("2026-10-27T08:00:00Z"), value: 742, quality: 0 },
{ meta: { site: "HQ", pointId: "L3-Z04.CO2" }, ts: ISODate("2026-10-27T08:01:00Z"), value: 755, quality: 0 }
], { ordered: false });db.createCollection("meter_kwh", {
timeseries: {
timeField: "ts",
metaField: "meta",
bucketMaxSpanSeconds: 14400,
bucketRoundingSeconds: 14400
}
});Cross-checking a bucket boundary
The manual's example says that with 14,400-second custom bucketing, a document stamped 2023-03-27T16:24:35Z that needs a new bucket gets a bucket from 16:00:00 to 19:59:59. Rounding down to a 14,400-second boundary is the same arithmetic as PostgreSQL's date_bin, which confirms the boundaries. This ran on PostgreSQL 18.6 as a planning aid; it is not MongoDB code.
SET TimeZone = 'UTC';
SELECT date_bin(interval '14400 seconds', t, timestamptz '1970-01-01 00:00+00')::text AS bucket_min_time,
(date_bin(interval '14400 seconds', t, timestamptz '1970-01-01 00:00+00') + interval '14399 seconds')::text AS bucket_max_time
FROM (VALUES (timestamptz '2023-03-27 16:24:35+00')) AS v(t); bucket_min_time | bucket_max_time
------------------------+------------------------
2023-03-27 16:00:00+00 | 2023-03-27 19:59:59+00Retention with expireAfterSeconds
Set expireAfterSeconds when creating the collection, or add or change it later with collMod; set it to "off" to disable removal. A measurement expires when its timeField plus the interval has passed, but MongoDB removes data a bucket at a time: the bucket is deleted only once every measurement in it has expired, by a background task that runs every 60 seconds. Data can therefore stay visible for up to a bucket span (one hour, 24 hours or 30 days depending on granularity) after its own expiry, longer on a busy server. Size retention-related storage with that lag in mind. The 396-day and 90-day values in the examples on this page are illustrative assumptions, not recommendations; choose your own from how long operators and reports need raw readings, any record-keeping obligations, and storage cost.
One option for long-term trends is a separate rollup collection: a scheduled aggregation can $group readings by hour with $dateTrunc and write the results with $merge into a regular collection (the manual allows $merge from a time series collection to another collection, but not into one; use $out to write into a time series collection). Wider retention design is covered in Time-Series Retention and Downsampling Design.
db.runCommand({ collMod: "bms_readings", expireAfterSeconds: 7776000 }); // 90 days
db.bms_readings.aggregate([
{ $match: { "meta.site": "HQ",
ts: { $gte: ISODate("2026-10-26T00:00:00Z"), $lt: ISODate("2026-10-27T00:00:00Z") } } },
{ $group: {
_id: { site: "$meta.site", pointId: "$meta.pointId",
hour: { $dateTrunc: { date: "$ts", unit: "hour" } } },
avg: { $avg: "$value" }, min: { $min: "$value" }, max: { $max: "$value" }, n: { $sum: 1 } } },
{ $merge: { into: "bms_hourly", whenMatched: "replace", whenNotMatched: "insert" } }
]);Inserts, queries and indexes
- Batch writes. Use one
insertMany()per batch instead of manyinsertOne()calls, group measurements with the same metaField in the same batch, and setordered: false. Keep the field order identical in every document, round values to the precision you need, and omit empty fields; all three help compression. - Query patterns. Filter on the metaField sub-fields for equality and on
timeFieldfor ranges; the automatic compound index supports both. Add secondary indexes for other common filters, within the limits below. Use$groupinstead ofdistinct, which cannot be indexed efficiently on time series collections. - Late and duplicate readings. Out-of-order readings are accepted, but a reading that falls outside every open bucket opens a new one, so heavy backfills create extra buckets. There is no unique index, so duplicate replays from a gateway are stored twice unless the writer deduplicates; see Handling Duplicate Telemetry Events in SQL for patterns.
- Sharding. Shard on the metaField or its sub-fields (for example a hashed
meta.site). Shard keys containing the timeField are deprecated from 8.0, zone sharding is not supported, and sharding administration commands cannot be run on sharded time series collections.
If a $group over a long range fails with a memory error, see the MongoDB error library entry for code 292 (memory limit without external sorting); creating a time series collection over an existing name returns NamespaceExists (48). Other server errors are listed in the MongoDB error library.
Limitations in the current manual
Time series collections behave like regular collections with exceptions that matter for production. As listed in the manual on 27 October 2026:
| Area | What the manual says | Practical effect |
|---|---|---|
| Unsupported features | MongoDB Search, change streams, Client-Side Field Level Encryption, Database Triggers, schema validation rules and reIndex are not supported | No change-stream alerts from the collection; validate readings before insert |
| Updates | Update commands must meet these requirements: "You can only match on the metaField field value." "You can only modify the metaField field value." The update document may contain only update operator expressions, must not limit the number of documents updated (multi: true or updateMany()), and "must not set upsert: true". The 7.0 and 8.0 manuals list the same requirements. | Measured values cannot be corrected with an update. On deletes the documentation is incomplete: the limitations pages for 9.0, 8.0 and 7.0 list update requirements and, for old data, say "To automatically delete old data, set up automatic removal (TTL).", but describe neither support for nor restrictions on deleting individual measurements. We did not run delete-and-re-insert of a single measurement; test it on your version. Keeping corrections in a separate collection is our design suggestion, not a manual recommendation. |
| Indexes | No default _id index; no unique or text indexes; multikey, 2d and sparse indexes only on the metaField | Database cannot enforce one reading per sensor and time |
| Document size | Maximum 4 MB per document | Keep each measurement small; do not embed arrays of samples |
| Collection type | Cannot convert to or from a time series collection; timeField and metaField are fixed at creation | Decide the design before loading; migrations copy data |
| Granularity | Bucket span can be increased but not decreased; bucket limit 1,000 measurements or 125 KB | Start with the finest span you will need |
| Transactions | Writes in transactions are not supported (reads are) | Keep work-order updates and readings in separate operations |
| Capped collections | A time series collection cannot be capped | Use expireAfterSeconds for retention |
Sizing, security and operations
- Sizing. Storage depends on bucket fill: few, stable series with regular readings fill buckets well; thousands of series with sparse readings do not. Load a representative week of data and check collection statistics before sizing a cluster, rather than relying on a general compression ratio.
- Bucket catalogue memory. Open buckets are tracked in an in-memory catalogue (by default up to 2.5 per cent of system memory). Very high series counts with irregular arrival close buckets early and increase write work.
- Security. Client-Side Field Level Encryption is not available on time series collections, so rely on TLS in transit, encryption at rest, network isolation and least-privilege roles; readings rarely contain personal data, but tenant and location metadata may be commercially sensitive.
- Upgrades. Time series collections are a backwards-incompatible feature: the manual lists downgrade paths that require dropping them first. Check this before upgrading a replica set you may need to roll back.
- Atlas. Time series collections can be created from the Atlas UI as well as from drivers and mongosh; pricing depends on the Atlas tier and is not covered here.
- Licence. The MongoDB server repository README states that versions released before 16 October 2018 "are published under the AGPL" and that later versions, including patch fixes for prior versions, are published under the Server Side Public License (SSPL) v1; the licence text in the repository is headed "Server Side Public License VERSION 1, OCTOBER 16, 2018". MongoDB Atlas is a commercial service on its own terms. This is a factual description, not legal advice: read the README, the licence and any service terms, and take advice for your situation.
When not to use MongoDB time series collections
- You do not already run MongoDB. Adopting MongoDB only for telemetry brings a new operational stack; a SQL time-series option such as TimescaleDB may fit a SQL-based CAFM or CMMS better.
- Readings that must be unique or corrected in place. Without unique indexes and with metaField-only updates, removing duplicate readings and correcting values are the application's job.
- Event-driven alarms from the database. Change streams and triggers are not supported; raise alarms in the streaming layer (Kafka consumers or the MQTT gateway) instead.
- Very large analytical workloads. Our editorial view: portfolio-wide analysis over years of second-level data is likely to fit a column store better, such as ClickHouse or a warehouse.
- Unordered or non-time data. The manual says time series collections are not intended for unordered data or data that is not time-dependent.
For a wide-column alternative whose documentation names high write volumes and multi-site availability among its design goals, see Apache Cassandra for High-Volume Telemetry, and for a shortlist across engines, the guide to choosing a time-series database.
Frequently asked questions
What is a MongoDB time series collection?
A collection type, available since MongoDB 5.0, that stores measurements in compressed, time-ordered buckets grouped by a metaField that identifies each series. The manual says it improves query efficiency and reduces disk usage compared with a regular collection.
What should I use as the metaField for sensor data?
A small object of stable identifiers you filter on, such as site and sensor or point ID. Avoid values that change often or are unique per reading, because each distinct metaField value gets its own buckets.
Which granularity should I choose?
The one closest to the interval between readings from a single source: seconds for second-level data, minutes for minute-level data, hours for hourly data. You can increase it later with collMod but not decrease it.
Does expireAfterSeconds delete readings exactly on time?
No. MongoDB removes a whole bucket once all of its measurements have expired, using a background task that runs every 60 seconds, so data can remain for some time after its own expiry.
Can I update a reading in a time series collection?
Only in limited ways. The current (9.0) manual says "You can only match on the metaField field value" and "You can only modify the metaField field value", and the 7.0 and 8.0 manuals say the same. A measured value therefore cannot be corrected with an update. The documentation we checked does not say whether a single measurement can be deleted and re-inserted (the limitations page covers updates and points to TTL for removing old data); we did not run it, so test it on your version. Another option is, as a design suggestion of ours, recording corrections in a separate collection that reports read in preference to the raw data.
Sources
- MongoDB manual: Time Series
- MongoDB manual: About Time Series Data
- MongoDB manual: Create and Query a Time Series Collection
- MongoDB manual: Set Granularity for Time Series Data
- MongoDB manual: Set up Automatic Removal for Time Series Collections (TTL)
- MongoDB manual: Time Series Collections Considerations
- MongoDB manual: Best Practices for Time Series Collections
- MongoDB manual: Time Series Collection Limitations
- MongoDB manual: Time Series Compression
- MongoDB manual: Shard a Time Series Collection
- MongoDB manual: Release Notes
- MongoDB manual: Release Notes for MongoDB 9.0
- MongoDB 8.0 manual: Time Series Collection Limitations
- MongoDB 7.0 manual: Time Series Collection Limitations
- mongodb/mongo repository: README (licence section)
- mongodb/mongo repository: LICENSE-Community.txt (SSPL v1)
Checked 27 October 2026.
How we research these articles: our editorial method. Research-based from official documentation; SQL examples are run on a local database where the page says so. CodeWithSQL earns nothing from the vendors mentioned.