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:
- Read cluster health and diagnose allocation issues from the REST API
- Know which signals to watch and what they predict
- Snapshot, restore, and validate data safely
- Choose the right ingestion path (ingest pipeline vs Logstash vs Beats) and build a working pipeline
- Apply a practical best-practices checklist covering the whole course
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 UNASSIGNEDWhen 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):
- Low (85%) — stops allocating new shards to the node.
- High (90%) — starts moving shards away from the node.
- Flood stage (95%) — sets all indices on that node to read-only.
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:
- Cluster overview: health, node count, shard summary.
- Per-node views: heap %, CPU, disk, indexing/search rates.
- Index-level views: docs, store size, indexing latency.
- Logs and slow logs (if enabled).
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
_cator_nodesAPI 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
- Always restore into a separate index (or separate cluster) to validate — never overwrite a live index with an untested restore.
- Take a snapshot before any reindex, bulk update, or upgrade operation.
- Test restores on a schedule, not just when you need them. A snapshot that cannot restore is not a backup.
- Monitor SLM policy execution and alert on failures.
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
- Run
GET /_cluster/health,GET /_cat/shards?v,GET /_cat/nodes?v. - Identify any UNASSIGNED shards and run
GET /_cluster/allocation/explain. - Reproduce a yellow state: create an index with
number_of_replicas: 1on a single-node cluster, then fix it by setting replicas to 0. - Check
GET /_cat/thread_pool?v&h=node_name,name,active,queue,rejectedand 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
- Register a filesystem repository:
PUT /_snapshot/lab-repowith a writablelocation. - Take a snapshot of
shop-orders-demo:PUT /_snapshot/lab-repo/snap-lab-1?wait_for_completion=true. - Verify:
GET /_snapshot/lab-repo/snap-lab-1— confirmstate: SUCCESS. - Restore with rename: restore
shop-orders-demoasrestored-shop-orders-demousingrename_pattern/rename_replacement. - Run
GET /restored-shop-orders-demo/_countand compare to the original. Run a sample query to confirm data integrity. - Set up an SLM policy with
PUT /_slm/policy/lab-dailyand 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
- Create a pipeline
normalize-orders-v1with these processors onshop-orders-demodocuments:setto copycreated_atinto@timestampdateto parse@timestampinto a proper datelowercaseoncustomer.nameconvertto ensureamountisdouble
- Test it with
POST /_ingest/pipeline/normalize-orders-v1/_simulateusing a sample document. - Inspect the simulated output: confirm
@timestampis parsed,customer.nameis lowercase,amountis numeric. - Attach the pipeline as
index.default_pipelineonshop-orders-demo. - 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
- Run
GET /_nodes/statsand find: heap used %, search query total and time, write rejections. - Run
GET /_cat/indices?v&s=store.size:desc— identify the three largest indices. - Run
GET /shop-orders-demo/_statsand locate: doc count, store size, indexing latency, search latency, request cache hit/miss ratio. - Open Kibana Stack Monitoring and correlate the same node metrics in the UI.
- 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
- Green = all shards allocated; Yellow = replicas missing
(reduced resilience); Red = primaries missing (data
unavailable). Diagnose with
_cluster/allocation/explainbefore acting. - Do not randomly delete indices or force-allocate shards during incidents. Find the cause first.
- The five signals that predict most production incidents: heap pressure, disk approaching watermark, thread pool rejections, growing pending tasks, and unassigned shards.
- Snapshots are the only supported backup. Register a repository, automate with SLM, and validate restores on a schedule — not on the day you need them.
- Use ingest pipelines for light transformation at
index time. Test with
_simulate. Use Beats to collect and ship. Use Logstash when you need complex parsing, buffering, or multiple destinations. - Explicit mappings + index templates + aliases are the foundation of a stable, evolvable schema. Treat dynamic mapping as a risk to constrain.
- Know whether Elasticsearch is your source of truth or a derived index — it changes your recovery options and backup obligations.