Tags or Fields? The InfluxDB Schema Decision That Decides Whether Your Database Survives
Tags are indexed, fields are not — and that one fact decides whether your InfluxDB instance scales or runs out of memory. A practical guide to the tags-vs-fields decision and series cardinality.
02 Mar 2026, 00:07 UTC

A team wires up application metrics, everything looks fine in staging, and three weeks after launch the influxd process is eating all available RAM and queries time out. The postmortem almost always lands on the same root cause: somebody put an unbounded identifier — a user ID, a request ID, a container ID — into a tag. This is the single most consequential schema decision in InfluxDB, and it's hard to fix after the fact.
The thesis is simple: in InfluxDB (1.x and 2.x, the TSM/TSI storage engines), tags are indexed and fields are not. Everything else — memory pressure, query speed, write throughput — follows from that one fact.
What tags and fields actually do
Every point in InfluxDB has a measurement name, a timestamp, and then your choice of tags and fields. Tags are string key-value pairs that get indexed in the TSI (Time Series Index). Fields hold the actual measured values — floats, integers, booleans, strings — and are stored in the TSM data files without an index.
The practical consequence: filtering or grouping by a tag is an index lookup. Filtering by a field value means scanning data. So WHERE region = 'eu-west' is cheap if region is a tag, and expensive if it's a field.
Series cardinality is the real constraint
A "series" is one unique combination of measurement plus tag set. InfluxDB keeps index structures for every series in memory, so total series count — cardinality — is the dominant scaling limit. Ten hosts times five regions is 50 series: trivial. A user_id tag across two million users is two million series per measurement: that's when memory usage climbs into the gigabytes and the index compaction starts hurting.
The rule of thumb that holds up in practice:
- Make it a tag only if you will filter or group by it and its value set is bounded and known in advance: host, region, environment, sensor type, status code class.
- Keep it as a field if it's a measured value or an unbounded identifier: latency, temperature, request ID, user ID, session ID.
- If you need to look up details by user ID, keep that mapping in a relational database and join at the application layer — InfluxDB has no cross-measurement joins anyway.
A worked example: HTTP request metrics
Say you're recording API latency. A schema that will hurt you:
http_requests,endpoint=/api/checkout,request_id=9f3a...,user_id=84721 latency_ms=132,status=200 1760227200000000000Both request_id and user_id are effectively unbounded. Every request creates a brand-new series. A healthier version:
http_requests,endpoint=/api/checkout,region=eu-west,status_class=2xx latency_ms=132,user_id=84721 1760227200000000000Here the tags are all low-cardinality dimensions you'd actually group dashboards by. The user ID survives as a field — you can't filter on it efficiently, but you shouldn't be doing per-user lookups in a time-series store in the first place. Note that endpoint is only safe if your routes are templated (/api/users/:id), not raw paths — raw paths with embedded IDs are a classic accidental-cardinality bomb.
Check cardinality before production, not after
Schema mistakes are expensive to retrofit: changing a tag to a field means rewriting data, and dropping a high-cardinality tag from existing data is a heavy operation. Verify on a staging dataset first.
On InfluxDB 1.x, you can estimate cardinality with:
SHOW SERIES CARDINALITY ON mydbRun this with the influx CLI against a staging instance — be aware this query is itself expensive on a large dataset, so don't run it casually against production. For 1.x TSI indexes on disk, influxd inspect report-tsi (run as a user with read access to the data directory, typically with the server stopped or against a snapshot) gives per-measurement cardinality breakdowns. On 2.x, use influx server-config and bucket-level inspection, or the Flux influxdb.cardinality() function against a test bucket.
The most honest test: write a representative workload at realistic volume and watch the influxd process RSS as unique tag combinations grow. If memory climbs linearly with request count rather than leveling off, you have an unbounded tag.
The trade-off, and the version caveat
Moving a dimension from tag to field isn't free: you lose the ability to filter and group by it cheaply. Sometimes the right answer is to accept a bounded-but-large tag set (say, 5,000 known sensor IDs) because the query pattern demands it — that's a legitimate engineering trade, as long as you've measured the memory cost.
Also, this advice is version-sensitive. InfluxDB 1.x and 2.x share the TSI model where cardinality is the hard wall. InfluxDB 3.x is a different storage architecture (Apache Arrow/Parquet on object storage) with substantially relaxed cardinality behavior, so if you're on 3.x, verify current documentation before applying 1.x-era rules.
What to do Monday morning
Audit your existing measurements: list every tag, and next to each write down whether you group or filter by it and whether its value set is bounded. Any tag that fails either test is a candidate to demote to a field in your next schema revision. Then pair the cleanup with downsampling — retention policies with continuous queries (1.x) or tasks (2.x) — so raw high-resolution data expires quickly while aggregates live long-term. Good schema hygiene plus aggressive retention is what keeps influxd small enough to forget about.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.