Fixing Prometheus Node Exporter High Memory Usage on a 2GB VPS

Running Prometheus, Node Exporter, and Grafana on a 2GB VPS sounds reasonable until you watch free memory crater from 800MB to under 100MB within a few hours of standing up the stack. That’s not a rare edge case. It’s a predictable failure mode that hits almost every engineer who deploys this setup without touching the defaults first.
The default Prometheus configuration wasn’t designed for memory-constrained environments. It was designed to be capable. Those are very different goals.
Key Takeaways
- Default Prometheus retention and scrape settings can consume 500MB–1.5GB of memory on a 2GB VPS within hours of deployment, leaving dangerously little headroom for the OS and application workloads.
- Node Exporter exposes over 1,000 metrics per scrape by default in 2026; filtering collectors to only what you need cuts memory pressure by 30–60% depending on your use case.
- Docker Compose resource limits (
mem_limit) are the fastest single change to protect a host from OOM-kill cascades on constrained infrastructure.- Prometheus’s
--storage.tsdb.retention.timeand--storage.tsdb.retention.sizeflags are the two most impactful knobs for controlling memory growth over time.- Engineers running this stack on $6–$12/month VPS instances (DigitalOcean, Hetzner, Vultr) consistently report stable operation after tuning — without upgrading their plan.
The Setup That Seems Fine Until It Isn’t
Prometheus + Node Exporter + Grafana via Docker Compose became the de facto self-hosted monitoring stack somewhere around 2021–2022. Grafana’s own documentation ships a reference docker-compose.yml that gets you from zero to dashboards in under 10 minutes. Last9 and Spacelift both maintain detailed guides covering the full setup. It works beautifully on a developer laptop with 16GB of RAM.
On a 2GB VPS — the tier running a huge share of indie projects, internal tools, and bootstrapped SaaS products — the math gets uncomfortable fast.
Prometheus stores time series data in memory before flushing to disk. The TSDB keeps a “head block” of roughly two hours of data in RAM at all times. According to Prometheus’s official documentation, each time series uses approximately 1–2 bytes per sample in memory-mapped files, but the head block overhead per series runs significantly higher — around 300–500 bytes per active series for metadata and indexing structures. Node Exporter’s default configuration exposes somewhere between 800 and 1,200 active metrics depending on the host. Multiply that across a 15-second scrape interval and you can see exactly why a 2GB box gets squeezed.
Docker adds its own overhead on top. Each container carries its own process, network namespace, and runtime memory cost. Running three containers — Prometheus, Node Exporter, Grafana — on a 2GB host leaves less than 1GB for the actual application you’re monitoring. That’s before counting the OS, SSH daemon, and anything else on the machine.
Node Exporter’s Default Collector Set Is Excessive for Most Use Cases
Node Exporter ships with collectors for CPU, memory, disk, network, filesystem, systemd, NFS, IPVS, and dozens more. The full list in 2026 sits at over 40 enabled collectors by default. For a typical VPS running a web app, you realistically care about 6–8 of those.
Every enabled collector scrapes metrics on every Prometheus pull. Disabling collectors you don’t need isn’t just a memory fix — it cuts CPU cycles per scrape and reduces the cardinality of your time series data in TSDB.
The fix in your docker-compose.yml:
node-exporter:
image: prom/node-exporter:latest
command:
- '--collector.disable-defaults'
- '--collector.cpu'
- '--collector.meminfo'
- '--collector.filesystem'
- '--collector.netdev'
- '--collector.diskstats'
- '--collector.loadavg'
- '--collector.uname'
This disables the full default set and explicitly enables only what matters. Engineers who’ve applied this report metric counts dropping from roughly 1,100 to 180–250 per scrape. That’s a significant reduction in active series Prometheus needs to track in its head block.
This approach can fail, though. If you’re running a workload that genuinely depends on systemd unit states or NFS metrics — and you disable those collectors — you’ll have gaps in exactly the data you need during an incident. Audit what you actually query in Grafana before stripping collectors. The goal is precision, not minimalism for its own sake.
Prometheus Retention Settings Are the Biggest Memory Lever
Prometheus’s default retention is 15 days. On a 2GB VPS, that’s a problem. The TSDB compresses older data to disk efficiently, but the WAL and head block still sit in RAM. Without explicit limits, Prometheus will grow its memory footprint indefinitely as series count increases.
Two flags control this:
prometheus:
image: prom/prometheus:latest
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=7d'
- '--storage.tsdb.retention.size=512MB'
- '--storage.tsdb.wal-compression=true'
--storage.tsdb.retention.time=7d cuts your retention window in half from the default. --storage.tsdb.retention.size=512MB adds a hard disk cap that also influences how aggressively Prometheus drops old data. WAL compression reduces the write-ahead log size by 30–50% according to Prometheus release notes from v2.11 onward. Together, these three flags typically bring Prometheus memory usage from 400–800MB down to 150–300MB on a lightly instrumented host.
The tradeoff is real: seven days of retention means you can’t investigate a two-week-old incident locally. For most debugging scenarios on a small VPS, that’s an acceptable constraint. If your team regularly digs into older data, this isn’t the right configuration — and this stack probably isn’t the right architecture either.
Docker Compose Resource Limits Prevent OOM Cascades
Without explicit memory limits, Docker lets each container claim as much RAM as the host provides. When Prometheus or Grafana spikes — after a restart, after ingesting a burst of metrics — it can starve the host OS. Linux’s OOM killer then starts terminating processes, often in unpredictable order.
Setting hard limits in docker-compose.yml:
services:
prometheus:
mem_limit: 512m
memswap_limit: 512m
grafana:
mem_limit: 256m
memswap_limit: 256m
node-exporter:
mem_limit: 128m
memswap_limit: 128m
memswap_limit equal to mem_limit disables swap for that container, preventing latency spikes from disk-backed memory. These limits force each service to stay within its lane. If Prometheus tries to exceed 512MB, it OOM-kills itself rather than taking down your application.
Default vs. Tuned: What the Numbers Actually Look Like
| Metric | Default Setup | Tuned Setup |
|---|---|---|
| Prometheus memory (steady state) | 450–900MB | 150–300MB |
| Node Exporter series count | 800–1,200 | 180–250 |
| Grafana memory | 200–400MB | 100–180MB |
| Total stack memory | 900–1,600MB | 400–650MB |
| Free RAM for application | <400MB | 1,000–1,300MB |
| Risk of OOM on traffic spike | High | Low |
| Data retention | 15 days | 7 days |
| Scrape interval | 15s | 15s (unchanged) |
The tuned setup doesn’t sacrifice observability in any meaningful way for a 2GB VPS workload. Seven days of retention covers the vast majority of debugging windows. The reduced series count still captures everything that matters operationally.
Three Real Scenarios Where This Plays Out Differently
Scenario 1 — Bootstrapped SaaS on Hetzner CX21 (2GB, €3.79/month) The application is a Node.js API with PostgreSQL. Monitoring is secondary. Apply collector filtering and retention limits as described above. Target: keep the full stack under 600MB so the application has 1.2GB of breathing room. Increasing scrape interval to 30 seconds cuts write load further without losing meaningful resolution.
Scenario 2 — Internal tool monitored across three VPS instances Remote-write from each host’s Prometheus to a central aggregator isn’t viable at this budget. Instead, run Node Exporter only on each host — no local Prometheus — and have a single slightly larger instance (4GB) scrape all three. This eliminates per-host TSDB overhead entirely. It’s the most aggressive memory reduction possible in this architecture.
Scenario 3 — Grafana is eating unexpected memory Grafana’s memory footprint is often the surprise. It caches dashboard queries in RAM. Twenty-plus panels refreshing every 30 seconds can push Grafana alone to 400MB. The fix: limit auto-refreshing dashboards, or remove Grafana from the VPS entirely. Grafana Cloud’s free tier handles 10,000 series — more than enough for a tuned stack — and it shifts the memory burden off your host completely.
What Stability Looks Like After Tuning
The changes above aren’t complex. They’re four additions to a docker-compose.yml and a collector flag list. Engineers who apply the full treatment — collector filtering, retention limits, WAL compression, and memory caps — consistently move from volatile to stable without touching their infrastructure budget.
What to expect after redeployment:
- Stack memory drops 50–65% within the first 24 hours
- Node Exporter series count falls 75–80% with aggressive collector filtering
- Prometheus self-terminates gracefully on spike instead of OOM-killing the host
- 7-day retention covers 95%+ of production debugging scenarios
Worth watching in the next 6–12 months: Prometheus 3.x ships a new TSDB engine with improved memory efficiency for low-cardinality workloads. Early benchmarks from the Prometheus GitHub discussions suggest a 20–30% reduction in head block memory for series counts under 500 — directly relevant to tuned small-VPS deployments. The current tuning numbers may shift once the 3.x stable release lands.
A 2GB VPS can run this stack reliably. It just can’t run it with defaults.
What’s your current scrape interval and series count on a constrained host? Those two numbers together tell you more about your memory ceiling than any single config flag.
References
- Monitoring a Linux host with Prometheus, Node Exporter, and Docker Compose | Grafana Cloud documenta
- Prometheus with Docker Compose: The Complete Setup Guide | Last9
- Prometheus with Docker Compose: Guide & Examples
Photo by Favour Usifo on Unsplash


