ISM Policy Actions Comparison

OpenSearch Index State Management gives you seven distinct actions — rollover, force_merge, shrink, allocation, snapshot, close/open, and delete — and the single most common policy design mistake is reaching for the wrong one, or firing an expensive one in the wrong tier. A force_merge on an index that is still ingesting burns hot-node I/O for nothing; a shrink on a follower index fails outright; a delete placed one transition too early destroys data an auditor still needs. This page is a decision reference: it compares every ISM action across cost, timing, preconditions, and reversibility, gives you a symptom-to-action decision flow, and shows an opensearch-py helper that reports which actions each managed index has actually run. It sits under OpenSearch ISM Architecture & Fundamentals and assumes you already understand the index lifecycle basics that these actions operate within.

The problem: the same lifecycle, seven levers

A managed index advances through named states, and each state runs an ordered list of actions before its transition conditions are evaluated. The actions are not interchangeable. Some rewrite the physical Lucene layout (force_merge, shrink), some only move pointers (rollover, allocation), some reach outside the OpenSearch cluster (snapshot), and one is terminal and irreversible (delete). Choosing correctly is a function of three questions: what physical change am I trying to make, what does the index need to look like before the action can run, and how much I/O will it cost on the tier that owns it. The comparison table below answers the first and third; the preconditions column answers the second.

The stakes are concrete. Actions execute on the node that currently holds the primary shards, so an action’s I/O cost lands on that tier’s hardware — the compute-heavy hot tier and the storage-heavy cold tier in the hot-warm-cold tier design have very different tolerances for a segment-rewriting merge. Fire the expensive actions where the hardware and the timing are right, and the lifecycle is cheap; fire them blindly and you get I/O storms, stuck transitions, and read-only indices.

Action-to-tier fit reference

Before the full comparison, this table aligns each action with the tier where it normally belongs and the hardware profile that tier runs on. It is the physical companion to the node role allocation model — an action is only “cheap” when the tier that runs it has the I/O budget for it.

Tier Storage profile vCPU : RAM Routing attr Actions that belong here Actions to avoid here
Hot NVMe SSD, high IOPS 1 : 4 data: hot rollover force_merge, shrink (steal ingest I/O)
Warm SATA SSD 1 : 8 data: warm allocation, shrink, force_merge rollover (writes have stopped)
Cold HDD / large SSD 1 : 16 data: cold allocation, snapshot, close force_merge (HDD merge is brutal)
Frozen Object storage snapshots 1 : 16 data: frozen snapshot, delete every write action (terminal tier)

The rule that falls out of this table: mutate the shape of an index (force_merge, shrink) exactly once, on the warm tier, after writes have stopped and before it settles onto slow cold-tier disks. Everything else is either a pointer move or an out-of-cluster copy.

The full comparison

This is the core of the page. Every ISM action, compared across what it does, when it should fire, its I/O cost, its hard preconditions, whether it can be undone, and the tier it typically runs in.

Action What it does When it fires Cost / I/O impact Hard preconditions Reversible? Typical tier
rollover Points the write alias at a fresh backing index; freezes the old one Write index hits size/age/doc-count ceiling Cheap — creates one empty index, repoints an alias rollover_alias set; source is is_write_index: true No (old index stays; you just stop writing to it) Hot
force_merge Merges segments down to max_num_segments After writes stop, before long-term storage High — rewrites every segment; heavy CPU + disk read/write Index should be read-only / no active ingest No (segments are gone) Warm
shrink Reduces primary shard count into a new index (num_new_shards) Over-sharded index whose write phase is done High — copies all segments into the new shard layout All primaries on one node; source read-only; target shard count divides source No (new index; source deleted after) Warm
allocation Sets routing filters so shards relocate to a tier On each tier transition (hot→warm→cold) Medium — network shard relocation, no rewrite Target nodes carry the routing attribute; capacity below high watermark Yes (change the filter back) Any transition
snapshot Copies the index to a registered snapshot repository Retention/archival point, or before a destructive action Medium–high — reads the whole index, writes to object store Snapshot repository registered; repo reachable Yes (restore from snapshot) Cold / frozen
close Closes the index: releases heap/file handles, blocks read+write Retention hold where the data must survive but need not be queryable Low — flushes then unloads shards None; reopening re-reads from disk Yes (open) Cold
open Reopens a closed index for search Ad-hoc query need against closed data Low–medium — reloads shards into memory Index is currently closed Yes (close) Cold
delete Permanently removes the index and its data End of retention, after snapshot if archival is required Cheap — frees storage immediately None enforced by ISM — that is the danger No Terminal

