08 — Operations, integration and best practices

You already know how to query and structure data in Elasticsearch. This chapter closes the loop: how to keep a cluster healthy, integrate cleanly, and not make the mistakes that cause 2am incidents. Read it as a developer-facing operations literacy guide — enough to diagnose, integrate safely, and know when to escalate.

Goals for this chapter

By the end, you should be able to:


8.1 Cluster health — green, yellow, red, and what to do

What each color means

Color Meaning Traffic impact
Green All primary and replica shards are allocated None — fully healthy
Yellow All primaries allocated; some replicas unassigned Reads and writes still work, but resilience is reduced (one node loss may cause red)
Red Some primary shards unassigned Data for those shards is unavailable; queries/writes to affected indices fail

Yellow is a warning, not an emergency. Red requires immediate attention.

Diagnostics — start here

GET /_cluster/health
GET /_cat/health?v

Spot unassigned shards:

GET /_cat/shards?v&h=index,shard,prirep,state,node,unassigned.reason

Filter to unassigned only (pipe in terminal):

curl -s "http://localhost:9200/_cat/shards?v" | grep UNASSIGNED

When you see UNASSIGNED, explain one shard:

GET /_cluster/allocation/explain

This returns the single most important diagnostic — the exact reason Elasticsearch cannot place the shard. Read it literally; it tells you which constraint is blocking.

Safe actions per color

Yellow

Root cause Safe action
Single-node cluster, number_of_replicas: 1 Set replicas to 0 for those indices — replicas cannot be placed on the only node
Disk watermark reached on some nodes Free disk, expand capacity, or rebalance data; do not fight the watermarks
Not enough nodes for awareness/zone rules Add a node or relax rules intentionally
Node recently left; replicas recovering Wait and monitor; watch initializing_shards count down
PUT /shop-orders-demo/_settings
{
  "index": {
    "number_of_replicas": 0
  }
}

Red

Priority order: 1. Identify which indices have unassigned primaries (_cat/shards?v). 2. Run _cluster/allocation/explain — read the exact blocker. 3. Fix the blocker: - Bring back the missing node (primaries lived there, no replica existed). - Free disk / expand if watermark is blocking. - Fix allocation filters or version compatibility if during an upgrade. 4. If data is gone: restore from snapshot or rebuild from source of truth.

Pitfall: do not randomly delete indices or force-allocate shards. Forced allocation can cause data loss if a shard copy is stale. Deleting the wrong index is unrecoverable without a snapshot. Always understand the cause before acting.

The disk watermark chain

Elasticsearch applies three thresholds (defaults, configurable):

Check current settings and node allocation:

GET /_cat/allocation?v
GET /_cluster/settings?include_defaults=true&filter_path=**watermark**

To clear a read-only block after creating headroom:

PUT /_all/_settings
{
  "index.blocks.read_only_allow_delete": null
}

Clear the block only after you have actual disk headroom. Clearing it on a still-full disk just re-triggers the block on the next write.


8.2 Monitoring — what to watch and where to look

The _cat family — quick cluster overview

GET /_cat/indices?v&s=store.size:desc

Lists all indices sorted by disk size — useful for spotting unexpectedly large indices.

GET /_cat/nodes?v&h=name,heap.percent,ram.percent,cpu,load_1m,node.role
GET /_cat/thread_pool?v&h=node_name,name,active,queue,rejected

The thread_pool view is high-value: rejections mean Elasticsearch is dropping work.

_nodes/stats and _stats — deeper numbers

GET /_nodes/stats

For a developer, the key sub-sections to check:

Section What to look at
indices.docs Total doc count and deleted docs (deleted docs cause merge overhead)
indices.store.size_in_bytes Disk consumed by shards
indices.indexing index_total, index_time_in_millis → indexing rate and average latency
indices.search query_total, query_time_in_millis, fetch_total, fetch_time_in_millis
indices.merges current, total_time_in_millis → merge activity and pressure
indices.refresh total_time_in_millis → how expensive refreshes are
indices.query_cache / request_cache Hit rate (low hit rate with high latency = no caching benefit)
jvm.mem.heap_used_percent Over 75% sustained = concerning; over 85% = act
thread_pool.search.rejected / thread_pool.write.rejected Any rejections → capacity problem

