The Counterintuitive Truth Nobody Warns You About
There's a moment every engineer experiences when they first set up Elasticsearch. The cluster is humming, all shards are allocated, the health dashboard is glowing a satisfying green, and queries are flying back in under 10 milliseconds. It feels like you've built something bulletproof. Then, weeks or months later, you check the same dashboard — still green, still healthy — but your search latency has quietly ballooned to 400ms, 800ms, sometimes over a second. The index is healthier than ever by every visible metric, and yet something is clearly, painfully wrong.
I've found this phenomenon catches even seasoned engineers off guard, because it violates every intuition we have about systems. Green means go. Healthy means fast. More data means... slower? That last one we sort of expect, but the mechanism behind why is where most explanations fall flat. Understanding the actual culprit requires peeking under the hood of how Elasticsearch manages and merges data over time — and once you see it, you can't unsee it.
The trap isn't a bug. It's a feature. A very necessary feature that, left unchecked, gradually turns your lightning-fast search engine into something that feels like it's reading data off a floppy disk.
Segments: The Hidden Architecture Beneath Your Index
To understand the problem, you need to understand segments. When Elasticsearch (built on top of Apache Lucene) writes data to an index, it doesn't update a single big file. Instead, it writes data into small, immutable files called segments. Think of segments like individual sticky notes. Each time you index a document, Elasticsearch eventually commits it to a new segment — a tiny, self-contained snapshot of a portion of your data.
These segments are immutable, meaning once written, they cannot be changed. If you update a document, Elasticsearch doesn't rewrite the original segment. It marks the old document as deleted and writes the new version into a fresh segment. Deletions aren't immediate either — they're tracked in a separate file called a tombstone until the segment gets cleaned up. This design is deliberate and brilliant for write performance, but it has a cost that only materializes over time.
When you run a search query, Elasticsearch has to search every single segment in the relevant shards and then merge those results together. Early in your index's life, you might have 5 or 10 segments. Queries are fast because there's little to consolidate. But after months of continuous indexing and updates, a single shard can accumulate hundreds of segments. Every query is now orchestrating a mini-merge operation across all of them, and that overhead adds up fast.
The Merge Process: Your Index's Metabolism
Elasticsearch isn't oblivious to the segment accumulation problem. It runs a background process that periodically merges smaller segments into larger ones, reducing the total segment count and purging documents that have been marked for deletion. This process is called segment merging, and it's essentially the metabolism of your index — keeping things clean and consolidated.
Here's where the "more green means more lag" trap really springs. As your index grows and becomes more "established" (which most monitoring tools interpret as healthy), the merge policy becomes more conservative. The default merge policy in Lucene is called TieredMergePolicy, and it groups segments into tiers based on size. It tries to merge segments within the same size tier together. The problem is that as your index matures and you accumulate many large segments, the algorithm has fewer opportunities to merge things efficiently. Large segments are expensive to merge, so the scheduler does it less frequently.
The result is a strange paradox. A brand new index with light traffic gets frequent, cheap merges. An older, heavily-used index — the one that looks the healthiest from the outside — can end up in a state of chronic under-merging, sitting on hundreds of segments across each shard. And every query is paying the interest on that debt.
Measuring the Damage
Before you start tuning anything, you need to see the actual segment situation inside your index. The Elasticsearch API gives you a direct window into this. Here's how I typically check segment counts across shards:
GET /your-index-name/_segments?prettyThat will return a verbose payload, but the key thing to look for is the num_committed_segments and num_search_segments values per shard. A healthy, well-merged shard might have 1 to 5 segments. A troubled shard might show 50, 100, or more.
You can also use the _stats API to get a broader picture:
GET /your-index-name/_stats/segments?prettyThis will show you aggregate counts including the number of deleted documents still lurking inside segments, which is another hidden tax on query performance. If you see a shard with 30% of its documents marked as deleted, that's wasted space and wasted query time — those tombstones are still being scanned during searches.
I've seen production indices where a shard had 40,000 deleted documents out of 120,000 total — a full third of the data being dead weight. The cluster health was green. The ops team had no idea.
The Refresh Interval Problem
Alongside segment accumulation, there's another mechanism that accelerates the slowdown: the refresh interval. By default, Elasticsearch refreshes every second, which means it commits buffered writes to new, searchable segments every 1000 milliseconds. This is what makes Elasticsearch "near real-time" — new documents become searchable within about a second of being indexed.
The catch is that each refresh can create a new segment. If you're indexing at any meaningful volume — say, hundreds of documents per second — that's hundreds of segment creation events per minute. Even with merging running in the background, the production rate of new segments can outpace the merge process, especially under write-heavy loads.
In my experience, most teams don't realize they can tune the refresh interval based on their actual latency requirements. If your use case can tolerate 30-second-old data instead of 1-second-old data, you can dramatically slow down segment accumulation. Here's how to adjust it dynamically without reindexing:
import requests
import json
index_name = "your-index-name"
es_host = "http://localhost:9200"
settings_payload = {
"index": {
"refresh_interval": "30s"
}
}
response = requests.put(
f"{es_host}/{index_name}/_settings",
headers={"Content-Type": "application/json"},
data=json.dumps(settings_payload)
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}")This single change can have a dramatic effect on both write performance and long-term segment health. Fewer refreshes mean fewer segments created per unit of time, which means the merge process has an easier job keeping up.
Force Merging: The Nuclear Option (Use With Care)
If your index is read-heavy or you're dealing with a historical index that no longer receives writes, you have access to a powerful but aggressive tool: the force merge API. This tells Elasticsearch to actively merge segments down to a target number, regardless of what the background scheduler thinks is appropriate.
// Using the Elasticsearch JavaScript client
const { Client } = require('@elastic/elasticsearch');
const client = new Client({ node: 'http://localhost:9200' });
async function forcemergeIndex(indexName, maxSegments = 1) {
try {
console.log(`Starting force merge on ${indexName} to ${maxSegments} segment(s)...`);
const response = await client.indices.forcemerge({
index: indexName,
max_num_segments: maxSegments,
flush: true
});
console.log('Force merge complete:', response);
} catch (error) {
console.error('Force merge failed:', error);
}
}
forcemergeIndex('your-historical-index', 1);The max_num_segments: 1 setting is the most aggressive configuration — it collapses the entire shard down to a single segment, which produces the fastest possible query performance for read-only data. I've seen this cut query times by 60-70% on stale indices that had accumulated over 200 segments per shard.
However — and this is a serious warning — never run a force merge on an index that's still receiving writes. The merge process is CPU and I/O intensive, and if new documents keep arriving during the merge, you'll end up creating new segments immediately after the merge completes, defeating the purpose entirely. Worse, the resource consumption during the operation can cause visible query degradation on other indices sharing the same cluster. Schedule it during off-hours, watch your cluster CPU closely, and make sure your circuit breakers are appropriately configured.
The Shard Size Equation
There's another layer to this problem that's easy to miss: shard sizing. When you first create an index, you set a number of primary shards, and that number is fixed for the life of the index (unless you use features like shrink or split APIs). Most tutorials suggest a handful of shards to start, which seems fine when your index is 5GB but becomes deeply problematic when it grows to 200GB.
Elasticsearch's general recommendation is to keep individual shards between 10GB and 50GB. Shards outside this range cause problems in opposite directions. Shards that are too small multiply the coordination overhead — the node has to manage too many tiny units. Shards that are too large make recovery slow and segment merges enormously expensive, because a single merge operation might need to process tens of gigabytes of data on one thread.
The connection back to our main topic is direct. An oversized shard accumulates segments faster in absolute terms, takes longer to merge them, and ends up holding more un-merged segments at any given moment. Your cluster health stays green — all shards are allocated, all nodes are responsive — but the query engine is working overtime on every single search request. The health dashboard was never designed to tell you about segment pressure. It only knows about allocation status and node availability.
Rethinking What "Healthy" Actually Means
The real lesson here is that cluster health and query performance are two separate things that only loosely correlate. A green Elasticsearch cluster is like a building where all the lights are on and all the doors open — it tells you the basic infrastructure is working, but it says nothing about whether the plumbing is clogged.
Building a meaningful performance monitoring strategy means going beyond the cluster health endpoint. You should be tracking search.query_time_in_millis and search.query_total from the _nodes/stats API, computing rolling averages of query latency per index, and alerting on segment count thresholds — not just on shard allocation failures. Tools like Elastic's own Kibana monitoring, or external solutions like Grafana paired with an Elasticsearch exporter for Prometheus, can expose these metrics and give you early warning before the slowdown becomes a user-facing incident.
The index that needs the most attention is often the one that looks the most stable. A new index is volatile and gets watched carefully. An old, established index fades into the background of your operational consciousness, chugging along in comfortable green status while slowly accumulating the internal debt that will eventually make your engineers scramble at 2am trying to figure out why search is suddenly slow. The answer, almost always, will have been building for months. Now you'll know where to look.