What this usually means
Slow queries in Elasticsearch almost never point to a single slow shard. Instead, they indicate a systemic bottleneck: too many shards per node causing thread pool exhaustion, garbage collection pauses from oversized segments, or circuit breakers tripping before queries complete. The most common underlying cause is a mismatch between query patterns and index design—overly broad queries hitting many shards, or deeply nested aggregations on unoptimized mappings. The real killer is when query latency becomes non-linear: adding one more concurrent request doubles the response time. That's when you know you've hit a contention limit.
The first ten minutes — establish facts before touching code.
- 1Run `GET /_nodes/hot_threads` on data nodes to identify stuck search threads
- 2Check search thread pool queue and rejections: `GET /_cat/thread_pool/search?v&h=name,active,queue,rejected`
- 3Analyze slow queries via `GET /_nodes/stats/thread_pool?fields=search,search_queue`
- 4Profile the slowest query with `_profile` API: `GET /my-index/_search?profile=true`
- 5Check circuit breaker stats: `GET /_nodes/stats/breaker` for fielddata or request circuit breakers
- 6Review node heap usage: `GET /_cat/nodes?v&h=heap.percent,ram.percent,cpu`
The specific files, logs, configs, and dashboards that usually own this bug.
- searchElasticsearch logs: `/var/log/elasticsearch/*.log` for `slowlog` entries and circuit breaker warnings
- searchHot threads output: `GET /_nodes/hot_threads`
- searchIndex-level settings: `GET /my-index/_settings` for `number_of_shards`, `refresh_interval`
- searchSegment info: `GET /my-index/_segments` for large segments
- searchTask management API: `GET /_tasks?actions=*search*&detailed=true` to see running queries
- searchCluster allocation explain: `GET /_cluster/allocation/explain` for stuck shards
- searchPrometheus/Grafana dashboards with Elasticsearch exporter for node-level JVM and thread pool metrics
Practical causes, not theory. These are the things you will actually find.
- warningToo many shards per node (over-sharding) causing search thread pool contention
- warningHeap pressure from fielddata or aggregations on high-cardinality fields
- warningSlow garbage collection (GC) due to large, unmerged segments
- warningCircuit breaker trips because of oversized request payloads or deep aggregations
- warningInefficient query structure: wildcard prefixes, script scoring, or large bool clauses
- warningHot shards: uneven document distribution across shards
- warningRefresh interval too low causing frequent segment creation and merge storms
Concrete fix directions. Pick the one that matches your root cause.
- buildReduce shard count per index (set `number_of_shards` to nodes * 1-2), reindex with `_split` or `_shrink`
- buildIncrease node heap size (up to 50% of RAM, max 32GB) and adjust GC settings (use G1GC)
- buildForce merge read-only indices to reduce segment count: `POST /my-index/_forcemerge?max_num_segments=1`
- buildTune search thread pool: `thread_pool.search.queue_size` and `thread_pool.search.max` in elasticsearch.yml
- buildRewrite queries: replace wildcard prefixes with `prefix` or `edge_ngram` mapping, use `search_after` for pagination
- buildEnable slow logs and set `index.search.slowlog.threshold.query.warn: 2s` to catch offenders
- buildAdd dedicated coordinating nodes to offload merge and aggregation work
A fix you cannot prove is a guess. Close the loop.
- verifiedRepeat the slow query after changes and confirm latency drops below 500ms
- verifiedMonitor search thread pool active and queue counts returning to zero between requests
- verifiedCheck circuit breaker stats: `GET /_nodes/stats/breaker` show no recent trips
- verifiedRun `_profile` on the same query to verify reduced shard-level time and no long-running phases
- verifiedUse _cat/shards to verify even distribution: `GET /_cat/shards/my-index?v&h=index,shard,prirep,state,node,store`
- verifiedLoad test with same concurrency: query latency should scale linearly, not exponentially
Things that make this bug worse or harder to find.
- warningAdding more nodes without reducing shard count—more nodes with too many shards just spreads contention
- warningBlindly increasing thread pool sizes—this can increase heap pressure and cause OOM
- warningDisabling circuit breakers—they protect your cluster from runaway queries
- warningIgnoring segment merging on read-only indices—huge segments cause high GC pauses
- warningUsing `_all` field or wildcard queries on large indices—these scan all fields
- warningSetting `refresh_interval=-1` without a plan—stale data can cause false positives in query results
The 30-Second Kibana Dashboard
Timeline
- 09:15Kibana dashboard for sales analytics loads in 30 seconds; users report timeouts
- 09:18Check Elasticsearch logs: no errors, but slow logs show queries taking 8-12s
- 09:20Run `GET /_nodes/hot_threads` on data nodes: 15 threads stuck in `search` phase
- 09:22Check search thread pool: `active=25, queue=40, rejected=12`
- 09:25Profile the slow query: aggregate over 10 million documents with date histogram
- 09:30Check index settings: sales index has 50 shards, 3 replicas (150 shards total) on 5 nodes
- 09:35Heap usage at 85% on all data nodes; fielddata circuit breaker is 40% of heap
- 09:40Force merge old monthly indices (read-only) and reduce shards to 10 per index
- 09:50Restart rolling restart to apply new thread pool settings
- 10:00Re-run slow query: now 300ms. Dashboard loads in under 2 seconds.
When the sales team's Kibana dashboard started loading in 30 seconds, I assumed it was a network issue or a failing node. But the cluster was green, and all nodes were healthy. The slow logs told a different story: the main query—a simple date histogram aggregation—was taking 8-12 seconds. That's way too long for a 10-million-document index.
I ran hot threads on all data nodes and saw 15 threads stuck in the search phase per node. The search thread pool had a queue of 40 and 12 rejections. That's when I realized: we had too many shards. The sales index had 50 shards with 3 replicas, so 150 shards across 5 data nodes—30 shards per node. Each query hit all shards, and the thread pool was saturated. The aggregation was also killing heap: fielddata circuit breaker was at 40% of heap because of a high-cardinality field in the bucket.
The fix was two-fold: reduce shard count and force merge old indices. I reindexed the current month index to 10 shards, and for historical months, I used `_forcemerge?max_num_segments=1` on read-only indices. I also bumped the search thread pool max from 13 to 17. After a rolling restart, the same query ran in 300ms. The dashboard loaded in under 2 seconds. The lesson: never let shard count grow unchecked—plan for node count, not data size.
Root cause
Over-sharding (50 shards per index) on a 5-node cluster caused search thread pool saturation and fielddata circuit breaker pressure from high-cardinality aggregation.
The fix
Reduced shard count to 10, force-merged read-only indices, and increased search thread pool size.
The lesson
Shard count must be a function of node count, not data size. Always test query performance with realistic shard distribution before production.
Elasticsearch distributes a query to every shard in the index. If you have 50 shards on 5 nodes, each node handles 30 shard-level queries (including replicas). The search thread pool has a fixed number of threads (default 13 per node). When 30 requests hit simultaneously, 13 run and 17 queue up. Each query waits for a thread, and the total latency becomes queue_time + execution_time. This is the contention spiral: as queue grows, response times become unpredictable and often trigger client timeouts.
The fix is to reduce the number of shards per node. A good rule of thumb is 20-40 shards per GB of heap. For a 30GB heap node, that means 600-1200 shards max across all indices. But for query performance, the active shards per query matters more. Aim for 1-2 shards per node per query. With 5 nodes, that's 5-10 shards total. Reindex with `_split` or `_shrink` to get there.
Circuit breakers prevent out-of-memory errors by aborting requests that exceed a memory threshold. But they can also cause slow queries by forcing retries or dropping requests before they complete. The two most common offenders are the fielddata circuit breaker and the request circuit breaker. Fielddata breaker trips when an aggregation loads too many unique values into memory. Request breaker trips when the serialized query size exceeds 60% of heap (default).
To diagnose, check `GET /_nodes/stats/breaker` and look for `tripped` counters. If you see trips, you need to either reduce the query size (e.g., use `composite` aggregation for high-cardinality buckets) or increase the breaker limit (not recommended). Better to optimize the query: avoid `terms` aggregation on fields with millions of unique values, and use `sampler` or `diversified_sampler` for approximate counts.
Hot threads output gives you a stack trace of threads that have been running for a long time. It's invaluable for identifying where time is spent: parsing, fetching, merging, or GC. But it's a snapshot, not a trend. Use it to see if threads are stuck in `search` phase (query execution) or `fetch` phase (retrieving documents). If many threads are in `search`, the bottleneck is shard-level processing. If in `fetch`, it's network or disk I/O.
I always run hot threads with `interval=5s` and `threads=10` to capture the hottest threads. Then I look for patterns: repeating method calls like `org.elasticsearch.search.aggregations.bucket.terms` suggest aggregation issues. Repeated `org.elasticsearch.index.fielddata` indicate fielddata loading. Use this to guide your next step—profile the query or check circuit breakers.
The default search thread pool has `max=13` and `queue_size=1000`. Increasing `max` can improve throughput but also increases heap pressure because each thread holds its own query context. A common mistake is to set `max` to the number of cores. That can cause OOM if queries are memory-heavy. Instead, increase slowly and monitor heap usage.
A better approach is to reduce the number of concurrent queries by adding a coordinating node layer. Coordinating nodes handle the scatter-gather phase and can queue requests independently, shielding data nodes from client timeouts. Set `number_of_coordinating_only_nodes` to 1-2 per data node. Then tune the data node search thread pool to match the shard count: if each node handles 10 shards per query, you need at least 10 threads, but no more than 20 to leave room for indexing.
Frequently asked questions
Why does my query run fast on a small index but slow on production?
Production indices have more shards, more segments, and more concurrent requests. The slow query is likely hitting shard contention (too many shards per node) or GC pauses from large segments. Profile the query on production with realistic data distribution, not just a single shard test.
Should I increase the search thread pool queue size?
Only if you have enough heap to handle the queued requests. Each queued query holds its context in memory. A large queue can mask a systemic problem. Better to reduce shard count or add coordinating nodes. Monitor rejections: if you see rejections, reduce shards, not increase queue.
How do I find which query is causing the slow log?
Enable search slow logs with `index.search.slowlog.threshold.query.warn: 2s`. The logs include the full query source. Then use `_tasks?actions=*search*&detailed=true` to correlate with active queries. In Kibana, the Monitoring UI shows the slowest queries under 'Top Queries'.
Can I use `_forcemerge` on a live index?
Yes, but it will block writes during the merge. For read-only indices, it's safe. For write indices, schedule during low traffic. Force merge reduces segment count (and heap usage) but creates one large segment that can cause longer GC pauses if not sized correctly. Use `max_num_segments=1` for indices < 50GB, otherwise use 5-10.
What is the best shard size for query performance?
Shards between 20GB and 50GB are optimal for query performance. Smaller shards increase parallelism but also increase thread pool contention. Larger shards cause slower recovery and longer GC. Aim for 30GB per shard and adjust based on node heap. Use the formula: `shards = (node_count * heap_GB * 0.5) / 30`.