How does date math like now-7d/d work in an Elasticsearch range query?
answer
- An anchor, then offsets, then optional rounding
- Two pipes terminate a literal date anchor
- Inclusive endpoints round the generous way
- Capital M and lowercase m are different units
- An unrounded now defeats caching
basics
~20 sDate math takes an anchor (now, or a date followed by two pipes), applies offsets such as minus seven days, then optionally rounds with a slash plus a unit. Rounding direction depends on which endpoint uses it.
solid answer
~40 sA date-math expression is an anchor plus optional offsets plus an optional rounding unit. The anchor is either `now` or a literal date terminated by `||`, offsets are `+`/`-` with a unit (`y`, `M`, `w`, `d`, `h`, `m`, `s`), and `/d` rounds to the day. So `now-7d/d` means "seven days ago, rounded to a day boundary". The rounding direction depends on the endpoint: `gte` and `lt` round **down** to the start of the period, `gt` and `lte` round **up** to the last millisecond of it — which is how `gte: now/d` includes all of today and `lte: now/d` also includes all of today. Rounding matters operationally too: an unrounded `now` changes every millisecond, so the query is effectively uncacheable; rounding to the minute or day makes the same filter reusable across requests.
code
json · 10 lines{
"query": {
"bool": {
"filter": [
{ "range": { "@timestamp": { "gte": "now-7d/d", "lt": "now/d" } } },
{ "range": { "price": { "gte": 10, "lt": 100 } } }
]
}
}
}go deeper
Be ready to read an expression like now-7d/d aloud: anchor, offset, rounding. Know that a literal date anchor must be followed by two pipe characters.
Explain the rounding direction per endpoint and why gte and lte with the same rounded value both include the whole period. Know the unit letters, especially months versus minutes.
Show that you round for cacheability on hot dashboard filters, keep ranges in filter context, and reach for the time_zone parameter before someone files an off-by-one-day bug.
Own the convention across services: a standard time-window vocabulary, rounded bounds by default, explicit time zones in reporting, and awareness of what unbounded time filters cost across a large tier.
## Anatomy of a date-math expression A date-math string has up to three parts: 1. **An anchor.** Either the literal `now`, or a date string terminated by two pipes: `2024-03-01||`. 2. **Zero or more offsets.** A `+` or `-`, a number, and a unit: `y` (years), `M` (months), `w` (weeks), `d` (days), `h` or `H` (hours), `m` (minutes), `s` (seconds). Note that `M` is months and `m` is minutes — case is significant and mixing them up is a real production bug. 3. **An optional rounding**, written as `/` followed by a unit: `/d` rounds to the day, `/M` to the month. So `now-1M/d` is "one month ago, rounded to a day boundary", and `2024-03-01||+1M/M` is "1 March plus one month, rounded to the month". ## Rounding direction depends on the endpoint This is the part interviewers probe, because it is counter-intuitive. Elasticsearch rounds in whichever direction makes the endpoint *inclusive of its own intent*: - `gte: "2024-11-18||/M"` rounds **down** to `2024-11-01T00:00:00.000`, so the whole of November is included. - `gt: "2024-11-18||/M"` rounds **up** to `2024-11-30T23:59:59.999`, so the whole of November is excluded. - `lt: "2024-11-18||/M"` rounds **down** to `2024-11-01T00:00:00.000`, so November is excluded. - `lte: "2024-11-18||/M"` rounds **up** to `2024-11-30T23:59:59.999`, so November is included. The mental shortcut: `gte` and `lt` take the start of the period; `gt` and `lte` take its final millisecond. ## The endpoints and other parameters A `range` query accepts `gt`, `gte`, `lt`, `lte`, and `boost`. On date fields it also accepts: - **`format`** — overrides the date format used to parse the values *in the query*, independent of the field's mapped format. It does not apply to date-math expressions such as `now`. - **`time_zone`** — a UTC offset or IANA zone used to convert the supplied date values, and to decide where day and month boundaries fall when rounding. It does not shift `now`, which is an absolute instant. On `range`-typed fields (`date_range`, `integer_range`, and friends) there is also a `relation` parameter — `INTERSECTS`, `CONTAINS` or `WITHIN` — that controls how the query range is compared with each document's stored range. ## Why rounding is also a performance decision A filter written as `{"range":{"@timestamp":{"gte":"now-1h"}}}` produces a different lower bound on every single request, because `now` is resolved at query execution. Two identical-looking requests one millisecond apart are not the same query, so caching layers cannot reuse anything and the range has to be evaluated afresh. Rounding fixes this. `now-1h/m` changes only once a minute; `now-7d/d` changes once a day. Every request inside that window produces a byte-identical query, which is what lets caching do its job. The trade-off is precision at the boundary — `now-7d/d` reaches back to the start of the day seven days ago rather than to this exact time seven days ago — and for dashboards and alerting windows that is almost always an acceptable exchange. A common shape is a rounded, cacheable coarse bound combined with an unrounded precise bound only where the extra precision genuinely matters. ## Time zones and the off-by-one-day bug Dates are stored internally as UTC milliseconds. If your users think in `Europe/Berlin` and you write `gte: "now-1d/d"` without `time_zone`, the day boundary is midnight UTC, which is 01:00 or 02:00 local — so an hour or two of "yesterday" is on the wrong side of the boundary. Passing `time_zone: "Europe/Berlin"` makes the rounding land on local midnight. The classic reported bug is a daily report that is consistently missing the first couple of hours, or double-counting them. ## Range queries belong in filter context A range on a timestamp almost never needs to contribute to relevance. Putting it in the `filter` clause of a `bool` query skips score computation and makes the clause eligible for caching, which — combined with rounded date math — is the difference between a time filter that is essentially free and one that is re-evaluated on every shard on every request. ## Numeric ranges The same query works on numeric fields with no date math involved: `{"range":{"price":{"gte":10,"lt":100}}}`. Numeric and date fields in modern Elasticsearch are indexed in a structure built for range lookups (a block k-d tree), which is why range filters on them are fast, and why running a range on a `keyword` field instead is a much more expensive, string-comparison-based operation.
- Why does a dashboard filter of gte now-1d get slower under load than gte now-1d/d?An unrounded `now` resolves to a new millisecond on every request, so no two queries are identical and caching layers cannot reuse a previous result. `now-1d/d` changes only at a day boundary, so every request within the day issues the identical range and the work can be reused. The cost is a coarser window edge.
- A daily report built on now-1d/d is missing the first two hours of each day. What is wrong?Rounding happens in UTC unless told otherwise, so the day boundary is midnight UTC rather than local midnight. Add `time_zone` with the users' IANA zone so `/d` rounds to local midnight. Note that `time_zone` shifts the supplied values and the rounding boundaries but not `now` itself, which is an absolute instant.
- What does the format parameter on a range query control, and what does it not?`format` says how to parse the date strings supplied in that query, overriding the field's mapped format — useful when the caller sends `dd/MM/yyyy` or epoch millis. It does not apply to date-math expressions such as `now-1d`, and it does not change how values were stored; the field's own format governs indexing.
saying these in an interview costs you the question
- Thinks all four endpoints round the same direction
- Reads capital M as minutes in an offset
- Assumes rounding respects the caller's local time zone by default
- Claims now is affected by the time_zone parameter
- Sees rounding as cosmetic rather than a caching decision