Elasticsearch ILM Architecture & Fundamentals
Index Lifecycle Management (ILM) replaces brittle, cron-driven maintenance scripts with a declarative state machine that governs shard allocation, replication, and storage optimization across the deployment. For search engineers, log analytics teams, and DevOps operators, mastering this system means understanding how policy execution intersects with node topology, allocation awareness, and automated data migration. This guide details the architectural primitives, phase-transition mechanics, governance controls, and automation patterns required for production resilience — and shows where each deeper topic on this site fits into the whole. ILM is best treated as a distributed control plane that continuously reconciles index metadata against a policy, not as a background convenience that “just works.”
Core Architecture & Tier Topology
ILM operates by binding index metadata to a policy object that lives cluster-wide. The architecture relies on explicit node attributes (data.hot, data.warm, data.cold) and _cluster/settings allocation awareness rules. When a policy triggers a phase transition, the ILM coordinator modifies the index.routing.allocation.require.data setting, which instructs the shard allocator to relocate shards to nodes matching the target tier. Misaligned node tags, missing disk watermark thresholds, or unbalanced shard counts will stall transitions and trigger allocation deadlocks. Mapping hardware topology to policy execution paths requires strict adherence to the hot-warm-cold architecture so that write-heavy workloads stay on high-IOPS NVMe storage while archival data migrates to high-density, cost-optimized tiers.
Two attributes anchor every managed index. The first is index.lifecycle.name, which attaches the policy; the second is index.lifecycle.rollover_alias, which decouples write traffic from the physical index so a new backing index can be created without interrupting ingestion. Both are normally set in an index template rather than on individual indices, guaranteeing that every index created from a data stream or alias inherits identical lifecycle behavior. When these fields are absent, the index becomes unmanaged: it will never roll over, never migrate tiers, and never be deleted, silently accumulating shards until disk watermarks are breached.
The allocator evaluates tier requirements in real time. If a target tier lacks sufficient disk capacity or violates cluster.routing.allocation.disk.watermark.low thresholds, ILM halts progression and holds the index in a waiting state. Operators must monitor GET _cat/allocation?v and GET _cluster/allocation/explain to diagnose routing bottlenecks before they cascade into cluster-wide rebalancing storms. Node roles matter as much as attributes: a node must carry the corresponding data role (for example data_hot, data_warm, data_cold, or data_frozen) for the built-in tier-based allocation to route shards correctly. Modern deployments prefer these data-tier roles over hand-written require/include/exclude filters, because the tiers integrate with _tier_preference fallback and searchable snapshots without manual routing rules.
Key architecture settings to internalize:
index.lifecycle.name— the policy attached to the index; drives all phase actions.index.lifecycle.rollover_alias— the write alias that rollover advances to a fresh backing index.index.routing.allocation.include._tier_preference— the ordered tier list the allocator walks (for exampledata_warm,data_hot), providing graceful fallback when a preferred tier is full.cluster.routing.allocation.disk.watermark.{low,high,flood_stage}— the capacity gates that pause allocation and, at flood stage, force indices read-only.indices.lifecycle.poll_interval— how often the ILM daemon evaluates the state machine (default10m); shorten it in test clusters, never below what the deployment can service.
Phase Mechanics & State Transitions
The ILM state machine progresses through four phases: Hot, Warm, Cold, and Delete (with an optional Frozen phase between Cold and Delete for searchable-snapshot workloads). Each phase executes an ordered sequence of actions such as rollover, shrink, forcemerge, searchable_snapshot, and delete. The Hot phase is the only phase that permits write operations; every later phase operates on indices that ingestion has already rolled past.
Rollover is triggered by size, age, or document-count thresholds, but requires a write alias pointing to the active index. The conditions are OR-combined — whichever fires first advances the alias — so properly configuring index rollover conditions prevents write-blocking, ensures seamless index handoff, and maintains consistent shard sizing for predictable query performance. A common baseline targets 30–50 GB per primary shard, which keeps Lucene segment merges efficient and query fan-out bounded.
During the Warm phase, ILM typically executes shrink to reduce primary shard count and forcemerge to consolidate segments, lowering read latency and heap overhead. The Cold phase transitions indices to read-only, often reducing replica counts and routing to cold-tier nodes; the Frozen phase mounts the index from a snapshot repository as a searchable_snapshot, trading query latency for near-zero local storage. Phase progression is idempotent: if an action fails due to transient network issues or resource contention, ILM retries the failed step. Operators can inspect a stuck step via GET <index>/_ilm/explain and manually re-run it for an affected index with POST /<index>/_ilm/retry once the root cause is resolved. The state machine guarantees that partial failures do not corrupt index metadata, but persistent allocation errors or malformed policy definitions still require manual intervention.
The policy document itself is a compact, declarative object. The block below is annotated to show what each phase boundary controls:
{
"policy": {
"phases": {
"hot": {
"min_age": "0ms",
"actions": {
"rollover": {
"max_primary_shard_size": "50gb",
"max_age": "1d"
},
"set_priority": { "priority": 100 }
}
},
"warm": {
"min_age": "2d",
"actions": {
"shrink": { "number_of_shards": 1 },
"forcemerge": { "max_num_segments": 1 },
"set_priority": { "priority": 50 }
}
},
"cold": {
"min_age": "30d",
"actions": {
"allocate": { "number_of_replicas": 0 },
"readonly": {},
"set_priority": { "priority": 0 }
}
},
"delete": {
"min_age": "90d",
"actions": { "delete": {} }
}
}
}
}Two subtleties trip up teams repeatedly. First, min_age is measured from the moment the index rolls over, not from when it was created — so a slow-filling index can sit in the Hot phase far longer than its min_age suggests. Second, actions within a phase run in a fixed internal order (allocation and priority before destructive actions like shrink and forcemerge), so you cannot reorder them by rearranging the JSON. Designing these boundaries in concert with data-stream templates is the subject of the companion ILM policy design and lifecycle synchronization topic, which covers versioned rollout and drift reconciliation across environments.
Security & Policy Governance
ILM policies are cluster-scoped resources that dictate data retention windows and storage behavior. Unrestricted modification of these policies introduces severe compliance and operational risk: a single careless edit to a delete phase can erase weeks of audit data. Production clusters must enforce least-privilege access controls to prevent accidental deletion, unauthorized phase acceleration, or malicious retention bypass. Implementing role-based access control by securing ILM policies with RBAC ensures that only designated automation service accounts and senior platform engineers can mutate lifecycle definitions, while read-only roles retain visibility into execution state.
The relevant privileges are coarse but consequential. Managing policies requires the top-level cluster privilege manage_ilm; observing them needs only read_ilm. Index-level manage is additionally required to start or stop ILM on specific indices. A practical governance model grants humans read_ilm by default and reserves manage_ilm for a CI/CD service account whose API key is scoped, rotated, and audited. Every policy mutation should flow through that account so the audit log attributes changes to a pipeline run rather than an ad-hoc console session.
Governance extends to retention compliance and disaster recovery. When regulatory mandates require immutable data preservation, ILM must integrate with snapshot lifecycle management (SLM) and, where geography demands it, cross-cluster replication. In scenarios where primary storage tiers fail or become unreachable, fallback routing for data retention provides the blueprint for redirecting read traffic to snapshot-mounted indices or secondary clusters without violating retention SLAs. Policy versioning, audit logging, and automated drift detection belong in CI/CD pipelines so that lifecycle definitions stay synchronized with the infrastructure-as-code repository that owns them. Treat the policy JSON as source-controlled configuration: pull requests, review, and a deterministic apply step, never a live edit in Kibana Dev Tools.
Production-Safe Automation with Python v8+
Automating ILM deployment and monitoring requires strict adherence to the official Python client v8+ API surface. The modern client enforces explicit keyword arguments (for example put_lifecycle(name=..., policy=...)), native async support, and typed responses. The methods you will use most are client.ilm.put_lifecycle, client.ilm.explain_lifecycle, client.ilm.retry, and client.cluster.health; note there is no ilm.explain or ilm.retry_lifecycle alias, and API/HTTP failures surface as ApiError (a sibling of TransportError, not a subclass), so catch ApiError explicitly. Below is a production-grade pattern for deploying policies, verifying execution state, and safely retrying stuck transitions.
import logging
from elasticsearch import Elasticsearch
from elasticsearch.exceptions import ConnectionError, ApiError
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("ilm_automation")
def deploy_ilm_policy(client: Elasticsearch, policy_name: str, policy_body: dict) -> bool:
"""
Idempotently deploy or update an ILM policy.
Uses PUT semantics to replace existing definitions safely.
"""
try:
client.ilm.put_lifecycle(name=policy_name, policy=policy_body["policy"])
logger.info(f"Policy '{policy_name}' deployed successfully.")
return True
except ApiError as e:
logger.error(f"API Error deploying policy: {e.error} - {e.info}")
return False
except ConnectionError as e:
logger.error(f"Cluster unreachable: {e}")
return False
def audit_ilm_progress(client: Elasticsearch, index_pattern: str) -> dict:
"""
Retrieve ILM execution state for indices matching a pattern.
Filters out healthy indices to surface actionable failures.
"""
try:
response = client.ilm.explain_lifecycle(index=index_pattern)
stuck_indices = {
idx: details
for idx, details in response["indices"].items()
if details.get("step") == "ERROR"
}
if stuck_indices:
logger.warning(f"Detected {len(stuck_indices)} stuck indices.")
return stuck_indices
except ApiError as e:
logger.error(f"Failed to audit ILM state: {e.error}")
return {}
def safe_retry_transition(client: Elasticsearch, index_name: str) -> bool:
"""
Trigger ILM retry for a stuck index after confirming cluster health.
Retry is a per-index operation; it re-runs the failed step for an index
currently sitting in the ERROR step.
"""
try:
health = client.cluster.health(wait_for_status="yellow", timeout="30s")
if health["status"] in ("green", "yellow"):
client.ilm.retry(index=index_name)
logger.info(f"Retry triggered for index '{index_name}'.")
return True
logger.warning("Cluster health degraded. Deferring retry.")
return False
except ApiError as e:
logger.error(f"Retry failed: {e.error}")
return False
# Usage Example
if __name__ == "__main__":
es_client = Elasticsearch(
"https://cluster-node-01:9200",
api_key=("id", "api_key_string"),
verify_certs=True,
request_timeout=30,
max_retries=3,
retry_on_timeout=True
)
policy_def = {
"policy": {
"phases": {
"hot": {"min_age": "0ms", "actions": {"rollover": {"max_primary_shard_size": "50gb"}}},
"warm": {"min_age": "2d", "actions": {"shrink": {"number_of_shards": 1}, "forcemerge": {"max_num_segments": 1}}},
"delete": {"min_age": "90d", "actions": {"delete": {}}}
}
}
}
deploy_ilm_policy(es_client, "logs-app-prod", policy_def)
stuck = audit_ilm_progress(es_client, "logs-app-prod-*")
for stuck_index in stuck:
safe_retry_transition(es_client, stuck_index)This pattern enforces connection resilience, validates cluster health before triggering retries, and isolates failed transitions for targeted remediation. It deliberately authenticates with an API key rather than basic credentials, matching the least-privilege service account described in the governance section. For high-volume orchestration — reconciling hundreds of indices, swapping aliases, or rolling policy updates transactionally — build on these same idempotent primitives and the async client so a single slow node does not block the whole run. Always wrap policy mutations in CI/CD steps so a partial deployment during a rolling upgrade cannot leave half the fleet on a stale definition.
Monitoring & Observability
ILM executes asynchronously, so silent stalls are the default failure mode unless you instrument for them. The authoritative per-index view is GET <index>/_ilm/explain, which reports the current phase, action, step, age, and — critically — step_info when a step has entered the ERROR state. Poll it against your managed patterns and alert on any index whose step equals ERROR or whose age in a phase exceeds the expected min_age by a wide margin.
Layer allocation and capacity signals on top of the ILM view, because most stalls are really allocation problems wearing an ILM costume:
GET _cat/allocation?v— shard count and disk usage per node; watch for a tier approaching its high watermark.GET _cluster/allocation/explain— the definitive answer to “why won’t this shard move?” when a transition hangs onallocateorshrink.GET _cat/nodes?v&h=name,heap.percent,disk.used_percent,node.role— heap and disk pressure with data-tier roles, so you can confirm a target tier has capacity before a phase needs it.GET _ilm/status— whether the ILM service isRUNNINGor was leftSTOPPEDafter maintenance; a stopped daemon freezes every managed index.
The alerting signals that matter most in production are: any index in the ERROR step, ILM service not RUNNING, any data tier above its high disk watermark, and rollover lag (an active write index older or larger than its rollover condition without having rolled). Wire these into whatever your team already runs — Kibana Stack Monitoring, Prometheus via the Elasticsearch exporter, or a scheduled job built on the audit_ilm_progress helper above — and treat a persistent ERROR step as a page, not a warning.
Common Failure Modes
The table below maps the symptoms operators actually observe to their root cause and the remediation that clears them. It is intentionally scoped to ILM-specific failures rather than general cluster health.
| Symptom | Root cause | Remediation |
|---|---|---|
Index stuck in ERROR step at check-rollover-ready | Write alias missing, or is_write_index not set on the active backing index | Repair the alias so exactly one index has is_write_index: true, then POST /<index>/_ilm/retry |
Transition to warm/cold hangs on allocate | Target tier has no eligible node (missing data role or attribute), or is over the high disk watermark | Add capacity or correct node roles/_tier_preference; verify with GET _cluster/allocation/explain |
| All indices suddenly read-only, ingestion failing | Flood-stage disk watermark breached | Free disk or expand the tier, then clear index.blocks.read_only_allow_delete once usage drops below the high watermark |
shrink step never completes | Not all primaries co-located on one node, or target shard count not a factor of the source | Allocate a copy of every shard to one node first; choose number_of_shards that divides the source count |
| New reindexed index bypasses retention | Destination inherited default settings; index.lifecycle.name not applied | Set the policy on the destination template before reindex; confirm with GET <index>/_settings?filter_path=**.lifecycle |
| Nothing transitions at all after maintenance | ILM service left in STOPPED state | POST _ilm/start; confirm GET _ilm/status reports RUNNING |
| Forcemerge and snapshot contend, heap spikes | Overlapping ILM forcemerge and SLM snapshot windows competing for I/O | Stagger SLM and ILM poll_interval/schedules so heavy I/O phases do not overlap |
When integrating ILM with automated data-migration jobs, remember that a reindexed destination does not inherit a policy unless the destination template defines index.lifecycle.name. Validate attachment after every migration to prevent orphaned indices from bypassing retention controls. The end-to-end mechanics of building those safe, resumable migrations live in the automated reindexing pipelines and workflows topic.
Frequently Asked Questions
How is ILM different from the deprecated Curator tool?
Curator ran as an external, cron-scheduled process that issued imperative API calls on a fixed clock, with no awareness of allocation state. ILM is an internal declarative state machine: you describe the desired lifecycle once as a policy, and the deployment reconciles every managed index against it, pausing safely when allocation or capacity is not ready. ILM also survives node restarts and coordinates with data tiers and searchable snapshots, which Curator never could.
Why is my index still in the Hot phase long after its warm min_age?
Phase min_age is measured from the index's rollover time, not its creation time. A slow-filling index that has not rolled over yet has an effective age of zero for phase purposes. Check GET <index>/_ilm/explain for age and whether rollover has fired; if the write alias is misconfigured, rollover never happens and later phases never begin.
Can I change a policy while indices are actively managed by it?
Yes. put_lifecycle updates the policy in place and bumps its version; already-managed indices pick up the new definition at their next step evaluation, though an index mid-phase finishes its current action under the version it started. Because live edits are hard to audit, route every change through a version-controlled apply step rather than editing in the console.
What clears an index that is stuck in the ERROR step?
First read step_info from GET <index>/_ilm/explain to find the real cause — usually an allocation or alias problem. Fix that underlying condition, then run POST /<index>/_ilm/retry, which re-runs the failed step for that single index. Retrying without fixing the cause simply returns the index to ERROR.
Related
- Understanding Hot-Warm-Cold Architecture — tier topology, node roles, and allocation awareness.
- Configuring Index Rollover Conditions — size, age, and document thresholds for the Hot phase.
- Securing ILM Policies with RBAC — least-privilege control over lifecycle definitions.
- Fallback Routing for Data Retention — surviving tier failure without breaking retention SLAs.
- ILM Policy Design & Lifecycle Synchronization — versioned rollout and drift reconciliation.
- Automated Reindexing Pipelines & Workflows — ILM-safe, zero-downtime data migration.
← Back to index-lifecycle-management.org home