Read the reversibility column carefully. allocation, snapshot, and close/open are round-trips — a mistake costs you a relocation or a restore. rollover, force_merge, and shrink are one-way transforms of physical layout, but the data survives. delete is the only action that destroys data, which is why it belongs in its own terminal state gated on the longest possible age condition and, for anything auditable, placed strictly after a snapshot.

Which action for which symptom

Policy design usually starts from a symptom — “shards are too big”, “the cold tier is out of disk”, “queries are slow on old indices” — not from an action name. This decision flow maps the common symptoms and goals onto the action that resolves each one.

Symptom-to-action decision flow for ISM actions A branching decision tree. The root asks what you are trying to change about the index. Branch one: the index is still taking writes and has grown too large, so rollover. Branch two: writes have stopped and there are too many segments, so force_merge. Branch three: writes have stopped and there are too many shards, so shrink. Branch four: the shards are on the wrong tier, so allocation. Branch five: you need a copy outside the OpenSearch cluster, so snapshot. Branch six: the data must survive but need not be searchable, so close. Branch seven: the data is past retention, so delete, but only after a snapshot if archival is required. What must change about this index? Match the symptom → run the matching action still writing, index too big writes done, many segments writes done, too many shards wrong tier / wrong hardware need copy off-cluster keep data, not queryable past retention rollover force_merge shrink allocation snapshot close delete archival required? snapshot first delete (safe)

Notice that delete never stands alone in the flow: when archival matters, the path routes through snapshot first so the terminal action can never destroy the only copy. That ordering — an expensive-but-reversible action guarding a cheap-but-irreversible one — is the recurring shape of a safe policy.

Where each action fires in the lifecycle

The comparison abstracts away sequence, but the actions only make sense in order. This state machine shows a representative policy and pins each action to the state that runs it, so you can see why force_merge and shrink sit in warm (after rollover has frozen the index) and why delete sits alone behind the longest age gate.

ISM lifecycle states annotated with their actions A left-to-right state diagram. Start goes to hot which runs the rollover action; at min_index_age seven days it moves to warm, which runs shrink, then force_merge, then allocation; at thirty days it moves to cold, which runs allocation and snapshot; at ninety days it moves to delete, which runs the delete action and reaches the end state. 7d 30d 90d hot rollover warm shrink·force_merge·alloc cold allocation·snapshot delete delete Action order within a state is significant: shrink requires the index read-only, so it runs before force_merge, which runs before the tier relocation. The write-mutating actions (rollover, shrink, force_merge) all complete before the index reaches cold storage.

1. rollover — end the write phase cheaply

rollover is the only action that operates on the alias, not the backing index, and it is the cheapest of the shape-changing operations because it creates one empty index and repoints a pointer. It must run in hot, and it requires the write index to be bootstrapped correctly. The deeper trigger mechanics live in Rollover Trigger Configuration.

JSON
{
  "rollover": {
    "min_index_age": "1d",
    "min_primary_shard_size": "50gb",
    "min_doc_count": 200000000
  }
}

Any one condition being met triggers the roll (conditions are OR-ed). Prefer min_primary_shard_size over min_size — it bounds the per-shard recovery cost, which is what actually hurts you on a node restart.

2. force_merge — reclaim segment overhead

force_merge rewrites the index down to max_num_segments segments. Fewer segments mean less per-segment memory and faster queries, but the merge reads and rewrites every byte, so it is the single most I/O-expensive action per gigabyte. Never run it while the index is still ingesting — the merge fights the write path and the freshly written segments immediately re-fragment.