Per-index stats:

GET /shop-orders-demo/_stats

Kibana Stack Monitoring

Kibana’s Stack Monitoring UI (sidebar > Management > Stack Monitoring) presents the same data visually:

Use it for historical trends and to correlate signals across time. Dev Tools _cat APIs are faster for point-in-time triage.

The signals that predict trouble

These five signals cover the vast majority of production incidents:

Signal Threshold to watch What it predicts
JVM heap % > 75–85% sustained GC storms, circuit breaker trips, OOM
Disk usage Approaching 85% watermark Shard allocation failures, eventual read-only
Search/write thread pool rejections Any rejections Requests being dropped, latency spikes
Growing pending tasks Queue that does not drain Master bottleneck, allocation stall
Unassigned shards Count > 0 Resilience loss (yellow) or data loss (red)

Every alert should have an associated action — who checks what, in which order. An alert without a runbook is noise. Pair each signal with the relevant _cat or _nodes API call as a first step.


8.3 Snapshots and restore — backup strategies

Snapshots are the only supported backup mechanism

Elasticsearch snapshots are incremental and segment-based: the first snapshot captures all segments; subsequent snapshots only copy new or changed segments. This makes them efficient for daily/hourly schedules.

Snapshots capture: - Index data (shards → segments) - Index metadata (mappings, settings) - Optionally: cluster state (templates, ILM policies, etc.)

Know your source of truth. Elasticsearch is frequently a derived store — data originally lives in a database or event stream and is indexed into ES for search. In that case, a full data rebuild from source may be more appropriate than a restore, especially after mapping changes. Snapshots are still critical for disaster recovery and for data that only exists in ES.

Step 1: Register a repository

File system (useful for local or NFS mounts):

PUT /_snapshot/my-repo
{
  "type": "fs",
  "settings": {
    "location": "/mnt/snapshots"
  }
}

S3-compatible (common in production):

PUT /_snapshot/my-repo
{
  "type": "s3",
  "settings": {
    "bucket": "my-es-snapshots",
    "region": "eu-west-1"
  }
}

Step 2: Take a snapshot

PUT /_snapshot/my-repo/snap-shop-orders-2026-05-30?wait_for_completion=true
{
  "indices": "shop-orders-demo",
  "include_global_state": false
}

wait_for_completion=true blocks until done (fine for ad-hoc; use async for automation).

Step 3: List and verify

GET /_snapshot/my-repo/_all
GET /_snapshot/my-repo/snap-shop-orders-2026-05-30

Check state: SUCCESS and shards.failed: 0.

Step 4: Restore

Restore to a renamed index to avoid clobbering the live index:

POST /_snapshot/my-repo/snap-shop-orders-2026-05-30/_restore
{
  "indices": "shop-orders-demo",
  "include_global_state": false,
  "rename_pattern": "(.+)",
  "rename_replacement": "restored-$1"
}

This restores shop-orders-demo as restored-shop-orders-demo. Validate doc counts and run a couple of queries before treating the restore as good.

Snapshot Lifecycle Management (SLM)

SLM schedules snapshots automatically. Create a policy:

PUT /_slm/policy/daily-shop-orders
{
  "schedule": "0 30 1 * * ?",
  "name": "<shop-orders-{now/d}>",
  "repository": "my-repo",
  "config": {
    "indices": ["shop-orders-*"],
    "include_global_state": false
  },
  "retention": {
    "expire_after": "30d",
    "min_count": 5,
    "max_count": 50
  }
}

Trigger it manually to test:

POST /_slm/policy/daily-shop-orders/_execute

Check SLM status:

GET /_slm/policy/daily-shop-orders
GET /_slm/stats

