skip to content

A terms aggregation fails with CircuitBreakingException — how do you diagnose and fix it?

level: seniorimportance: must knowfreq 55%

answer

  1. The exception text names which one tripped
  2. Different ones protect different structures
  3. Node stats carry per-breaker tripped counters
  4. One of them may indict a neighbour's query, not yours
  5. Changing the limit removes the detector, not the cause

basics

~20 s

Read which breaker tripped from the exception: request means the aggregation's own structures, fielddata means uninverted text or ordinals, parent means cluster-wide heap pressure. Fix the query — smaller size, composite paging, narrower filter, pre-aggregation — rather than raising the limit.

solid answer

~50 s

Circuit breakers are pre-flight memory estimates that reject a request instead of letting the JVM OOM. The exception names the breaker and the byte figures, and that name is the diagnosis. **request** means the aggregation's own in-flight structures — usually a huge `size`, a deep sub-aggregation tree, or a `date_histogram` over an enormous range. **fielddata** means uninverted `text` fielddata or global ordinals on a high-cardinality field. **parent** means the node was already close to its heap ceiling, so the culprit may be someone else's query entirely. Confirm with `GET _nodes/stats/breaker`, which carries per-breaker `tripped` counters. Fix the shape of the work: cut `size` and `shard_size`, page buckets with a `composite` aggregation instead of one giant `terms`, aggregate on a `keyword` sub-field, narrow the query first, or pre-aggregate with a transform. Raising the breaker limits is the one thing you should not do — it converts a clean rejection into a garbage-collection death spiral.

code

bash · 3 lines
bash
GET _nodes/stats/breaker
GET _cat/fielddata?v&s=size:desc
GET _tasks?actions=*search*&detailed

go deeper

for a junior

Recall that Elasticsearch rejects requests it estimates will use too much heap, and that shrinking the aggregation — smaller size, narrower time range — is the response, not editing cluster settings.

for a middle

Explain what each breaker protects and which request properties inflate memory: terms size and shard_size, nested bucket multiplication, fielddata on text, cardinality precision.

for a senior

Show the diagnostic path end to end: read the breaker name, correlate with node stats and running tasks, decide whether the request or the neighbourhood is at fault, and pick composite paging, filtering, or pre-aggregation as the fix.

for a principal

Own the guardrails: cluster-wide bucket limits, whether ad-hoc aggregation traffic is isolated from ingest, quotas or a query-review path for dashboards, and when the honest answer is capacity rather than tuning.

