Prometheus vs VictoriaMetrics vs Grafana Mimir: Metrics Storage 2026
TL;DR
Prometheus is the industry standard pull-based metrics system — it scrapes metrics from your services and stores them locally. But Prometheus has limits: 15-day default retention, single-node architecture, and no built-in long-term storage. VictoriaMetrics solves this — a Prometheus-compatible time series database that compresses data 10x better, scales to billions of data points, and runs as a single binary with much lower resource usage. Grafana Mimir takes the distributed path — a horizontally scalable, multi-tenant Prometheus that stores data in object storage (S3, GCS) for unlimited long-term retention. For single-server self-hosted: Prometheus + VictoriaMetrics as long-term storage. For high-scale multi-tenant monitoring: Grafana Mimir. For getting started with minimal complexity: Prometheus alone.
Key Takeaways
- Prometheus default retention: 15 days — longer requires either remote write or more disk
- VictoriaMetrics uses 10x less disk than Prometheus for the same data (better compression)
- All three are PromQL compatible — same dashboards and alerting rules work unchanged
- Grafana Mimir requires object storage — S3 or GCS, no local disk for long-term data
- VictoriaMetrics single binary handles ingestion + storage + query — simpler than Mimir's microservices
- prom-client for Node.js works with all three — it exposes metrics in Prometheus format
- Grafana visualizes all three — the same Grafana dashboards work regardless of backend
The Metrics Stack Architecture
Node.js App
└── prom-client (exposes /metrics endpoint)
│
▼
Metrics Scraper (pulls every 15 seconds)
├── Prometheus (stores locally, 15-day default retention)
├── VictoriaMetrics (stores locally, excellent compression, long-term)
└── Grafana Agent → Grafana Mimir (stores in S3, unlimited, multi-tenant)
│
▼
Grafana (visualization — queries PromQL from any backend)
Node.js Metrics with prom-client
Before comparing the backends, instrument your Node.js app — all three use the same format:
Installation
npm install prom-client
Express/Fastify Metrics Endpoint
import { Registry, Counter, Histogram, Gauge, collectDefaultMetrics } from "prom-client";
import express from "express";
// Create a registry
const register = new Registry();
// Collect default Node.js metrics (event loop, GC, memory, etc.)
collectDefaultMetrics({ register });
// Custom metrics
const httpRequestDuration = new Histogram({
name: "http_request_duration_seconds",
help: "Duration of HTTP requests in seconds",
labelNames: ["method", "route", "status_code"],
buckets: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
registers: [register],
});
const httpRequestTotal = new Counter({
name: "http_requests_total",
help: "Total number of HTTP requests",
labelNames: ["method", "route", "status_code"],
registers: [register],
});
const activeConnections = new Gauge({
name: "active_connections",
help: "Number of active WebSocket connections",
registers: [register],
});
// Middleware to instrument all routes
const app = express();
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer({
method: req.method,
route: req.route?.path ?? req.path,
});
res.on("finish", () => {
const labels = {
method: req.method,
route: req.route?.path ?? req.path,
status_code: res.statusCode,
};
end(labels);
httpRequestTotal.inc(labels);
});
next();
});
// Metrics endpoint — scraped by Prometheus/VM/Mimir agent
app.get("/metrics", async (req, res) => {
res.set("Content-Type", register.contentType);
res.end(await register.metrics());
});
// Example route with custom business metrics
const ordersCreated = new Counter({
name: "orders_created_total",
help: "Total orders created",
labelNames: ["status", "payment_method"],
registers: [register],
});
app.post("/orders", async (req, res) => {
// ... create order
ordersCreated.inc({ status: "success", payment_method: "stripe" });
res.json({ orderId: "..." });
});
Prometheus: The Pull-Based Standard
Prometheus is a pull-based time series database. It scrapes /metrics endpoints on a schedule and stores the data locally.
Docker Compose Setup
# docker-compose.yml — Prometheus + Grafana
version: "3.8"
services:
prometheus:
image: prom/prometheus:latest
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- "--config.file=/etc/prometheus/prometheus.yml"
- "--storage.tsdb.retention.time=30d" # 30 days retention
- "--storage.tsdb.path=/prometheus"
- "--web.enable-remote-write-receiver" # Accept remote writes
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
environment:
GF_SECURITY_ADMIN_PASSWORD: "admin"
GF_USERS_ALLOW_SIGN_UP: "false"
node-exporter:
image: prom/node-exporter:latest
ports:
- "9100:9100"
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
volumes:
prometheus_data:
grafana_data:
Prometheus Configuration
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
external_labels:
environment: production
# Alerting
alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]
# Load alerting rules
rule_files:
- "alerts/*.yml"
# Scrape configs
scrape_configs:
# Scrape Prometheus itself
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
# Scrape Node.js app
- job_name: "my-app"
static_configs:
- targets: ["app:3000"]
metrics_path: /metrics
scheme: http
# Scrape Node Exporter (system metrics)
- job_name: "node-exporter"
static_configs:
- targets: ["node-exporter:9100"]
# Kubernetes pod discovery (if using K8s)
- job_name: "kubernetes-pods"
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: "true"
Alert Rules
# alerts/nodejs.yml
groups:
- name: nodejs
rules:
- alert: HighErrorRate
expr: |
rate(http_requests_total{status_code=~"5.."}[5m])
/
rate(http_requests_total[5m]) > 0.05
for: 5m
labels:
severity: critical
annotations:
summary: "High error rate: {{ $value | humanizePercentage }}"
- alert: SlowRequests
expr: |
histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m])) > 2
for: 10m
labels:
severity: warning
annotations:
summary: "P99 latency above 2s: {{ $value }}s"
VictoriaMetrics: High-Performance Drop-In
VictoriaMetrics is a time series database compatible with Prometheus. It uses a unique storage format achieving 10x better compression and handling significantly more data on the same hardware.
Single-Node Setup
# docker-compose.yml — VictoriaMetrics replaces Prometheus storage
version: "3.8"
services:
victoriametrics:
image: victoriametrics/victoria-metrics:latest
ports:
- "8428:8428"
volumes:
- vm_data:/victoria-metrics-data
command:
- "--storageDataPath=/victoria-metrics-data"
- "--retentionPeriod=365" # 1 year retention (Prometheus: 15 days default)
- "--httpListenAddr=:8428"
vmagent:
image: victoriametrics/vmagent:latest
ports:
- "8429:8429"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml # Same config!
- vmagent_data:/tmp/vmagent-remotewrite-data
command:
- "--promscrape.config=/etc/prometheus/prometheus.yml"
- "--remoteWrite.url=http://victoriametrics:8428/api/v1/write"
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
volumes:
vm_data:
vmagent_data:
grafana_data:
PromQL Compatibility
# These PromQL queries work identically on Prometheus and VictoriaMetrics
# P99 request latency
histogram_quantile(
0.99,
rate(http_request_duration_seconds_bucket[5m])
)
# Request rate by route
sum(rate(http_requests_total[5m])) by (route)
# Error rate
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
# Active connections
active_connections
# Memory usage
process_resident_memory_bytes / 1024 / 1024 # MB
MetricsQL (VictoriaMetrics Extensions)
# VictoriaMetrics adds MetricsQL extensions to PromQL
# Keep last value for sparse metrics
keep_last_value(some_gauge)
# Share of each time series in the total
sum(rate(http_requests_total[5m])) by (route)
/
ignoring(route) group_left sum(rate(http_requests_total[5m]))
# Running average
running_avg(rate(http_requests_total[5m]))
Grafana Mimir: Distributed Long-Term Metrics
Grafana Mimir stores metrics in object storage (S3, GCS, Azure Blob) — enabling unlimited retention at low cost and horizontal scaling for high-ingestion environments.
Docker Compose (Single-Binary Mode)
# docker-compose.yml — Mimir in single-binary mode (production uses microservices)
version: "3.8"
services:
mimir:
image: grafana/mimir:latest
command:
- "--config.file=/etc/mimir/mimir.yaml"
ports:
- "9009:9009"
volumes:
- ./mimir.yaml:/etc/mimir/mimir.yaml
- mimir_data:/data
grafana-agent:
image: grafana/agent:latest
ports:
- "12345:12345"
volumes:
- ./agent.yaml:/etc/agent/config.yaml
command:
- "--config.file=/etc/agent/config.yaml"
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana_data:/var/lib/grafana
volumes:
mimir_data:
grafana_data:
# mimir.yaml
target: all # Single binary mode
common:
storage:
backend: filesystem # For dev; use s3 in production
filesystem:
dir: /data
blocks_storage:
filesystem:
dir: /data/blocks
alertmanager_storage:
filesystem:
dir: /data/alertmanager
ruler_storage:
filesystem:
dir: /data/rules
memberlist:
join_members:
- "mimir:7946"
limits:
max_global_series_per_user: 1500000
# agent.yaml — Grafana Agent to scrape and send to Mimir
metrics:
global:
scrape_interval: 15s
remote_write:
- url: http://mimir:9009/api/v1/push
headers:
X-Scope-OrgID: demo-tenant # Required for multi-tenancy
configs:
- name: default
scrape_configs:
- job_name: my-app
static_configs:
- targets: ["app:3000"]
Feature Comparison
| Feature | Prometheus | VictoriaMetrics | Grafana Mimir |
|---|---|---|---|
| Architecture | Single-node | Single-node (+ cluster) | Distributed microservices |
| Storage | Local disk | Local disk | Object storage (S3/GCS) |
| Default retention | 15 days | Any (unlimited) | Unlimited |
| Disk efficiency | Baseline | ✅ 10x better | Good (object storage) |
| PromQL compatible | ✅ Native | ✅ + MetricsQL | ✅ |
| Multi-tenancy | ❌ | Partial | ✅ Native |
| High availability | Manual | ✅ Cluster mode | ✅ Built-in |
| Setup complexity | Low | Low | High |
| RAM at 1M series | ~8 GB | ~1 GB | Distributed |
| Alerting | Alertmanager | ✅ + vmalert | ✅ Compatible |
| Remote write | ✅ (send) | ✅ (send/receive) | ✅ (receive) |
| GitHub stars | 55k | 13k | 4k |
Production Cardinality and Label Management
The most common production pitfall across all three backends is cardinality explosion — creating too many unique label combinations that exhaust memory. Prometheus allocates approximately 8GB RAM per million active time series, meaning a label like user_id applied to HTTP request metrics immediately makes the system unscalable. VictoriaMetrics handles cardinality more efficiently with its storage format, maintaining approximately 1GB per million series, but the architectural discipline of avoiding high-cardinality labels remains essential regardless of backend. The recommended pattern is to use labels for dimensions that have bounded cardinality: method, status_code, route, and service are safe; user_id, request_id, and session_token are not. Recording rules in all three systems allow pre-computing expensive queries into new time series with reduced cardinality, which is the correct approach for dashboards querying across millions of events.
Long-Term Retention Strategy and Storage Costs
Retention strategy drives storage architecture decisions. Prometheus's default 15-day retention fits many operational use cases where recent alerting data is the primary concern, but compliance requirements often mandate 90 or 365 days. VictoriaMetrics compresses data so efficiently that a year of metrics for a moderately sized service typically fits on a single 100GB SSD — dramatically cheaper than the equivalent Prometheus storage. Grafana Mimir offloads to S3, where the storage cost per GB is approximately 20x cheaper than EBS or local NVMe, making it the most cost-effective solution for truly long-term retention. However, Mimir's query performance for recent data involves fetching blocks from S3 and caching them locally; VictoriaMetrics accesses local disk directly, giving it lower query latency for typical monitoring workloads. Teams with 12-24 month compliance requirements should seriously evaluate the Mimir or VictoriaMetrics Cluster approach.
High Availability and Clustering Patterns
Single-node Prometheus has no built-in high availability, which is why production setups typically run two identical Prometheus instances scraping the same targets and use Alertmanager deduplication. VictoriaMetrics Cluster mode distributes ingestion across vminsert nodes and storage across vmstorage nodes, with vmselect handling query distribution — a horizontally scalable architecture that handles billions of active series. Grafana Mimir's microservices architecture takes this further, separating the ingester, querier, compactor, store-gateway, and ruler into independently scalable services. For organizations on Kubernetes, the Prometheus Operator and related Helm charts make deploying highly available Prometheus straightforward, while the VictoriaMetrics Operator provides similar Kubernetes-native management for VictoriaMetrics Cluster. The practical rule: if your monitoring infrastructure itself needs to be monitored, you need HA — which points to VictoriaMetrics Cluster or Grafana Mimir.
Security and Multi-Tenant Isolation
Prometheus has no authentication or authorization built in — the standard approach is to put Prometheus behind a reverse proxy with basic auth or mTLS. This is acceptable for single-team infrastructure but breaks down in shared platform environments. Grafana Mimir's native multi-tenancy uses the X-Scope-OrgID header to isolate metrics between tenants, making it the correct choice for platform teams providing monitoring as a service to multiple engineering teams. VictoriaMetrics Enterprise adds tenant-level access control, but the open-source single-node version has no tenant isolation. All three should be deployed in private network segments with firewall rules preventing public access to the metrics endpoints themselves — exposing Prometheus metrics publicly leaks service topology, endpoint names, and performance characteristics that aid attackers in reconnaissance.
Node.js Ecosystem Integration
Beyond prom-client, the Node.js ecosystem has converged on OpenTelemetry as the instrumentation standard in 2026. The @opentelemetry/exporter-prometheus package exports OTel metrics in Prometheus format, meaning you can instrument once with the OTel SDK and ship to any backend. This is particularly valuable for teams running mixed infrastructure — Node.js services alongside Go, Python, or Java services can all share the same instrumentation library while the metrics backend remains interchangeable. The prom-client library remains the most direct path for Node.js-specific Prometheus metrics and requires less setup than the full OTel stack, but for teams building new services in 2026, the OTel approach provides better long-term portability. Fastify users can use @fastify/metrics which wraps prom-client and automatically instruments route handler duration and error rates.
When to Use Each
Choose Prometheus if:
- You're getting started with metrics and want the most documentation and community resources
- Short-term retention (15-30 days) is sufficient for your use case
- Single server, small-to-medium scale (under 1M active series)
- Kubernetes Service Monitor and PodMonitor resources are in your workflow
Choose VictoriaMetrics if:
- You want Prometheus compatibility with dramatically better resource efficiency
- Long-term retention (months or years) without object storage costs
- You're migrating from Prometheus and want a drop-in upgrade
- Single-binary simplicity vs Mimir's microservice complexity is preferred
Choose Grafana Mimir if:
- You need unlimited retention stored in cheap object storage (S3 pricing)
- Multi-tenant metrics isolation is required (SaaS platforms monitoring multiple customers)
- Horizontal scaling to billions of series across multiple ingest nodes is needed
- You're already in the Grafana ecosystem (Loki, Tempo, Grafana Cloud)
Methodology
Data sourced from official Prometheus, VictoriaMetrics, and Grafana Mimir documentation, VictoriaMetrics benchmark blog posts and comparison articles (victoriametrics.com/blog), and community benchmarks from r/devops and the CNCF observability working group. Resource usage figures from VictoriaMetrics' published benchmarks comparing against Prometheus with equivalent data. GitHub star counts as of February 2026.
Related: OpenTelemetry vs Sentry vs Datadog for distributed tracing and error monitoring, or Langfuse vs LangSmith vs Helicone for AI-specific observability.
See also: Lit vs Svelte and PgBouncer vs pgcat vs Supavisor 2026