Snapshot best practices


8.4 Ingest pipelines and Logstash/Beats — when to use what

Ingest pipelines — ES-native, runs on ingest nodes

Ingest pipelines process documents inside Elasticsearch before they are written to shards. They run on ingest-role nodes (or any node in small clusters).

Common processors:

Processor What it does
grok Pattern extraction from unstructured text (log lines)
dissect Faster delimiter-based field splitting (prefer over grok when possible)
date Parse a timestamp string into a date field
convert String → long, double, boolean, etc.
rename Rename a field
remove Drop unwanted fields
set Set a field value (constant or from another field)
lowercase / uppercase Normalize string case
geoip Enrich an IP field with city/country/coordinates
user_agent Parse a user-agent string into structured fields
script Arbitrary Painless logic (powerful; use sparingly)

Register a pipeline

PUT /_ingest/pipeline/normalize-orders-v1
{
  "description": "Normalize shop-orders-demo documents on ingest",
  "processors": [
    {
      "set": {
        "field": "@timestamp",
        "value": "{{created_at}}",
        "ignore_failure": true
      }
    },
    {
      "date": {
        "field": "@timestamp",
        "formats": ["ISO8601", "yyyy-MM-dd'T'HH:mm:ssZ"],
        "ignore_failure": true
      }
    },
    {
      "lowercase": {
        "field": "customer.name",
        "ignore_missing": true
      }
    },
    {
      "convert": {
        "field": "amount",
        "type": "double",
        "ignore_missing": true,
        "ignore_failure": true
      }
    },
    {
      "remove": {
        "field": ["internal_debug_field"],
        "ignore_missing": true
      }
    }
  ],
  "on_failure": [
    {
      "set": {
        "field": "ingest.error",
        "value": "{{_ingest.on_failure_message}}"
      }
    }
  ]
}

Test with _simulate before deploying

POST /_ingest/pipeline/normalize-orders-v1/_simulate
{
  "docs": [
    {
      "_index": "shop-orders-demo",
      "_source": {
        "created_at": "2026-05-30T09:15:00Z",
        "customer": { "name": "Alice SMITH" },
        "amount": "149.99",
        "internal_debug_field": "drop-me"
      }
    }
  ]
}

Inspect the _simulate response’s doc._source to verify the transformation before attaching it to an index.

Attach to an index

Per-request (one-off or testing):

POST /shop-orders-demo/_doc?pipeline=normalize-orders-v1
{ "created_at": "2026-05-30T09:15:00Z", "amount": "149.99" }

As the index default (applies to all documents indexed to that index):

PUT /shop-orders-demo/_settings
{
  "index.default_pipeline": "normalize-orders-v1"
}

Or set it in the index template so every new index in the pattern picks it up automatically.

Decision guide: pipeline vs Beats vs Logstash

Scenario Best choice
App indexes directly into ES; light transformation needed (normalize, convert, enrich) Ingest pipeline — ES-native, zero extra infrastructure
Collecting logs/metrics from servers, containers, or services; minimal transformation Beats (Filebeat, Metricbeat) — lightweight agents, designed to ship and hand off
Complex multi-step parsing, multiple input sources or sinks, buffering, fan-out Logstash — full pipeline engine with persistent queues
Heavy enrichment that would be expensive on ingest nodes (machine learning, heavy scripts) Logstash or pre-processing before indexing

Comparison table:

Ingest pipeline Beats Logstash
Runs inside Elasticsearch Agent on source host Separate JVM process
Infrastructure None extra Lightweight agents Dedicated nodes/pods
Transformation power Medium (processors, scripts) Minimal (some modules) High (full plugin ecosystem)
Buffering / durability None (synchronous) Limited Persistent queues
Multi-destination output No No (sends to ES or Logstash) Yes
Typical use Enrich/normalize at index time Collect and ship Complex ETL, fan-out

Use ingest pipelines by default for anything you can express in processors. Add Logstash only when you genuinely need its power — it adds operational complexity. Beats are for collection, not transformation.


