Elasticsearch JVM Heap Sizing on a Small VPS: Heap, Page Cache and the Rest of the Budget

Elasticsearch is the classic way to turn a $10/month VPS into a $30/month VPS, not because the software is heavy but because of one configuration line: -Xmx. The default templates assume a machine with 8–16 GB to spare, and on a 2 GB box the JVM heap, the filesystem page cache, the segment merge buffers and the kernel all compete for the same pages. Get the split wrong and you get either an OOM kill at 3 a.m. or a cluster that sits idle while the disk thrashes. This is a sizing guide for small boxes, with the RAM you actually have to rent as the cost variable — see current small VPS RAM pricing before you pick a plan.

The guidance below applies to Elasticsearch 8.x on a single node with no replicas, which is the realistic shape of a small deployment: a search box for a blog, a log viewer for a handful of servers, or an internal document index.

Why the 50% heap rule is only half the rule

The long-standing advice is “set heap to 50% of RAM, leave the rest to the filesystem”. That guidance comes from production clusters with tens of gigabytes, where 50% still leaves double-digit gigabytes of page cache. On a 2 GB box, 50% heap gives 1 GB to the OS, which has to hold the kernel, the JVM’s own non-heap memory, and every segment file Elasticsearch reads.

Two mechanisms make that painful. First, Lucene reads segment files through mmap; if the page cache is starved, every search becomes a disk read. Second, the JVM reserves memory beyond the heap: metaspace, thread stacks, direct buffers, plus roughly 0.5–1 GB of JVM overhead once heap exceeds 4 GB. Heap is not the process footprint.

Heap size Approx. RSS overhead beyond heap Typical total process RSS
512 MB 250–350 MB 0.8–0.9 GB
1 GB 350–500 MB 1.4–1.6 GB
2 GB 500–700 MB 2.5–2.8 GB
4 GB 700–1000 MB 4.8–5.2 GB

That table is why a “2 GB heap on a 4 GB VPS” configuration, copied straight from a tutorial, leaves almost nothing for page cache and gets the node killed under a merge.

Concrete heap values for 2, 4 and 8 GB boxes

These targets assume Elasticsearch plus the OS, with no other heavy service on the box. If you also run Kibana, add 0.5–1 GB or move Kibana to another host.

VPS RAM Heap (-Xmx / -Xms) Free for OS + page cache Realistic index size Verdict
2 GB 512 m ~1.1 GB 1–3 GB of index Works for a small log or doc index; no Kibana
2 GB 768 m ~0.8 GB 0.5–1.5 GB Tight; acceptable if index fits cache
4 GB 1 g ~2.5 GB 3–8 GB The sweet spot for one node + Kibana
4 GB 2 g ~1.5 GB 2–4 GB Only if queries are aggregation-heavy
8 GB 2–3 g ~5 GB 10–25 GB Comfortable single node

Set -Xms equal to -Xmx. A growing heap causes pauses, and the reserved-and-touched memory is the same either way; letting the JVM resize just adds GC churn. Never exceed 31 GB even on large boxes — compressed oops and zero-based pointers stop working above that, and pointer compression is lost, which is a different article but the same principle: bigger heap is not automatically faster.

The settings file that reflects the table

On a Debian or Ubuntu package install, the heap lives in /etc/elasticsearch/jvm.options.d/heap.options, not in the main file — editing jvm.options directly gets overwritten on upgrade.

# /etc/elasticsearch/jvm.options.d/heap.options
-Xms1g
-Xmx1g

Then set the same node to be single-purpose and honest about its memory:

# /etc/elasticsearch/elasticsearch.yml
node.name: node-1
discovery.type: single-node
bootstrap.memory_lock: true
indices.memory.index_buffer_size: 10%
indices.queries.cache.size: 5%

bootstrap.memory_lock stops the kernel swapping the heap, which is the right trade on a box with adequate RAM and the wrong one on a box where something else is competing. Locking memory on a 2 GB VPS with 768 m of heap plus Kibana will trigger the OOM killer rather than swap, and an OOM kill takes the whole node down. On 2 GB, leave memory locking off and budget swap instead.