JSON
{
  "force_merge": {
    "max_num_segments": 1
  }
}

Merging to a single segment is optimal for read-only warm indices; it is a mistake for anything still receiving writes.

3. shrink — collapse an over-sharded index

shrink produces a new index with fewer primary shards. It is the correct fix when rollover sizing left you with many small shards whose per-shard overhead dominates. Its preconditions are the strictest of any action: every primary must sit on one node, the source must be read-only, and the new shard count must divide the old.

JSON
{
  "shrink": {
    "num_new_shards": 1,
    "force_unsafe": false
  }
}

Because both shrink and force_merge are warm-tier layout rewrites with a mandatory ordering, the decision of which to run first — and when to run both — is detailed in choosing between force_merge and shrink for warm indices.

4. allocation — move shards between tiers

allocation sets routing filters that make the allocator relocate shards onto the target tier. It is a pointer-and-relocation operation, not a rewrite, and it is fully reversible: set the filter back and the shards move back. It is the workhorse of the data tier routing patterns that move data hot→warm→cold.

JSON
{
  "allocation": {
    "require": { "data": "warm" },
    "wait_for": true
  }
}

Set wait_for: false if you do not want the ISM worker to block on relocation completing — pair that with a fallback strategy so a full target tier does not silently strand shards.

5. snapshot, close, and delete — the retention endgame

These three actions govern what happens at end-of-life. snapshot copies the index to a registered repository (reversible via restore); close unloads it from memory while keeping it on disk (reversible via open); delete frees the storage permanently (never reversible).

JSON
{
  "snapshot": {
    "repository": "s3-archive",
    "snapshot": "-lifecycle"
  }
}

The safe pattern is always the same: snapshot into a state of its own, transition to a delete state gated on the longest age condition, and let the snapshot be the durable copy the delete is allowed to destroy.

Production automation: which actions has each index run?

The operational question this whole page serves is “which of these actions has ISM actually executed against my managed indices, and did any fail?”. The _plugins/_ism/explain endpoint answers it, but its raw output is verbose. The opensearch-py helper below queries explain, extracts the current state and last action per index, flags failures, and prints a compact per-action audit. It uses structured logging and narrow exception handling so it is safe to schedule. Scope its service account per Security & Access Boundaries to read-only ISM explain.

Python
import logging
from collections import Counter
from opensearchpy import OpenSearch, exceptions

logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)

# Actions we expect to see across a healthy hot-warm-cold lifecycle.
KNOWN_ACTIONS = {"rollover", "force_merge", "shrink", "allocation", "snapshot", "close", "delete"}

class ActionAuditor:
    def __init__(self, client: OpenSearch):
        self.client = client

    def explain(self, pattern: str) -> dict:
        """Fetch the ISM explain document for every index matching the pattern."""
        try:
            return self.client.transport.perform_request(
                "GET", f"/_plugins/_ism/explain/{pattern}"
            )
        except exceptions.ConnectionError:
            logger.error("Cannot reach OpenSearch cluster.")
            return {}
        except exceptions.TransportError as exc:
            logger.error("ISM explain failed for %s: %s", pattern, exc)
            return {}

    def audit(self, pattern: str = "logs-*") -> Counter:
        """Report the current state and last action per managed index; flag failures."""
        doc = self.explain(pattern)
        run_counts: Counter = Counter()
        for index, info in doc.items():
            if not isinstance(info, dict) or "state" not in info:
                continue  # skip the 'total_managed_indices' summary key
            state = info.get("state", {}).get("name", "unknown")
            action = info.get("action", {}).get("name")
            failed = info.get("action", {}).get("failed", False)
            retries = info.get("retry_info", {}).get("consumed_retries", 0)
            if action in KNOWN_ACTIONS:
                run_counts[action] += 1
            if failed:
                logger.warning(
                    "FAILED action=%s on %s (state=%s, retries=%d)",
                    action, index, state, retries,
                )
            else:
                logger.info("%s -> state=%s last_action=%s", index, state, action)
        logger.info("Action run distribution: %s", dict(run_counts))
        return run_counts

