Configuring Index Rollover Conditions
Rollover is the single boundary that decides how large every managed index gets, how often the write path cuts to a fresh backing index, and when data becomes eligible to migrate off the hot tier. Set the conditions too aggressively and the deployment fragments into thousands of tiny shards, flooding the master node with cluster-state updates; set them too loosely and a single primary shard sails past the 50 GB soft limit, dragging query latency down and stretching recovery times into hours. This page is about picking rollover thresholds that hold a stable shard size under variable ingest, wiring them into an ILM policy through a write alias, and verifying the trigger actually fires in production rather than silently stalling.
Rollover is the first transition in the lifecycle state machine — the handoff from active ingestion into managed storage. It assumes the surrounding architecture is already sound: node data-tier roles are tagged, disk watermarks are configured, and every index inherits its policy from a template rather than being managed by hand. Those foundations belong to Index Lifecycle Management (ILM), the control plane this page plugs into. What follows is how to make that first cut land at exactly the boundary your cluster capacity plan expects.
Prerequisites
Architecture: The Write Alias and the OR-Combined Trigger
Rollover decouples ingestion from physical storage through a single write alias. All ingestion clients target the alias, never a concrete index name; behind it sits exactly one backing index flagged is_write_index: true. When any rollover condition is met, Elasticsearch creates the next backing index (-000002, -000003, …), flips the write flag to it, and leaves the previous index immutable and append-closed for later phases to shrink, force-merge, and eventually delete. The conditions are OR-combined — whichever threshold fires first advances the alias — which is why a robust policy always defines both a time trigger and a size trigger.
This transition is the entry edge of the phase state machine. The hot phase is the only phase where writes and rollover happen; once an index rolls over, it is frozen against ingestion and becomes a candidate for tier migration governed by the hot-warm-cold architecture. Two settings anchor the whole mechanism: index.lifecycle.rollover_alias, which names the alias ILM advances, and index.lifecycle.name, which attaches the policy carrying the rollover action. If either is missing, the index is unmanaged — it will never roll over and will accumulate shards until a disk watermark blocks writes.
Three effects follow directly from where you set the thresholds:
- Shard size stability.
max_primary_shard_sizeis the condition that most directly controls the resource that matters — primary shard size. Anchoring it near 50 GB keeps Lucene segment merges efficient and query fan-out bounded regardless of ingest spikes. - Predictable index boundaries.
max_ageguarantees a fresh index on a fixed cadence (daily, weekly) even during a lull, which keeps retention math and index naming legible for operators and dashboards. - Runaway protection.
max_docsacts as a safety valve against pathological cardinality — a burst of tiny documents can breach a doc-count ceiling long before either size or age would.
Configuration Reference
A production rollover action lives inside the hot phase of an ILM policy. The block below is annotated to show what each field controls; it pairs the size and age triggers so neither an ingest spike nor a quiet period produces a badly-sized index.
PUT _ilm/policy/logs-production-rollover
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50gb",
"max_age": "1d",
"max_docs": 100000000
},
"set_priority": {
"priority": 100
}
}
},
"warm": {
"min_age": "2d",
"actions": {
"allocate": {
"number_of_replicas": 1,
"require": { "data": "warm" }
},
"shrink": { "number_of_shards": 1 },
"force_merge": { "max_num_segments": 1 }
}
}
}
}
}A few field-level rules that trip up most first deployments:
min_agein later phases is measured from the rollover event, not index creation. Awarmphase withmin_age: 2dstarts its clock the moment the index rolls over, so the age trigger and the phase timers interact — a slow-filling index that hitsmax_ageearly begins its warm countdown early too.max_sizeis deprecated in favour ofmax_primary_shard_size. The oldermax_sizemeasured total store including replicas, which made shard sizing unpredictable as replica counts changed. Prefermax_primary_shard_sizeso the threshold tracks the resource that actually governs merge and query cost.- Always define at least two conditions. A policy with only
max_agewill let a traffic surge produce a 300 GB shard; one with onlymax_primary_shard_sizewill never roll a low-traffic index, leaving stale data pinned to the hot tier past its retention intent.
The rollover action must be paired with an alias. The policy attaches through the template, and the template names the alias via index.lifecycle.rollover_alias; building that policy-and-template pair is covered in depth under building custom ILM policies via the API.
Step-by-Step Implementation
Deploying rollover is a deterministic three-step bootstrap: apply the policy, create the first backing index with an explicit write alias, then confirm ILM has adopted it. The Python v8+ script below automates the sequence with typed error handling.
import logging
from elasticsearch import Elasticsearch, ApiError
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def bootstrap_rollover_pipeline(es: Elasticsearch, policy_name: str, alias: str) -> None:
rollover_policy = {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50gb",
"max_age": "1d",
},
"set_priority": {"priority": 100},
},
}
}
}
try:
# 1. Apply (or update) the ILM policy — carries the rollover action.
es.ilm.put_lifecycle(name=policy_name, policy=rollover_policy)
logger.info("Policy '%s' applied.", policy_name)
# 2. Bootstrap the first backing index and flag it the write index.
# The -000001 suffix is required: ILM increments the numeric suffix on each rollover.
initial_index = f"{alias}-000001"
es.indices.create(
index=initial_index,
aliases={alias: {"is_write_index": True}},
settings={
"index.lifecycle.name": policy_name,
"index.lifecycle.rollover_alias": alias,
},
)
logger.info("Index '%s' created with write alias '%s'.", initial_index, alias)
# 3. Verify ILM has picked up the index and reports a phase.
explain = es.ilm.explain_lifecycle(index=initial_index)
phase = explain["indices"][initial_index]["phase"]
logger.info("ILM attached. Current phase: %s", phase)
except ApiError as exc:
logger.error("Elasticsearch API error: %s", exc.info)
raise
# v8 client initialization
es_client = Elasticsearch(
"https://localhost:9200",
basic_auth=("elastic", "YOUR_SECURE_PASSWORD"),
ca_certs="/path/to/http_ca.crt",
verify_certs=True,
)
bootstrap_rollover_pipeline(es_client, "logs-production-rollover", "logs-production")The -000001 suffix is not cosmetic: ILM parses the trailing zero-padded integer and increments it on each rollover, so the alias must be bootstrapped onto an index that ends in a number. For manual testing, or to force an emergency cut during a capacity incident, trigger the transition explicitly against the alias:
POST logs-production/_rollover
{
"conditions": {
"max_age": "1d",
"max_primary_shard_size": "50gb"
}
}An explicit _rollover call with a conditions block evaluates the same OR logic on demand; omit conditions entirely to force an unconditional roll (useful when draining a hot node for maintenance).
Verification
Confirm the trigger is real, not just that the policy applied. First, verify the alias has exactly one write index — a fractured alias is the most common cause of silent ingest failure:
GET _cat/aliases/logs-production?v&h=alias,index,is_write_indexExactly one row must show is_write_index as true. Next, inspect where ILM thinks the current index sits and which step it is waiting on:
GET logs-production-000001/_ilm/explain{
"indices": {
"logs-production-000001": {
"managed": true,
"phase": "hot",
"action": "rollover",
"step": "check-rollover-ready",
"phase_execution": { "policy": "logs-production-rollover" }
}
}
}"managed": true confirms the policy is attached; "step": "check-rollover-ready" means ILM is actively polling the conditions each indices.lifecycle.poll_interval (default 10m). An index reporting "managed": false copied its settings but never adopted the policy — it will ignore every rollover threshold. Finally, watch primary shard size approach the trigger so you can confirm the cut lands where you expect:
GET _cat/shards/logs-production*?v&h=index,shard,prirep,store&s=store:descWhen the pri shard store reaches ~50 GB, the next poll should advance the alias to logs-production-000002.
Threshold Tuning & Performance Guidance
Static thresholds copied from a blog post rarely fit a specific cluster. Tune them against the resource each condition actually governs:
- Size the primary cap to Lucene, not to disk. Target 30–50 GB per primary shard. Below ~10 GB the fixed per-shard overhead (cluster state, thread-pool slots, file handles) dominates and the master node drowns in shard bookkeeping; above ~50 GB segment merges and shard recovery grow expensive and slow. Set
max_primary_shard_sizeat the top of that band. - Derive
max_agefrom ingest, then round to an operational boundary. If a shard reaches 50 GB in roughly 18 hours at peak, amax_ageof1dmeans size usually fires first on busy days and age catches the quiet ones. Aligningmax_ageto a calendar boundary (daily, weekly) keeps index names and retention windows legible. - Keep
max_docsas a ceiling, not a primary lever. Set it well above the doc count a normal 50 GB shard holds, so it only trips during genuine cardinality anomalies. Sizing rollover primarily by document count makes shard size swing with average document size. - Account for shard count.
max_primary_shard_sizemeasures the largest single primary, so an index withnumber_of_shards: 3rolls when its biggest shard hits the cap, meaning total index size is roughly the cap times the shard count. Choose primary count and the size cap together; the interaction between shard count and the size trigger is explored in configuring rollover based on max primary shard size.
On JVM heap: each open shard carries a fixed heap cost for its segment metadata and field data structures, so under-rolling (too many small shards) is a heap problem long before it is a disk problem. A common rule of thumb is to keep total shards per node under 20 per GB of configured heap; oversized max_age/max_docs values that spawn many tiny indices breach this quietly.
Troubleshooting
illegal_argument_exception: rollover target [alias] does not point to a write index
Symptom: the _rollover call or the ILM step fails because the alias has no index flagged is_write_index: true, or points at more than one. Resolution:
- Inspect routing with
GET _cat/aliases/logs-production?v&h=alias,index,is_write_index. - Reassign the write flag explicitly to the newest backing index:
POST /_aliases { "actions": [ { "add": { "index": "logs-production-000003", "alias": "logs-production", "is_write_index": true } } ] } - Confirm the template still sets
index.lifecycle.rollover_aliasso future indices inherit the alias.
Index stuck in the hot phase despite exceeding thresholds
Symptom: _ilm/explain shows the index in hot/rollover well past its max_age or size, with a step that never advances. Resolution:
- Read the failure detail:
GET logs-production-000002/_ilm/explainand look forstep_info,failed_step, orwaiting_for_snapshot. - Remember ILM polls every
indices.lifecycle.poll_interval(default10m) — a delay under ten minutes is expected, not a fault. - After fixing the root cause (usually disk watermarks or pending shard relocation), re-run the failed step with
POST /logs-production-000002/_ilm/retry.
max_primary_shard_size appears ignored
Symptom: shards grow past the configured cap without rolling. Resolution:
- Confirm the condition is on the rollover action, not misplaced in a later phase.
- Check that
index.routing.allocation.total_shards_per_nodeis not artificially capping new shard creation on the target tier. - Watch
GET _nodes/stats/breaker— heavy segment merging near a full primary can trip theparentcircuit breaker and stall the step that would otherwise roll the index.
security_exception from an automation pipeline
Symptom: the CI/CD service account fails to apply the policy or create the bootstrap index. Resolution:
- Grant
manage_ilmandmanage_index_templatescluster privileges, plusmanageon the index pattern. - Validate the token’s effective roles with
GET _security/_authenticate. - Keep authoring and application privileges separate, per the role matrix in Securing ILM Policies with RBAC.
FAQ
Do rollover conditions use AND or OR logic?
OR. Elasticsearch evaluates every condition on each poll and rolls the index as soon as the first one is satisfied. That is exactly why a production policy pairs max_primary_shard_size with max_age: size fires first on busy days to cap shard growth, and age catches quiet periods so an index never lingers indefinitely. Defining a single condition leaves one failure mode uncovered.
Why does the write alias need `is_write_index: true` on exactly one index?
Rollover advances a pointer, and that pointer must be unambiguous. If zero indices carry the write flag, ingestion fails with no write index is defined; if more than one does, it fails with has more than one write index. ILM maintains the single-write-index invariant automatically when it rolls, but a manual alias edit or a bad bootstrap can fracture it. Verify with GET _cat/aliases/<alias>?v before and after any manual intervention.
What is the difference between `max_size` and `max_primary_shard_size`?
max_size measures the total store of the index including replicas, so the threshold you set drifts as replica counts change and does not map cleanly to any single shard. max_primary_shard_size measures the largest individual primary shard, which is the value that actually governs Lucene merge cost, query latency, and recovery time. max_size is deprecated; use max_primary_shard_size and anchor it near 50 GB.
How soon after a threshold is crossed does rollover happen?
Not instantly. ILM re-evaluates the state machine every indices.lifecycle.poll_interval, which defaults to 10m. An index that crosses max_primary_shard_size at minute one will roll at the next poll, so a lag of up to ten minutes is normal. Shorten the interval in test clusters to observe behaviour faster, but never set it below what the deployment can service without adding master-node load. For an immediate cut, call POST <alias>/_rollover explicitly.
Should I set rollover conditions per tenant in a shared cluster?
Yes. Overlapping conditions across tenants on shared hot nodes cause cross-tenant shard contention and make one noisy tenant’s ingest spike everyone’s problem. Give each tenant its own alias and an index template with a distinct index.lifecycle.name, so thresholds are scoped and no policy collision occurs. The full namespace-isolation pattern is covered in setting up ILM for multi-tenant log analytics.
Related
- Understanding hot-warm-cold architecture — where an index goes after it rolls off the hot tier, and the node roles that route it there.
- Configuring rollover based on max primary shard size — the shard-count-versus-size-cap interaction in detail.
- Building custom ILM policies via the API — authoring the policy-and-template pair that carries this rollover action.
- Securing ILM policies with RBAC — the privileges an automation account needs to apply rollover policies safely.
- Setting up ILM for multi-tenant log analytics — scoping per-tenant rollover conditions without policy collision.
← Back to ILM Architecture & Fundamentals