Swap and OOM behaviour on a small box

Swap does not save Elasticsearch performance, but it does prevent the kernel from killing the process during a transient merge spike. A 1–2 GB swapfile with vm.swappiness=1 is a cheap insurance policy; the goal is that swap is almost never used, not that the node runs from it.

fallocate -l 2G /swapfile && chmod 600 /swapfile
mkswap /swapfile && swapon /swapfile
echo 'vm.swappiness=1' >> /etc/sysctl.conf
sysctl -p

Understand the failure modes. If the kernel OOM killer fires, it picks by oom_score, and the JVM with the largest RSS usually loses — you get a dead node and a 30-second gap in your dashboard. If Elasticsearch’s own circuit breaker trips instead, you get a CircuitBreakingException in the logs and a 429 to the client, which is far more graceful. Configure the breaker limits rather than letting the kernel decide.

Measuring fielddata and segment memory before you add RAM

Most “we need more RAM” conclusions are actually fielddata bloat from a keyword field being sorted or aggregated. Check the node stats and the hot indices before upgrading the plan.

curl -s localhost:9200/_nodes/stats/jvm,indices/segments?pretty \
  | jq '.nodes[].jvm.mem, .nodes[].indices.segments.memory'
curl -s 'localhost:9200/_cat/fielddata?v&bytes=g'
curl -s 'localhost:9200/_cat/segments?v&bytes=g' | sort -k3 -h | tail -20

If fielddata is over 10–15% of your heap, the fix is mapping, not money: make the field doc_values-based (the default for keyword), stop sorting on text fields, and avoid high-cardinality terms aggregations on analysed strings. Segment memory tells you how much of the index is mmap’d and therefore how much page cache you want to keep free — if segment memory approaches the RAM you have outside the heap, your search latency is disk-bound and a heap increase will not help.

Reducing merge pressure before renting more RAM

Segment merges are the hidden RAM consumer on a small node. Each merge buffers segments in memory and rewrites them, so a bulk reindex can double the working set for a few minutes. On a 2 GB box that is precisely when the OOM killer arrives. Three settings reduce the peak:

  • index.refresh_interval: 30s instead of 1 s halves the number of small segments; for a log index 60s is fine.
  • index.transaction_log.durability: async for non-critical indices reduces fsync cost, at the price of losing the last few seconds on a crash.
  • Bulk-load with a single large index and index.number_of_replicas: 0, then let the merge settle before you point traffic at it.

These are free. They buy you a tier of headroom, and on a budget box that is worth more than the $3/month difference between a 2 GB and a 4 GB plan spent on the wrong tier.

Turning the sizing into a purchasing decision

Work backwards from the index size. A rule of thumb that holds surprisingly well on small indices: keep total index size under about 25–30% of the box’s RAM, so the hot segments stay in page cache. For a 4 GB box that means 1–1.2 GB of index, which feels small until you remember that most log indices are 80% vacuum if you are not using index.codec: best_compression and a sensible refresh interval.

  • Index under 1 GB, single user: 2 GB VPS with a 512 m heap is enough, and the cheapest answer.
  • Index 1–5 GB, dashboard queries: 4 GB VPS, 1 g heap, second IP not needed.
  • Index 5–20 GB: 8 GB VPS, 2 g heap, and expect to spend the difference on snapshot storage.
  • Index over 20 GB on one node: buy RAM only if your queries are dominated by aggregations; otherwise compress, roll over, and delete old indices instead of renting more.

Compare the annual delta between tiers before buying: moving from 2 GB to 4 GB typically costs $2–4/month annually, which is far less than the engineering time spent tuning a box that is too small. The budget VPS plans with 4 GB and above are where single-node Elasticsearch starts to be comfortable rather than merely possible.

Affordable-Vps-Server-Author
Affordable-Vps-Server-Author
Articles: 392

Leave a Reply