What Is a Composite Aggregation in Elasticsearch?
A complete, zero-prior-knowledge walkthrough of how Elasticsearch lets you page through millions of unique bucket combinations reliably — without ever running out of memory or losing your place.
Imagine you’re handed a massive filing cabinet with millions of folders, and your job is to go through every single one, in order, without skipping any and without your assistant accidentally handing you the same folder twice. If you tried to grab the whole cabinet’s contents in one armful, you’d drop everything — literally, in computing terms, you’d run out of memory. The sensible approach is to take a manageable stack at a time, remember exactly where you stopped, and ask for the next stack starting right after that point. A composite aggregation is Elasticsearch’s purpose-built tool for doing exactly this over aggregation results — reliably paging through every unique combination of values in your data, one manageable batch at a time, in a way that scales to millions or even billions of unique combinations without ever trying to hold them all in memory at once. In this article, we’ll build a complete, practical understanding of what a composite aggregation is, how it works internally, and how real production systems use it to process enormous datasets safely.
1Core Concepts
Before we can understand composite aggregations, we need to understand the problem they were specifically built to solve. Elasticsearch’s aggregations feature lets you compute summary statistics and group data into buckets — similar to a GROUP BY clause in SQL. A terms aggregation, for example, can group all your documents by a field’s distinct values: group log entries by status_code, group sales records by region, group user sessions by browser. This is extraordinarily useful, and for a field with a modest number of distinct values — say, a few dozen status codes, or a handful of regions — a simple terms aggregation works beautifully, returning every bucket in a single response.
But what happens when the field (or combination of fields) you want to group by has millions of distinct values? Imagine grouping web analytics data by the combination of user_id and day — across a large platform, that could easily produce tens of millions of unique buckets. A standard terms aggregation isn’t designed for this: it computes and holds its entire bucket list in memory on each node before returning results, and asking it to return millions of buckets in one response would consume an enormous, unpredictable amount of memory, risking the exact kind of resource exhaustion problem we’ve seen elsewhere in this series with uncontrolled field growth and runaway wildcard queries.
Think of a standard terms aggregation like asking someone to memorize every unique license plate that drove past a busy highway checkpoint in a single day, then recite the entire list back to you at once — fine for a quiet country road with a few dozen cars, but an impossible task at a checkpoint seeing millions of vehicles. A composite aggregation is like handing that same person a clipboard and asking instead: “Just write down the next 100 plates, in order, and tell me the last one you wrote — I’ll come back for the next 100 after that.” The total workload is identical, but broken into batches nobody has to hold in their head all at once.
Formally, a composite aggregation is a special type of bucket aggregation designed specifically for this “paginate through every unique combination, reliably and efficiently” use case. It groups documents into buckets based on one or more sources — each source is itself a smaller aggregation, like a terms, histogram, date_histogram, or geotile_grid aggregation — and it combines the results from all of those sources into composite keys, meaning each resulting bucket represents a unique combination of values, one from each source, rather than a single field’s value alone. Crucially, it returns results in a fixed-size page (controlled by a size parameter, similar in spirit to a SQL LIMIT) along with a special token — the after key — that tells you exactly where you left off, so your very next request can pick up precisely where the previous one ended.
A regular terms aggregation tries to compute and return its full result set in one response, which becomes impractical past a certain cardinality. A composite aggregation is deliberately built for sequential, memory-bounded pagination — it never tries to hold more than one page’s worth of buckets in memory at a time, no matter how many total unique combinations exist across your entire dataset.
It’s worth being precise about what “composite” actually refers to here, since the name can be a little abstract on first encounter. It refers to the fact that a resulting bucket’s key is itself a small JSON object combining values from multiple sources at once — for example, {"region": "APAC", "day": "2026-09-15"} — rather than a single scalar value the way a plain terms aggregation’s bucket key would be. This is precisely what makes composite aggregations so useful for multi-dimensional grouping tasks: analyzing traffic by both country and hour, or sales by both product category and week, where you genuinely need every unique pairing accounted for, not just the top values of either dimension considered independently.
| Approach | Result Set Model | Memory Behavior | Best For |
|---|---|---|---|
Plain terms aggregation | Top N buckets, one response | Grows with requested size | Small-to-moderate cardinality, “top values” views |
Deep terms pagination | Simulated via huge size + client slicing | Grows sharply, unpredictably | Rarely a good idea past a few thousand buckets |
| Composite aggregation | Fixed-size sequential pages | Bounded by page size only | Millions/billions of unique combinations |
| Scroll API | Sequential pages of raw documents | Server-side context held open | Bulk document export, not bucketed aggregation |
2Architecture & Components
sources
An ordered array of named sub-aggregations — typically terms, date_histogram, histogram, or geotile_grid — each contributing one dimension to the final composite bucket key. Order matters, since it determines sort order and how the after key is interpreted.
size
The maximum number of composite buckets to return per request — the equivalent of “how many folders to hand me from the stack this time.” A typical value ranges from a few hundred to a few thousand, balanced against available memory and network payload size.
after
An optional parameter, set on every request after the first, containing the exact composite key of the last bucket seen in the previous response — this is what tells Elasticsearch precisely where to resume.
sub-aggregations
Just like any other bucket aggregation, a composite aggregation can nest further metric aggregations (sum, average, cardinality) inside each of its buckets, computing statistics per unique combination as it pages through.
Each entry inside the sources array is itself a small named aggregation, and the order you list them in is not cosmetic — it directly determines the sort order composite buckets are returned in, and correspondingly, the exact shape of the after key you’ll receive back. A composite aggregation with sources [region, day] sorts and pages through results primarily by region, then by day within each region — a composite aggregation with sources [day, region] does the reverse. This ordering guarantee is actually one of composite aggregation’s most valuable, easy-to-overlook properties: unlike some other pagination-style features in Elasticsearch, composite aggregation results are returned in a stable, deterministic, fully-ordered sequence, which is exactly what makes reliable “pick up where I left off” pagination possible in the first place.
flowchart TD
A["sources: [ terms on 'region', date_histogram on 'day' ]"] --> B["Composite key example:
{ 'region': 'APAC', 'day': '2026-09-15' }"]
B --> C{Composite aggregation sorted
by source order}
C --> D["Page 1: first 'size' buckets returned
+ after_key = last bucket's key"]
D --> E["Next request sets 'after' to that key"]
E --> F["Page 2 resumes exactly after
the previous last bucket, no gaps, no repeats"]
One more architectural detail matters for correctness: because a terms source inside a composite aggregation sorts alphabetically (lexicographically) by default rather than by document count, composite aggregation pagination behaves predictably and consistently across requests — a property that a plain terms aggregation’s default “most documents first” ordering does not reliably offer once you try to page deep into its results, which is precisely one of the underlying reasons composite aggregation exists as a distinct feature rather than simply being “a terms aggregation with a bigger size.”
Two smaller but genuinely useful configuration options round out the architecture. The missing_bucket setting, available on each source, controls whether documents lacking a value for that particular source field are included in their own dedicated bucket (with a null composite key component) or silently excluded from the aggregation entirely — a decision that matters a great deal for data-quality-sensitive use cases like the deduplication and auditing patterns covered later in this article, where a missing value might itself be exactly the kind of anomaly worth surfacing rather than quietly dropping. Similarly, an order setting on individual sources lets you choose ascending or descending sort direction per dimension, which affects the sequence buckets are walked through but, importantly, does not change the fundamental guarantee that every unique combination will eventually be visited exactly once across a complete pagination walk.
3Internal Working
To understand why composite aggregation scales so much better than deep pagination through a regular terms aggregation, we need to look at what each approach actually does internally, shard by shard.
A regular terms aggregation, when asked for its top N buckets, works by having every shard independently compute its own local view of the top buckets, which the coordinating node then merges into a single, globally accurate top-N list. This works well for a reasonably small N. But if you try to simulate “pagination” with a plain terms aggregation by requesting an enormous size (say, asking for the top 500,000 buckets just so you can slice out page 5,001 client-side), every shard is forced to compute and transmit its own version of that entire 500,000-bucket list, and the coordinating node must merge all of those massive per-shard lists together — an approach whose memory and network cost grows sharply, and often unpredictably, the deeper you try to page.
Deep pagination through a plain terms aggregation requires the whole cluster to compute and hold an ever-growing prefix of the entire result set in memory, just to let you skip to a later page — a pattern that becomes prohibitively expensive well before you reach the millions-of-buckets scale composite aggregation is designed to handle comfortably.
A composite aggregation avoids this entirely by never attempting to compute a full ranked list at all. Instead, each shard independently builds a small, bounded structure — conceptually similar to a min-heap or a sorted, size-limited buffer — holding only the current page’s worth of the smallest (in composite-key sort order) buckets it has seen so far, discarding candidates that fall outside that current window as it processes documents. Because the after parameter tells every shard exactly which composite key to start strictly after, each shard can immediately skip past everything it already knows is “before” that point, rather than recomputing or re-transmitting buckets that were already returned in a previous page. The coordinating node then merges each shard’s small, bounded candidate set into the final page of results — a merge operation whose cost stays proportional to the requested size, not to the total number of unique combinations that might exist across the entire dataset.
This is the fundamental architectural insight worth internalizing: a composite aggregation’s per-request cost is governed almost entirely by your chosen size (how big each page is) and the number of shards involved — not by the total cardinality of the underlying data. Whether your dataset has ten thousand or ten billion unique combinations, a single composite aggregation request asking for a page of 1,000 buckets does roughly the same, bounded amount of work per shard. This predictability, more than raw speed on any single request, is precisely why composite aggregation is the tool of choice whenever you genuinely need to walk through every bucket in a high-cardinality dataset, rather than just the top handful.
It’s worth visualizing the cost curves of these two approaches side by side to see exactly why this matters in practice. Deep pagination through a plain terms aggregation has a cost curve that climbs steeply and continuously as you request more buckets — page 1,000 costs meaningfully more than page 1, because the underlying mechanism has to compute and hold an ever-larger prefix of the ranked list regardless of how far into that list you actually care about. Composite aggregation’s cost curve, by contrast, stays essentially flat across every page of a walk — page 1,000 costs roughly the same as page 1, because each shard only ever needs to track a fixed-size window of candidates relative to the current after key, never a growing prefix of the whole ranked sequence. This flatness is not a minor implementation detail; it’s the entire reason composite aggregation can be used confidently on datasets whose total cardinality is unknown or unbounded in advance, a scenario where deep terms pagination would become impractical well before the walk ever finished.
4Data Flow & Lifecycle
Let’s trace the full lifecycle of a typical composite aggregation job — the pattern almost every real-world usage of this feature follows, whether it’s exporting data, powering a dashboard, or feeding an ETL pipeline.
First Request (No after)
A client submits a composite aggregation request with a defined size and sources, but no after parameter — Elasticsearch treats this as “start from the very beginning” of the sorted composite key sequence.
First Page Returned
The response includes up to size composite buckets, sorted by the composite key, plus an after_key field containing the exact key of the final bucket in this page.
Client Processes the Page
The client does whatever it needs with this batch — writes it to a downstream system, aggregates it into a report, streams it into a data pipeline — while holding only this one page’s worth of data in memory at any given time.
Next Request (With after)
The client issues an essentially identical request, but this time sets the after parameter to the exact after_key value received in the previous response.
Repeat Until Exhausted
This request/process/repeat cycle continues, each time advancing the after key, until a response is returned containing fewer buckets than the requested size (or zero buckets) — the signal that every unique combination has now been visited.
Completion
The client stops issuing further requests once that under-sized (or empty) final page is received, having processed every single composite bucket exactly once, in a fully deterministic order, without ever holding more than one page in memory.
This lifecycle pattern is deliberately stateless on the server side — unlike Elasticsearch’s scroll API, which historically kept a server-side search context open and consuming resources between requests, a composite aggregation’s after_key is simply a piece of data the client carries forward and resends. This means there’s no server-side session to expire, no background resource being held open while your client is busy processing a page, and no risk of a long-idle client causing cluster-side cleanup problems — a meaningful operational advantage for pipelines that might pause for minutes or hours between pages while performing slow downstream work.
This statelessness also gives composite aggregation a useful idempotency property that many other pagination mechanisms lack: if a client crashes, retries, or simply re-sends the exact same request with the same after value, it receives the exact same page of results again (assuming the underlying data hasn’t changed in the meantime), rather than silently advancing past data it never actually finished processing. This makes it substantially easier to build genuinely reliable, retry-safe batch pipelines on top of composite aggregation than on top of a stateful mechanism, where a lost or duplicated request can leave the server-side session in an ambiguous state that’s hard for the client to reason about after the fact.
5Advantages, Disadvantages & Trade-offs
Advantages
- Scales to millions or billions of unique bucket combinations with predictable, bounded per-request memory and network cost, regardless of total cardinality.
- Stateless between requests — no server-side session or scroll context to keep alive, expire, or accidentally leave open under load.
- Fully deterministic, ordered pagination guarantees every unique combination is visited exactly once, with no gaps and no duplicates, even across many sequential requests.
- Supports multi-dimensional grouping naturally, combining several source fields into a single composite key without awkward workarounds.
- Composes cleanly with nested sub-aggregations, letting you compute real metrics (sums, averages, cardinalities) per unique combination as you page through.
Disadvantages & Trade-offs
- Strictly sequential — you cannot jump directly to “page 500” the way you might with a traditional offset-based pagination scheme; you must walk through pages in order.
- Does not return a total bucket count up front, since computing that would require the very full scan the feature is designed to avoid — clients wanting a progress estimate need a separate, coarser approach.
- Slightly more complex to implement client-side than a single request-response call, since the calling code must manage the request/process/repeat loop and correctly carry the after key forward.
- Not the right tool for simple “top 10 values” dashboards, where a plain
termsaggregation is both simpler to write and cheaper to run. - Overall wall-clock time to walk an entire high-cardinality dataset can still be substantial, since the total work hasn’t disappeared — it’s been made memory-safe and predictable, not instantaneous.
A composite aggregation is like reading a very long book one chapter at a time, using a bookmark, rather than trying to photograph every page at once and sort through the pile afterward. You can’t jump straight to chapter 40 without first knowing where chapter 39 ended — but you’ll never drop the book, you’ll never lose your place, and you can put it down between chapters for as long as you like without losing any progress at all.
The trade-off worth naming explicitly is the same space-versus-time and flexibility-versus-predictability trade-off seen elsewhere in this series, applied here to pagination itself: a random-access, jump-to-any-page interface is more flexible for a human casually browsing results, but fundamentally harder to make memory-safe at very large scale, since supporting “jump to page 500,000” essentially requires being able to cheaply compute what comes before it. A sequential, bookmark-based interface gives up that flexibility in exchange for a guarantee that no matter how large the underlying dataset grows, the cost of the next page never depends on how many pages came before it — precisely the guarantee a batch pipeline processing an unbounded, ever-growing dataset actually needs far more than a human casually browsing ever would.
6Security
Composite aggregation’s security profile centers, much like the other features covered in this series, on resource consumption rather than data exposure — the feature exists precisely to make large-scale aggregation safer, but it can still be misused or misconfigured in ways that strain a cluster.
The most direct risk is an oversized size parameter. While composite aggregation is far more memory-bounded than deep pagination through a plain terms aggregation, its per-request cost still scales with whatever size you request — asking for a page of 100,000 or 1,000,000 buckets in a single call, especially with several nested sub-aggregations computing additional metrics per bucket, can still meaningfully strain heap memory and response payload size on a single request, even though the feature’s design keeps that cost bounded and predictable rather than unbounded and runaway. This becomes a genuine concern specifically when composite aggregation requests are triggered by user-controllable input — for example, an internal dashboard or reporting tool that lets a user configure their own page size without a sensible upper limit enforced server-side.
Cap size at a sensible, application-appropriate ceiling (commonly a few hundred to a few thousand, depending on how many sub-aggregations are nested inside), and rely on Elasticsearch’s built-in request circuit breaker — which tracks estimated memory use for an in-flight request and rejects it outright with a clear error if it would exceed a configured limit — as a hard backstop against any single aggregation request, composite or otherwise, consuming more memory than the cluster can safely spare.
A second, more operational concern is runaway pagination loops in client code — a bug in a pipeline’s request/process/repeat cycle that fails to correctly advance the after parameter, or that retries the same page indefinitely after a transient failure without proper backoff, can generate a sustained stream of redundant load against a cluster. Because composite aggregation queries can involve real computational work per shard (especially with nested metric sub-aggregations), a client-side bug of this kind is a more subtle, harder-to-spot resource risk than an obviously malformed request — it looks like legitimate, well-formed traffic, just needlessly repeated, which is precisely why the monitoring practices in the next chapter matter as much for composite aggregation as for any other potentially expensive query pattern.
7Monitoring, Logging & Metrics
The Profile API
Attaching "profile": true to a composite aggregation request reveals exactly how much time each shard spent building its candidate bucket set — useful for spotting whether a particular shard or sub-aggregation is disproportionately expensive.
Circuit Breaker Stats
GET /_nodes/stats/breaker shows how close the request circuit breaker is running to its configured limit — a rising trend here, correlated with aggregation-heavy workloads, is an early warning sign worth investigating before requests start being rejected outright.
Slow Log
Elasticsearch’s configurable search slow log captures any request — including composite aggregation calls — exceeding a defined latency threshold, along with its full request body, making it possible to identify a specific misconfigured or unexpectedly expensive pagination job after the fact.
Application-Level Progress Logging
Because composite aggregation doesn’t return a total count up front, most production pipelines log their own running page count and elapsed time as they walk through results, giving operators visibility into pipeline progress and health independent of anything Elasticsearch itself reports.
Because composite aggregation is so often used inside long-running batch jobs — exporting a full dataset, feeding a nightly ETL run — it’s worth pairing cluster-side monitoring with application-side observability: logging the current after key, the number of pages processed so far, and the elapsed wall-clock time at reasonable intervals lets an operator quickly tell whether a stalled-looking job is genuinely stuck, merely slow due to expected data volume, or has silently crashed partway through without properly reporting failure. This kind of lightweight, application-level checkpoint logging is standard practice across nearly every production system that leans on composite aggregation for bulk data processing.
8Design Patterns & Anti-patterns
Proven Patterns
“Full Dataset Walk”
Using composite aggregation as a reliable, memory-safe alternative to the scroll API when the goal is to visit every unique bucket combination in a large dataset — for example, exporting a complete list of every distinct user-and-day combination for a downstream analytics warehouse.
“Checkpointed Batch Jobs”
Persisting the current after_key to durable storage (a database row, a file, a message queue offset) after each successfully processed page, so a batch job that fails partway through can safely resume from the last checkpoint instead of restarting entirely from the beginning.
“Multi-dimensional Rollups”
Combining a date_histogram source with one or more terms sources to walk through every unique time-bucket-and-category combination, feeding each into a nested sum or average sub-aggregation — a common building block for pre-computed reporting rollups.
“Bounded Dashboard Pagination”
Exposing composite aggregation’s page-at-a-time model directly through a “load more” style UI control, rather than trying to retrofit traditional numbered-page navigation onto a fundamentally sequential pagination mechanism.
Anti-patterns to Avoid
The Problem
Using composite aggregation for a simple “show the top 10 categories” dashboard widget, where a plain terms aggregation would be simpler to write, easier to reason about, and just as fast — adding unnecessary pagination-loop complexity to client code for a use case that never needed it.
Why It Happens
Once a team learns composite aggregation for genuinely high-cardinality use cases, there’s a temptation to reach for it as a general-purpose default “safer” aggregation tool, rather than reserving it specifically for the full-walk, high-cardinality scenarios it was actually designed for.
The Fix
Use a plain terms (or other bucket) aggregation whenever you genuinely only need the top N results and total cardinality is modest; reserve composite aggregation specifically for cases where you need to visit every unique combination across a large or unbounded set.
The Problem
Mutating the underlying data (deleting or heavily updating documents) while a long-running composite aggregation walk is still in progress, then being surprised when the walk’s results appear inconsistent — some combinations missing, others double-counted relative to what existed at the start.
Why It Happens
Composite aggregation, like most Elasticsearch queries, reflects the data as it exists at the moment each individual page request executes — it does not implicitly snapshot the entire dataset at the start of a multi-page walk the way a database transaction might.
The Fix
For jobs where consistency across the full walk genuinely matters, pin the aggregation to a specific point-in-time snapshot of the index (using Elasticsearch’s point-in-time, or PIT, feature) for the duration of the pagination job, ensuring every page sees the exact same consistent view of the data regardless of how long the full walk takes or what writes happen concurrently.
The Problem
Running many composite aggregation pagination walks concurrently against the same index from a single pipeline, without coordinating their combined resource footprint, causing the cluster’s request circuit breaker to reject requests under combined load that would each have been perfectly fine running alone.
Why It Happens
Because a single composite aggregation request is individually well-behaved and bounded, it’s easy to assume that running many of them in parallel remains proportionally safe — but circuit breakers and thread pools track cluster-wide resource use, not per-request use in isolation.
The Fix
Treat concurrent composite aggregation jobs the same way you’d treat any other batch of potentially expensive concurrent requests: apply a sensible concurrency limit at the application or pipeline-orchestration level, and monitor cluster-wide circuit breaker and thread-pool metrics under realistic combined load before assuming a pattern that works for one job scales safely to many simultaneous ones.
9Best Practices & Common Mistakes
Choose a size that balances throughput and safety
A larger page size reduces the total number of round trips needed to walk a dataset, but increases per-request memory and payload cost — most production jobs settle somewhere in the low thousands after empirical testing against real data volume.
Use a point-in-time for long-running walks
Pair composite aggregation with Elasticsearch’s point-in-time feature whenever a job spans a meaningful duration, ensuring consistent, repeatable results even as the underlying index continues to receive writes.
Persist the after key for resumability
Always checkpoint the current after_key somewhere durable after each successfully processed page, so a crashed or interrupted job can resume cleanly rather than restarting an expensive full walk from scratch.
Forgetting the completion condition
Some implementations mistakenly loop until they receive zero buckets, rather than fewer than the requested size — while a fully empty final page can occur, the more general, reliable termination signal is any page returning strictly fewer buckets than requested.
Assuming source order doesn’t matter
Reordering the sources array between requests within the same logical walk silently changes both the sort order and the meaning of any in-flight after_key, producing corrupted or nonsensical pagination — the sources configuration must stay identical across every page of a single walk.
Ignoring nested sub-aggregation cost
Adding expensive nested sub-aggregations (like a high-precision cardinality metric) inside every composite bucket can meaningfully increase per-page cost, even though the outer composite aggregation’s own pagination mechanics remain efficient — profile the full request, not just the composite structure alone.
Log progress at sensible intervals
For long-running walks spanning many thousands of pages, log a lightweight progress line every N pages rather than on every single page, avoiding log-volume overhead while still giving operators a reliable signal that a job is actively progressing rather than silently stalled.
Treating the after key as human-meaningful
The after key is an opaque structure meant to be passed back verbatim, not parsed, modified, or reconstructed manually by application code — teams that try to hand-craft an after key from scratch instead of using the one Elasticsearch returned risk subtle pagination bugs that are difficult to diagnose.
10Real-world & Industry Examples
Data Warehouse ETL Pipelines
Organizations that periodically export aggregated data from Elasticsearch into a downstream data warehouse or business intelligence system commonly use composite aggregation to walk through every unique combination of dimensions (customer, product, day) reliably, feeding each page directly into a bulk-load or streaming-insert process without needing to hold the entire result set in application memory at once.
Observability & Metrics Rollups
Monitoring platforms that pre-compute rolled-up metrics — average latency per service per minute, error counts per endpoint per hour — over enormous volumes of raw trace and log data use composite aggregation to systematically walk through every unique service-and-time-window combination, generating compact, pre-aggregated summary records that dashboards can query far more cheaply than scanning raw event data on every page load.
Deduplication & Data Quality Auditing
Data engineering teams running deduplication or data-quality audits over large, potentially messy datasets use composite aggregation to enumerate every unique combination of a candidate “identity” (like email plus name plus signup date), comparing bucket document counts to flag combinations with unexpectedly many documents as likely duplicate or malformed records — a task that inherently requires visiting every unique combination, not just the most common ones.
Marketing & Ad-tech Attribution
Advertising and marketing analytics platforms, which routinely need to attribute conversions across enormous combinations of campaign, channel, device, and geography, rely on composite aggregation to compute complete, exhaustive attribution reports rather than approximate top-N summaries, since underreporting a long tail of smaller campaigns can meaningfully distort attribution accuracy in ways a “top 10 channels” dashboard would never reveal.
Compliance & Regulatory Reporting
Financial services and other regulated industries that must produce complete, auditable reports — every unique account-and-transaction-type combination for a reporting period, for example — depend on composite aggregation’s exhaustive, gap-free guarantee specifically because a regulator reviewing such a report cares that nothing was silently omitted, a requirement a “top N” summary view is fundamentally unable to satisfy regardless of how large N is chosen to be.
Search Index Migration & Reindexing Verification
Teams migrating data between indices or Elasticsearch clusters often use composite aggregation as a verification step after reindexing, walking through every unique combination of a chosen dimension in both the source and destination index and comparing bucket-level document counts, catching subtle data-loss or transformation bugs that a simple total-document-count comparison would completely miss.
11Frequently Asked Questions
terms aggregation is for.terms, date_histogram, and histogram, a geotile_grid source is also supported for geospatial bucketing, letting composite aggregation walk through every unique combination that includes a geographic dimension alongside any other source types.12Summary and Key Takeaways
A composite aggregation is Elasticsearch’s purpose-built solution for reliably walking through every unique combination of one or more grouping dimensions — combinations that could number in the millions or billions — without ever needing to hold the full result set in memory at once. It achieves this through a deliberately simple, deterministic mechanism: sorted, bounded bucket sets on each shard, and a stateless after key the client carries forward to resume exactly where the previous page left off. This design trades the convenience of jumping to an arbitrary page for the guarantee of predictable, bounded resource use regardless of how large the underlying dataset grows.
Key Takeaways
- Composite aggregation exists to page reliably through high-cardinality bucket combinations, where a plain
termsaggregation’s return-everything-at-once model breaks down. - Each bucket’s key combines values from multiple sources (terms, date_histogram, histogram, geotile_grid), enabling genuine multi-dimensional grouping.
- The
afterkey drives stateless, gapless, duplicate-free pagination — no server-side session is kept open between requests, unlike the scroll API. - Per-request cost scales with your chosen
size, not with the total number of unique combinations in the dataset — the defining property that makes it safe at scale. - Pagination is strictly sequential — there is no jumping directly to an arbitrary page, only walking forward one page at a time.
- Pair it with a point-in-time snapshot for any long-running walk where consistency across pages genuinely matters, since the index can keep changing underneath an in-progress walk otherwise.
- Real systems — ETL pipelines, observability rollups, deduplication audits, and ad-tech attribution — rely on composite aggregation precisely because they need to visit every combination, not just the most popular ones.
None of this requires memorizing Elasticsearch’s internal heap-merging implementation details. What it requires is the mental model this article has built from the start: think of the after key as a bookmark in a very long book, think of each page as a manageable stack you can process and set aside, and remember that the whole point of this design is that neither the book’s length nor the size of any single stack ever has to be known in advance for the approach to work reliably. Configure your sources deliberately, choose a sensible page size, checkpoint your progress, and a composite aggregation becomes exactly what it should be — a dependable way to touch every last record in a dataset too large to hold in memory all at once.
Taken together with the other techniques covered across this series — dynamic templates for controlling how new data gets typed, wildcard queries for ad-hoc pattern matching, and edge n-grams for instant, keystroke-driven search — composite aggregation completes a recurring theme: Elasticsearch consistently offers a deliberate, more disciplined alternative wherever a naive, convenience-first approach would eventually break down at real production scale. Learning to recognize which situation you’re actually in — small and simple, or large and unbounded — is what lets you reach for the right tool the first time, rather than discovering the limits of the convenient one the hard way, in production, under load.