## What a circuit breaker is Elasticsearch runs several **circuit breakers**, each of which estimates how much memory an operation is about to need and refuses it with `CircuitBreakingException` if that would push a tracked total past a configured fraction of the JVM heap. The point is failure containment: one bad request returns an error to one caller instead of driving the node into unbounded garbage collection, where it becomes unresponsive, drops out of the cluster, and takes its shards' recovery cost with it. The breakers you meet around aggregations are: - **request** (`indices.breaker.request.limit`, 60% of the heap by default) — per-request data structures, which for aggregations means bucket arrays, ordinal maps for that request, and the sketches behind `cardinality` and `percentiles`. - **fielddata** (`indices.breaker.fielddata.limit`, 40% by default) — uninverted `text` fielddata and global ordinal maps. - **parent** (`indices.breaker.total.limit`) — the sum across breakers plus, when real-memory accounting is enabled, actual heap usage. It trips at a high fraction of the heap and is the one that fires when the node is simply full. - **in_flight_requests** and **accounting** — inbound transport payloads, and memory held by open Lucene segments. ## Reading the failure The exception text names the breaker, the estimated bytes for this request, and the current usage against the limit. That is your first fork: - `[request]` — this aggregation is intrinsically too big. Look at `size` on the `terms` aggregation, the number of nested sub-aggregation levels (buckets multiply), the span and interval of a `date_histogram`, and `precision_threshold` on `cardinality`. - `[fielddata]` — someone is aggregating on a `text` field with `fielddata: true`, or global ordinals for a very high-cardinality `keyword` field have grown large. `GET _cat/fielddata?v&s=size:desc` names the field. - `[parent]` — the request may be innocent. Look at what else is running: `GET _nodes/stats/breaker` for `tripped` counts per breaker, `GET _tasks?actions=*search*&detailed` for concurrent searches, and the search slow log. A single node tripping while its peers are fine usually means a hot shard rather than a global problem. A related but distinct failure is `too_many_buckets_exception`, thrown when a request would produce more buckets than the `search.max_buckets` cluster setting (65,536 by default). That is a bucket-count guard, not a memory breaker, and its fix is the same family of changes. ## Fixing the shape of the work **Shrink the request.** A `terms` aggregation with `"size": 100000` asks every shard for `shard_size` candidate buckets and merges them on the coordinating node. If you genuinely need every bucket, do not use `terms` at all — use the `composite` aggregation, which streams buckets in pages with an `after` key and holds bounded state. **Aggregate on the right field.** Move off `text` and onto a `keyword` sub-field so the values come from doc_values on disk rather than heap fielddata. **Filter before you aggregate.** Most oversized aggregations are oversized because the query matched everything. Narrowing the time range, or routing to the right data tier, removes the work rather than budgeting for it. **Bound the depth.** Nested `terms` inside `terms` multiplies bucket counts. `"collect_mode": "breadth_first"` on the outer aggregation defers sub-aggregation collection until the surviving parent buckets are known, which is the right mode when the outer field is high-cardinality but you only want a few top buckets. **Sample when the answer may be approximate.** A `sampler` aggregation restricts expensive sub-aggregations to the top-scoring documents per shard. **Move the work off the query path.** A continuous transform writing a pre-aggregated summary index, or downsampling for time-series data, turns a nightly heap crisis into a cheap lookup. **Then, and only then, consider capacity.** More heap, more nodes, or smaller shards genuinely help — but heap above roughly 30 GB loses compressed object pointers, so scaling out beats scaling up. ## Why not just raise the limit Because the breaker is not the problem; it is the detector. Raising `indices.breaker.request.limit` lets the same request allocate more, and the outcome is a node in continuous full GC — far worse operationally than an error returned to one client, because it takes every other query on that node with it. If you are convinced the limits are wrong for your workload, change them deliberately as a capacity decision with monitoring, not as a reaction to a single failing query. ## The interview shape Interviewers use this question to separate people who read the error from people who search for a setting to turn up. Say which breaker, say what that breaker protects, name the property of the request that made it large, and reach for the query shape before the configuration.

  • How would you get every bucket of a high-cardinality field without tripping the request breaker?
    Use the `composite` aggregation. It walks the value space in sorted order and returns a page of buckets plus an `after` key you feed into the next request, so shard-side and coordinating-node state stays bounded regardless of cardinality. A `terms` aggregation with a huge `size` instead materializes everything at once.
  • The parent breaker trips but your aggregation is small. What do you check?
    Whether the node was already near its heap ceiling for unrelated reasons: concurrent expensive searches via the tasks API, field data usage from another index, an unbalanced shard count on that node, or an oversized bulk indexing load. The parent breaker reflects total pressure, so the failing request is often the victim rather than the cause.
  • How does too_many_buckets_exception differ from a CircuitBreakingException?
    It is a count guard, not a memory estimate: `search.max_buckets` caps how many buckets a single search may produce, defaulting to 65,536. It fires deterministically on request shape rather than depending on current heap, so it is the safer signal that a nested aggregation is multiplying buckets. The remedy is the same — fewer, coarser buckets, or composite paging.
  • When is collect_mode: breadth_first the right choice?
    When an outer `terms` aggregation runs over a high-cardinality field but you only want a handful of top buckets, and it has expensive sub-aggregations. Breadth-first collects the parent buckets first, prunes to the survivors, then replays the documents for sub-aggregations — trading a second pass for a much smaller intermediate structure.

saying these in an interview costs you the question

  • Raising the breaker limit as the primary fix
  • Not reading which breaker the exception names
  • Assuming the failing request is always the guilty one
  • Confusing search.max_buckets with a memory breaker
  • Believing a breaker trip means the node already ran out of heap

context