if __name__ == "__main__":
    client = OpenSearch(
        hosts=[{"host": "localhost", "port": 9200}],
        http_auth=("admin", "admin"),
        use_ssl=True,
        verify_certs=True,
    )
    ActionAuditor(client).audit("logs-*")

The run_counts Counter gives you a fleet-wide picture: a spike in force_merge on the hot tier, or a delete count climbing faster than your snapshot count, is exactly the kind of misapplied-action signal the troubleshooting section below chases down.

Operational guardrails

Actions interact with cluster-level settings that decide whether they can even run. The expensive shape-changing actions need throttling headroom; the tier-moving actions need watermark headroom. Set these before turning on an aggressive policy.

merge costSindex×nsegmentsntarget\text{merge cost} \approx S_{index} \times \frac{n_{segments}}{n_{target}}

The I/O a force_merge performs scales with index size SindexS_{index} and the ratio of current to target segments — which is why it is cheap on a freshly-rolled index and ruinous on a large, heavily-updated one.

Setting Recommended value Purpose
indices.recovery.max_bytes_per_sec 80mb (HDD) / 250mb (NVMe) Throttle the shard copy behind shrink and allocation
cluster.routing.allocation.disk.watermark.high 88% allocation cannot place shards on a tier past this
cluster.routing.allocation.disk.watermark.flood_stage 93% Read-only lock — a force_merge here fails mid-merge
plugins.index_state_management.job_interval 5m How often ISM evaluates the next action
ISM retry block count: 3, backoff: exponential Absorb transient action failures before alerting
cluster.max_shards_per_node tier-specific rollover fails when the new index cannot allocate

Troubleshooting misapplied actions

The failures below are all the right action in the wrong place or the wrong action for the symptom — the class of problem this comparison exists to prevent.

Symptom Root cause Fix command
force_merge action stuck in WAITING Index still ingesting; merge cannot converge against active writes POST _plugins/_ism/change_policy/<idx> to a state that rolls over first, then merges
shrink fails not all shards on one node Primaries spread across nodes; precondition unmet PUT <idx>/_settings {"index.routing.allocation.require._name":"<node>"} then retry
delete fired before archival snapshot delete state reachable without a preceding snapshot state Re-sequence the policy so snapshot transitions into delete; POST _plugins/_ism/change_policy/<idx>
allocation action never completes Target tier past the high watermark; no capacity GET _cat/allocation?v then free disk or lift data-tier-routing-patterns fallback
rollover action failed on a follower Write actions are blocked on CCR follower indices Manage the lifecycle on the leader; attach a read-safe follower policy instead
Action shows failed: true in explain Transient error exhausted retries POST _plugins/_ism/retry/<idx> after fixing the underlying condition

For the follower case specifically, remember that follower indices are read-only: rollover, force_merge, shrink, and open/close all fail against them, so a CCR topology must run its write-mutating actions on the leader and let replication carry the settings across.

Frequently asked questions

Should force_merge or shrink run first when an index needs both?

shrink first, then force_merge. shrink creates a brand-new index with the reduced shard count, so any merge you did beforehand is wasted work on the discarded source. Merging the already-shrunk target means you rewrite the data exactly once. The full ordering and preconditions are covered in choosing between force_merge and shrink for warm indices.

Is close a substitute for delete when I want to save disk?

No — close keeps the index fully on disk; it only releases heap and file handles. It saves memory and CPU, not storage. Use close for a retention hold where the data must survive and might be queried again after open; use snapshot plus delete when you actually need the disk back.

Which actions are irreversible?

Only delete destroys data irrecoverably. force_merge and shrink are one-way physical transforms but keep the data. rollover, allocation, snapshot, and close/open are all reversible — you can repoint, relocate, restore, or reopen. This is why a safe policy always places snapshot before delete.

Can I run two shape-changing actions in the same state?

Yes, and it is common to list shrink then force_merge then allocation in the warm state — ISM runs a state’s actions in order. But every action must complete (or exhaust retries) before the next starts, so a stuck shrink blocks the whole state. Keep expensive actions in states with generous retry blocks.

Up: OpenSearch ISM Architecture & Fundamentals