The Elasticsearch date_range aggregation is a multi-bucket aggregation that groups documents into explicitly defined date intervals. Each range is given by a from/to pair (either bound is optional, both inclusive of from and exclusive of to), and can have a human-friendly key. Use it for non-uniform time partitions like "this week vs last week vs older", or for fixed reporting periods like fiscal quarters.
Syntax
GET /sales/_search
{
"size": 0,
"aggs": {
"sales_periods": {
"date_range": {
"field": "order_date",
"format": "yyyy-MM-dd",
"time_zone": "UTC",
"ranges": [
{ "to": "now-12M/M", "key": "older" },
{ "from": "now-12M/M", "to": "now-6M/M", "key": "last_year_h1" },
{ "from": "now-6M/M", "key": "recent" }
]
}
}
}
}
Each range produces one bucket with a doc_count and any sub-aggregations attached. Buckets can overlap - a document satisfying multiple ranges is counted in each.
Parameters
| Parameter | Default | Description |
|---|---|---|
field |
required | date or date_nanos field. |
ranges |
required | Array of { from, to, key? } objects. At least one of from or to per range. |
format |
- | Java date format applied to both inputs and bucket keys. |
time_zone |
UTC | Time zone for boundary evaluation and date math. |
keyed |
false | Return the response as a map keyed by key instead of an array. |
missing |
- | Substitute date for documents missing the field. |
from is inclusive, to is exclusive - mirroring half-open interval conventions. Documents on the to boundary fall into the next range.
Examples
Fixed reporting periods with keyed response for easier JSON parsing:
"date_range": {
"field": "order_date",
"format": "yyyy-MM-dd",
"keyed": true,
"ranges": [
{ "key": "Q1", "from": "2024-01-01", "to": "2024-04-01" },
{ "key": "Q2", "from": "2024-04-01", "to": "2024-07-01" },
{ "key": "Q3", "from": "2024-07-01", "to": "2024-10-01" },
{ "key": "Q4", "from": "2024-10-01", "to": "2025-01-01" }
]
}
Relative ranges with date math anchored to month boundaries:
"date_range": {
"field": "@timestamp",
"ranges": [
{ "to": "now-1M/M", "key": "before_last_month" },
{ "from": "now-1M/M", "to": "now/M", "key": "last_month" },
{ "from": "now/M", "key": "this_month" }
]
}
Date range with sum sub-aggregation:
"aggs": {
"periods": {
"date_range": {
"field": "order_date",
"ranges": [
{ "to": "now-30d", "key": "older" },
{ "from": "now-30d", "key": "last_30_days" }
]
},
"aggs": {
"revenue": { "sum": { "field": "amount" } }
}
}
}
Performance Notes
date_range is cheap. It evaluates each document's field against a fixed list of ranges, so per-document cost is constant in the number of ranges (typically a handful). No per-bucket map is built beyond the range count itself, so memory usage is trivial compared to terms or date_histogram aggregations.
Date math expressions (now-1M/M, now/d) are evaluated once at request time relative to the coordinating node's clock and the configured time_zone. Subtle bugs arise when clusters span time zones or when clients pass time_zone inconsistently - results shift by a full bucket at boundaries. Use absolute timestamps for reproducible reports.
date_range is operationally safe but loses to date_histogram when ranges are uniform and continuous. Pulse helps identify dashboards that are repeatedly running expensive aggregations against Elasticsearch and OpenSearch and surfaces optimization opportunities where date_range could replace a more expensive pattern.
Common Mistakes
- Treating
toas inclusive. It is exclusive; a document at exactlytofalls into the next range. - Building dozens of contiguous ranges manually instead of using date_histogram - date_histogram is purpose-built for uniform intervals.
- Mixing
formatbetween the index data and the aggregation. The aggregation parsesfrom/toaccording toformat; mismatches throw parse errors. - Forgetting time zone.
now/din UTC and inAsia/Tokyoproduce different bucket boundaries. - Defining overlapping ranges accidentally and then being surprised that totals exceed
hits.total.
Frequently Asked Questions
Q: Can date ranges overlap?
A: Yes. A document falling into multiple ranges contributes to each bucket. This is intentional - use it for "rolling vs absolute" comparisons - but verify it is what you want, since totals across overlapping buckets exceed the number of matched documents.
Q: Are from and to inclusive or exclusive?
A: from is inclusive, to is exclusive. A range { "from": "2024-01-01", "to": "2024-02-01" } covers all of January but not midnight on February 1.
Q: How are documents with missing date fields handled?
A: They are skipped unless missing is set, in which case the substitute date is used.
Q: Can I use date math like now-1M/M?
A: Yes. Date math is fully supported in from and to. The /M rounds down to the start of the month; /d to start of day. See the Elasticsearch date math docs for the full grammar.
Q: What is the difference between date_range and date_histogram?
A: The date_histogram aggregation creates uniform-width buckets continuously across the time range. The date_range aggregation creates an explicit, possibly non-contiguous, possibly overlapping set of buckets you define.
Q: Does date_range support date_nanos?
A: Yes. The format parameter controls how the bounds are parsed; nanosecond-precision bounds are accepted against date_nanos fields.
Related Reading
- Date Histogram Aggregation: uniform time buckets.
- Auto Date Histogram Aggregation: variable-resolution time buckets.
- Sum Aggregation: per-range totals.
- Composite Aggregation: paginate combined buckets including date sources.
- Elasticsearch Query Language: the DSL date_range runs inside.