Scaling Prometheus Storage with Grafana Mimir

Photo by Murphy Karen, U.S. Fish and Wildlife Service via Wikimedia Commons (Public domain)
Grafana Mimir is an open-source, horizontally scalable long-term storage backend for Prometheus metrics. It receives data over the Prometheus remote_write protocol, stores it as TSDB blocks in object storage such as S3, GCS, or Azure Blob, and serves PromQL queries. It also adds multi-tenancy and high availability that a single Prometheus lacks.
Add a remote_write block to prometheus.yml pointing at Mimir's push endpoint at /api/v1/push. You must include an X-Scope-OrgID header naming the tenant, because Mimir rejects writes without one. Prometheus then streams samples asynchronously from its write-ahead log while still scraping and serving local queries.
No. remote_write is additive — Prometheus keeps its local TSDB and forwards a copy of each sample to the remote endpoint. Keep several hours or days of local retention so you have a fast fallback for recent data and a buffer that replays if Mimir is temporarily unreachable.
Every write and read request carries an X-Scope-OrgID header identifying the tenant. Mimir stores each tenant's blocks separately in object storage and enforces per-tenant limits like ingestion rate, max series, and retention period. This lets one cluster serve several teams or projects without their data or resource usage colliding.
Usually the compactor is not running or there is no query cache. The compactor merges thousands of small two-hour blocks into larger ones, which is the biggest lever on read latency. Adding a results cache to the query-frontend and store-gateway, and watching for runaway series cardinality, resolves most slowdowns.

Photo by Murphy Karen, U.S. Fish and Wildlife Service via Wikimedia Commons (Public domain)
Key Takeaway
Prometheus was never built to hold years of metrics. Point its remote_write at Grafana Mimir and samples stream into horizontally scalable blocks storage backed by object storage like S3. Mimir adds multi-tenancy via the X-Scope-OrgID header and keeps PromQL queries fast through sharded store-gateways and a compactor.
A single Prometheus is a wonderful thing until you ask it to remember. Local TSDB retention is measured in days or weeks, scaling means a bigger box, and once you run more than one Prometheus you have no single place to query everything. On a monitoring stack I run, I hit all three walls at once: the disk filled, the queries slowed, and I had three Prometheus servers that could not see each other's data.
Grafana Mimir is the answer I settled on. It is an open-source, horizontally scalable long-term store that speaks the Prometheus remote_write protocol on the write side and PromQL on the read side. Prometheus keeps doing what it is good at (scraping and short-term local queries) and forwards every sample to Mimir, which persists it to object storage. This post is how the pieces fit and the configuration I actually use.
Scaling Prometheus vertically hits a hard ceiling: the TSDB is single-node, and there is no built-in replication or clustering. remote_write breaks that ceiling by decoupling ingestion from storage. Prometheus buffers samples in a write-ahead log and streams them asynchronously to a remote endpoint, so it keeps scraping and serving local queries even while forwarding. If the remote endpoint is briefly down, the WAL absorbs the backlog and replays it. That single design choice is what lets you fan many Prometheus servers into one central, replicated store.
Do not delete local retention when you adopt remote_write. Keep a few hours to a few days on the Prometheus box: it gives you a fast local fallback for recent data and a buffer if Mimir is unreachable. remote_write is additive, not a replacement for the local TSDB.
On the Prometheus side you add a remote_write block pointing at Mimir's push endpoint. The path is /api/v1/push, and the one thing people miss is the X-Scope-OrgID header — Mimir refuses writes that do not name a tenant. Here is the block I use, with sane queue tuning for a busy scrape config:
# prometheus.yml
remote_write:
- url: http://mimir:8080/api/v1/push
headers:
X-Scope-OrgID: team-platform # the tenant ID — required by Mimir
queue_config:
capacity: 10000 # samples buffered per shard
max_shards: 50 # upper bound on parallel senders
min_shards: 1
max_samples_per_send: 2000
batch_send_deadline: 5s
metadata_config:
send: true # forward metric metadata (help/type)Mimir does not authenticate on its own — the docs are explicit that auth is expected to sit in front of it, typically an nginx or the mimir gateway that validates a token and injects the X-Scope-OrgID header. In a trusted internal network you can run with the header set directly, but never expose the raw push endpoint to the internet.
Understanding Mimir's write path is what makes the durability guarantees make sense. Mimir is a set of horizontally scalable microservices, each doing one job, and a sample flows through them in order:
Ingesters hold up to two hours of un-flushed data on local disk. If you run replication factor 1 to save memory and an ingester dies before it uploads its block, that window is gone permanently. Keep the default replication factor of three in any environment you care about.
The X-Scope-OrgID header is not a formality — it is Mimir's tenancy boundary. Every tenant gets its own set of TSDB blocks in object storage, its own series, and its own configurable limits. This is how I run one Mimir cluster for several projects without their metrics colliding or one noisy team starving the others. Limits are set per tenant and enforced at ingestion:
# mimir runtime overrides — per-tenant limits
overrides:
team-platform:
ingestion_rate: 150000 # samples/sec accepted
ingestion_burst_size: 1500000
max_global_series_per_user: 1500000
compactor_blocks_retention_period: 1y
team-billing:
ingestion_rate: 25000
max_global_series_per_user: 200000
compactor_blocks_retention_period: 90dNotice retention is per tenant too. The billing team's data expires after ninety days; the platform team keeps a full year. On the query side, Grafana passes the same X-Scope-OrgID so a datasource only ever sees its own tenant. If you need to read across tenants you can enable multi-tenant queries by sending a pipe-separated list of tenant IDs, but I keep that off by default to preserve isolation.
Object storage is cheap and durable but slow per request, so a naive design would make a one-year PromQL range query crawl. Mimir avoids this on several fronts. The query-frontend splits a wide time range into smaller parallel sub-queries and caches results, so re-running yesterday's dashboard is nearly free. The store-gateway shards blocks across instances and caches the block index and chunks, cutting the number of object-storage round trips. And the compactor's whole job is to turn thousands of tiny blocks into a handful of large ones, which is the single biggest lever on read latency.
For a small setup you do not need the full microservices deployment. Mimir ships a monolithic mode that runs every component in one binary with a single config file — perfect for a single VPS with S3-compatible object storage behind it. Start there, split into microservices only when one component becomes the bottleneck.
Be honest about scale. If you run one Prometheus and need thirty days of history, just bump local retention or bolt on Thanos sidecar — Mimir's operational surface (object storage, hash rings, several stateful components) is not free. I reach for Mimir when there is more than one Prometheus to unify, when retention needs to span quarters or years, or when separate teams need isolated, rate-limited tenancy on shared infrastructure. Below that bar, the simpler tool wins.