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.

Rollover: OR-combined conditions cut a new write index behind the aliasThree rollover conditions are OR-combined: max_primary_shard_size at least 50gb, max_age at least 1d, and max_docs at least 100M. Their outputs converge on an OR gate labelled "any 1". The gate output triggers a Rollover event that creates the next backing index, flips the write flag, and freezes the previous one. Below, the write alias logs-production points ingestion clients at exactly one index. The rollover event spawns logs-production-000002 and sets is_write_index:true on it, while logs-production-000001 becomes append-closed and immutable, handed to the warm, cold and delete phases.Rollover conditionsOR-combined · first to firemax_primary_shard_size ≥ 50gbmax_age ≥ 1dmax_docs ≥ 100MORany 1Rollover eventcreate next index · flip write flag · freeze previousspawnsWrite aliaslogs-productionclients write here onlylogs-production-000001append-closed · immutable→ warm / cold / deleterolllogs-production-000002is_write_index: truecurrent write targetalias tracks is_write_index:true

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:

  1. Shard size stability. max_primary_shard_size is 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.
  2. Predictable index boundaries. max_age guarantees 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.
  3. Runaway protection. max_docs acts 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_age in later phases is measured from the rollover event, not index creation. A warm phase with min_age: 2d starts its clock the moment the index rolls over, so the age trigger and the phase timers interact — a slow-filling index that hits max_age early begins its warm countdown early too.
  • max_size is deprecated in favour of max_primary_shard_size. The older max_size measured total store including replicas, which made shard sizing unpredictable as replica counts changed. Prefer max_primary_shard_size so the threshold tracks the resource that actually governs merge and query cost.
  • Always define at least two conditions. A policy with only max_age will let a traffic surge produce a 300 GB shard; one with only max_primary_shard_size will 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_index

Exactly 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:desc

When 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:

  1. 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_size at the top of that band.
  2. Derive max_age from ingest, then round to an operational boundary. If a shard reaches 50 GB in roughly 18 hours at peak, a max_age of 1d means size usually fires first on busy days and age catches the quiet ones. Aligning max_age to a calendar boundary (daily, weekly) keeps index names and retention windows legible.
  3. Keep max_docs as 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.
  4. Account for shard count. max_primary_shard_size measures the largest single primary, so an index with number_of_shards: 3 rolls 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:

  1. Inspect routing with GET _cat/aliases/logs-production?v&h=alias,index,is_write_index.
  2. 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 } }
      ]
    }
  3. Confirm the template still sets index.lifecycle.rollover_alias so 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:

  1. Read the failure detail: GET logs-production-000002/_ilm/explain and look for step_info, failed_step, or waiting_for_snapshot.
  2. Remember ILM polls every indices.lifecycle.poll_interval (default 10m) — a delay under ten minutes is expected, not a fault.
  3. 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:

  1. Confirm the condition is on the rollover action, not misplaced in a later phase.
  2. Check that index.routing.allocation.total_shards_per_node is not artificially capping new shard creation on the target tier.
  3. Watch GET _nodes/stats/breaker — heavy segment merging near a full primary can trip the parent circuit 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:

  1. Grant manage_ilm and manage_index_templates cluster privileges, plus manage on the index pattern.
  2. Validate the token’s effective roles with GET _security/_authenticate.
  3. 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.

← Back to ILM Architecture & Fundamentals