Configuring a cluster correctly: static vs dynamic settings, persistent vs transient, discovery and cluster formation, node roles, JVM heap sizing, thread pools, and circuit breakers for a stable and optimal cluster.

Up until episode 13 we worked on a single node. But Elasticsearch's power is only felt when several nodes unite to form a cluster — and that's where configuration becomes important. Cluster misconfiguration is the most common source of production incidents: split-brain, nodes that can't join, or OOM from a wrong heap.
Episode 14 covers cluster and node configuration: static vs dynamic settings, persistent vs transient, discovery and cluster formation, node role configuration, JVM heap sizing, thread pools, and circuit breakers. This is the foundation that makes all the subsequent scaling episodes make sense.
There are two kinds of settings in Elasticsearch:
elasticsearch.yml when the node starts; cannot be changed at runtime without a restart. Examples: node.name, node.roles, path.data, cluster.name.cluster.routing.allocation.enable, index.number_of_replicas.cluster.name: my-cluster
node.name: data-1
node.roles: [master, data]
path.data: /var/lib/elasticsearch
path.logs: /var/log/elasticsearch
network.host: 0.0.0.0
discovery.seed_hosts: ["node1:9300", "node2:9300", "node3:9300"]Rule of thumb: use the default settings as much as possible; change only what's truly needed, and write static changes to the config file so they're recorded (infrastructure as code, episode 28).
Dynamic settings have two layers:
PUT /_cluster/settings{
"persistent": {
"cluster.routing.allocation.enable": "all"
},
"transient": {
"cluster.routing.allocation.disk.threshold_enabled": false
}
}Warning
Use persistent for all production changes; transient is only for quick debugging. A leftover transient can cause unexpected behavior after a restart. And remember: elasticsearch.yml remains the primary config file — the reading precedence is always: elasticsearch.yml → transient → persistent.
When a node starts, it must find other nodes and form a cluster. The discovery mechanism uses several seed hosts as an initial list:
discovery.seed_hosts: ["node1:9300", "node2:9300", "node3:9300"]
cluster.initial_master_nodes: ["master-1", "master-2", "master-3"]cluster.initial_master_nodes is only used the first time a cluster is formed (bootstrap) to elect the initial master. After that, the master is chosen through voting. The number of master-eligible nodes must be odd to avoid tied votes — we'll return to the quorum topic in episode 29.
Node roles are configured in node.roles (static):
| Role | Purpose |
|---|---|
master | Manages cluster state, routing, and master election |
data | Stores data and executes search/aggregation |
ingest | Runs ingest pipelines |
ml | Runs machine learning jobs |
remote_cluster_client | Connection for cross-cluster search (episode 22) |
In a small cluster, one node runs all roles. In large-scale production, separate them: dedicated master nodes (lightweight), dedicated data nodes (heavy), and dedicated coordinating nodes to manage search traffic without storing data. Roles determine the load and hardware profile — episode 18 goes deeper on this.
The golden rules of heap configuration:
-Xms must equal -Xmx — avoiding heap resizing at runtime.-Xms8g
-Xmx8g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=500Monitor heap usage: safe values are below 75–85%. If it keeps approaching 100%, it doesn't necessarily mean you should raise the heap — it could be a heavy aggregation query or bloated field data. Episodes 19 and 21 cover performance diagnosis.
Every node has thread pools for different kinds of work:
| Thread Pool | Handles | When full |
|---|---|---|
search | Search queries | RejectedExecutionException on overload |
write | Index operations | Write queue full |
get | Get-by-ID operations | Slow when overwhelmed |
bulk | Bulk operations | Mass write rejections |
Thread pool sizes are computed automatically from the CPU count. In almost all cases you don't need to change them — what you need to understand is how to read overload signals: a RejectedExecutionException in the logs means a thread pool is full. When that happens, the answer isn't to raise the thread pool, but to add nodes or fix queries.
The circuit breaker is the last line of defense: it monitors the estimated JVM memory used by certain operations, and rejects requests as the limit approaches — preventing the out-of-memory that would take down a node. The three main breakers:
| Breaker | Monitors |
|---|---|
indices.breaker.total | Total memory of all operations (default 95% of heap) |
fielddata | Field data cache for aggregations |
request | Per-request search memory |
GET /_nodes/stats/breakerIf you often see Data too large, data for [<breaker>]... errors in search responses, that's a signal of memory-hungry aggregations or sorting — not a reason to raise the breaker. A too-loose breaker just delays the node's death.
Tip
The best configuration principle: don't touch what doesn't need touching. Elasticsearch's defaults are tuned for the majority of workloads. Document every change you make, because a setting that "sparkles" on one workload can be poison on another.
-Xms different from -Xmx. The JVM performs unnecessary resizing — make them equal.
An even number of master-eligible nodes. Voting can deadlock (tie) — keep the count odd.
Changing dynamic settings via transient in production. Use persistent and record the change.
Raising thread pools on overload. That masks the symptom, not the cause — find the root problem (heavy queries, too few nodes).
Raising circuit breakers. Almost always wrong; fix memory usage, not its limit.
In episode 14 you mastered cluster and node configuration: static vs dynamic settings, persistent vs transient, discovery with seed hosts and bootstrap, node roles, JVM heap sizing with the half-RAM-max-32GB rule, thread pools, and circuit breakers.
Key takeaways:
discovery.seed_hosts; bootstrap once with initial_master_nodes.-Xms = -Xmx.RejectedExecutionException means overload, not a reason to raise thread pools.A correctly configured cluster can still be breached — what separates a production cluster from a learning cluster is security. In episode 15 we'll cover security fundamentals: enabling xpack.security, built-in users and realms, user and role management, authentication methods (native, LDAP/AD, SAML, API keys, service tokens), and authorization with RBAC, DLS/FLS, and role mapping. See you there!