8.5 Best practices wrap-up

This section is a concise checklist tying together the entire course. Work through it when designing a new index, reviewing an integration, or doing a health check on an existing cluster.

Mappings

Queries

Shards, ILM, and templates

Aliases for zero-downtime changes

When you need to fix a mapping or reindex: 1. Create the new index with the correct mapping. 2. Reindex from old to new. 3. Flip the alias atomically to point to the new index. 4. Applications pointing at the alias see no interruption.

POST /_aliases
{
  "actions": [
    { "remove": { "index": "shop-orders-demo-v1", "alias": "shop-orders" } },
    { "add":    { "index": "shop-orders-demo-v2", "alias": "shop-orders" } }
  ]
}

Elasticsearch as a derived store

Integration hygiene

# Check for per-document errors in a bulk response
# errors: true in the response body means at least one item failed
# Always check "items[].index.error" when errors is true

Monitor heap, disk, and rejections

Safety habits


Labs

Lab 1 — Cluster health triage

  1. Run GET /_cluster/health, GET /_cat/shards?v, GET /_cat/nodes?v.
  2. Identify any UNASSIGNED shards and run GET /_cluster/allocation/explain.
  3. Reproduce a yellow state: create an index with number_of_replicas: 1 on a single-node cluster, then fix it by setting replicas to 0.
  4. Check GET /_cat/thread_pool?v&h=node_name,name,active,queue,rejected and interpret any non-zero rejected counts.

Takeaway: five API calls give you a near-complete picture of cluster health. The allocation explain is the single most useful diagnostic.


Lab 2 — Snapshot and restore drill

  1. Register a filesystem repository: PUT /_snapshot/lab-repo with a writable location.
  2. Take a snapshot of shop-orders-demo: PUT /_snapshot/lab-repo/snap-lab-1?wait_for_completion=true.
  3. Verify: GET /_snapshot/lab-repo/snap-lab-1 — confirm state: SUCCESS.
  4. Restore with rename: restore shop-orders-demo as restored-shop-orders-demo using rename_pattern/rename_replacement.
  5. Run GET /restored-shop-orders-demo/_count and compare to the original. Run a sample query to confirm data integrity.
  6. Set up an SLM policy with PUT /_slm/policy/lab-daily and execute it manually.

Takeaway: “backup” only exists if you have practiced the restore. Rename-on-restore prevents clobbering live data.


Lab 3 — Build and test an ingest pipeline

  1. Create a pipeline normalize-orders-v1 with these processors on shop-orders-demo documents:
    • set to copy created_at into @timestamp
    • date to parse @timestamp into a proper date
    • lowercase on customer.name
    • convert to ensure amount is double
  2. Test it with POST /_ingest/pipeline/normalize-orders-v1/_simulate using a sample document.
  3. Inspect the simulated output: confirm @timestamp is parsed, customer.name is lowercase, amount is numeric.
  4. Attach the pipeline as index.default_pipeline on shop-orders-demo.
  5. Index a new test document without specifying the pipeline explicitly — confirm the pipeline ran.

Takeaway: always _simulate before deploying. on_failure handlers ensure bad documents are visible rather than silently lost.


Lab 4 — Monitoring signals baseline

  1. Run GET /_nodes/stats and find: heap used %, search query total and time, write rejections.
  2. Run GET /_cat/indices?v&s=store.size:desc — identify the three largest indices.
  3. Run GET /shop-orders-demo/_stats and locate: doc count, store size, indexing latency, search latency, request cache hit/miss ratio.
  4. Open Kibana Stack Monitoring and correlate the same node metrics in the UI.
  5. Identify which of the five “signals that predict trouble” (heap, disk, rejections, pending tasks, unassigned shards) are non-zero in your cluster and note what action each would warrant.

Takeaway: you can read the state of a cluster in under five minutes. Monitoring is only useful if you know what action follows each signal.


Summary