Active Nerds
Module #27 · expert Article

What Are the Key Levers for etcd Performance Tuning?: Architecture & Implementation

The most impactful etcd performance levers are: NVMe/SSD disk with low fsync latency, dedicated disk for etcd WAL, `--heartbeat-interval` and `--election-tim...

15 min read+25 XPPublished 2026-05-31

Quick Answer:

The most impactful etcd performance levers are: NVMe/SSD disk with low fsync latency, dedicated disk for etcd WAL, --heartbeat-interval and --election-timeout tuned to network RTT, --quota-backend-bytes set appropriately, and regular compaction + defragmentation.


Detailed Answer:

etcd performance is almost entirely I/O-bound because every committed write requires at least two fdatasync() calls to the WAL. Disk fsync latency is the single most important factor — etcd targets sub-10ms WAL fsync. Everything else (CPU, network) is secondary. The second-biggest lever is network latency between etcd members: heartbeat intervals must be at least 10× the network RTT, and election timeouts must be at least 5× the heartbeat interval. The third lever is compaction and defrag cadence: an un-compacted etcd accumulates historical revisions and grows its bbolt file indefinitely, eventually hitting the --quota-backend-bytes limit (default 2GB, max 8GB).


Deep Dive:

Disk performance — the most critical factor:

# Test disk sequential write latency (run on etcd node)
# Target: < 10ms for 99th percentile
fio --rw=write \
    --ioengine=sync \
    --fdatasync=1 \
    --directory=/var/lib/etcd \
    --size=22m \
    --bs=2300 \
    --name=etcd-fio-test \
    --output-format=json | \
    jq '.jobs[0].sync.lat_ns | {
      p50_ms: (.percentile["50.000000"] / 1000000),
      p99_ms: (.percentile["99.000000"] / 1000000),
      p999_ms: (.percentile["99.900000"] / 1000000)
    }'

# Good result:  p99 < 10ms
# Warning:      p99 10-50ms
# Bad:          p99 > 50ms (etcd will be unstable)

etcd static pod tuning (kubeadm clusters):

# /etc/kubernetes/manifests/etcd.yaml
apiVersion: v1
kind: Pod
metadata:
  name: etcd
  namespace: kube-system
spec:
  containers:
  - command:
    - etcd
    # --- Disk & quota ---
    - --data-dir=/var/lib/etcd          # On NVMe/SSD dedicated disk
    - --wal-dir=/var/lib/etcd-wal       # Separate disk from data dir (ideal)
    - --quota-backend-bytes=6442450944  # 6GB quota (default 2GB is too small)
    - --auto-compaction-mode=periodic
    - --auto-compaction-retention=1h    # Compact revisions older than 1 hour
    
    # --- Raft timing (tune to your network RTT) ---
    # Rule: heartbeat = 10x RTT, election-timeout = 5x heartbeat
    # For 1ms RTT: heartbeat=10ms, election-timeout=100ms (defaults — fine)
    # For 10ms RTT: heartbeat=100ms, election-timeout=1000ms
    - --heartbeat-interval=100          # milliseconds
    - --election-timeout=1000          # milliseconds (10x heartbeat)

    # --- Snapshot tuning ---
    - --snapshot-count=10000            # snapshot every 10k committed entries
                                        # lower = more snapshots = less WAL replay
                                        # higher = faster writes, more WAL replay on restart

    # --- gRPC & connection ---
    - --max-snapshots=5
    - --max-wals=5
    - --grpc-keepalive-min-time=5s
    - --grpc-keepalive-interval=2h
    - --grpc-keepalive-timeout=20s

    # --- Peer & client TLS ---
    - --peer-client-cert-auth=true
    - --client-cert-auth=true

    image: registry.k8s.io/etcd:3.5.12-0
    resources:
      requests:
        cpu: 200m
        memory: 900Mi      # etcd working set fits in memory — give it enough
      limits:
        cpu: "4"
        memory: 8Gi
    volumeMounts:
    - mountPath: /var/lib/etcd
      name: etcd-data
    - mountPath: /var/lib/etcd-wal
      name: etcd-wal          # separate PV or disk mount
  volumes:
  - name: etcd-data
    hostPath:
      path: /var/lib/etcd
  - name: etcd-wal
    hostPath:
      path: /mnt/etcd-wal     # mount a second NVMe disk here

Compaction and defrag — must be automated:

#!/bin/bash
# etcd-maintenance.sh — run as a CronJob on control plane nodes
# Recommended cadence: every 30 minutes for compaction, nightly for defrag

ETCDCTL="etcdctl --endpoints=https://127.0.0.1:2379 \
  --cacert=/etc/kubernetes/pki/etcd/ca.crt \
  --cert=/etc/kubernetes/pki/etcd/peer.crt \
  --key=/etc/kubernetes/pki/etcd/peer.key"

# Step 1: Get current revision
REV=$($ETCDCTL endpoint status --write-out=json | \
  jq -r '.[0].Status.header.revision')

echo "Current revision: $REV"

# Step 2: Compact to current revision (removes old historical data)
$ETCDCTL compact $REV
echo "Compacted to revision $REV"

# Step 3: Defrag each member sequentially (NOT all at once — causes quorum loss)
for ENDPOINT in \
  https://etcd-0.etcd:2379 \
  https://etcd-1.etcd:2379 \
  https://etcd-2.etcd:2379; do
  echo "Defragmenting $ENDPOINT..."
  $ETCDCTL defrag --endpoints=$ENDPOINT
  # Wait between members to avoid simultaneous unavailability
  sleep 30
done

# Step 4: Check DB size after defrag
$ETCDCTL endpoint status --write-out=table

Kubernetes CronJob for etcd maintenance:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: etcd-compaction
  namespace: kube-system
spec:
  schedule: "*/30 * * * *"   # every 30 minutes
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          hostNetwork: true
          tolerations:
            - key: node-role.kubernetes.io/control-plane
              operator: Exists
              effect: NoSchedule
          nodeSelector:
            node-role.kubernetes.io/control-plane: ""
          containers:
            - name: etcd-maintenance
              image: registry.k8s.io/etcd:3.5.12-0
              command: ["/bin/sh", "/scripts/maintenance.sh"]
              volumeMounts:
                - name: etcd-certs
                  mountPath: /etc/kubernetes/pki/etcd
                  readOnly: true
                - name: scripts
                  mountPath: /scripts
          volumes:
            - name: etcd-certs
              hostPath:
                path: /etc/kubernetes/pki/etcd
            - name: scripts
              configMap:
                name: etcd-maintenance-scripts
          restartPolicy: OnFailure

OS-level tuning for etcd nodes:

# /etc/sysctl.d/etcd.conf — apply with: sysctl --system
# Increase max open files
fs.file-max = 1000000

# Reduce dirty page write-back (forces more frequent flushes — good for WAL)
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10

# Disable swap entirely — etcd latency spikes when memory pages are swapped
vm.swappiness = 0

# Increase network backlog
net.core.somaxconn = 32768
net.ipv4.tcp_max_syn_backlog = 16384

# Set I/O scheduler to none/mq-deadline for NVMe (not cfq which is for HDDs)
# Run for each etcd disk:
echo "mq-deadline" > /sys/block/nvme0n1/queue/scheduler

# CPU governor: performance mode (avoid frequency scaling latency)
cpupower frequency-set -g performance

Performance tuning impact table:

| Tuning Lever | Default | Recommended | Expected Improvement | |---|---|---|---| | Disk type | HDD/network | Local NVMe | 10–100× fsync latency | | WAL on separate disk | Same disk | Separate NVMe | 2–3× write throughput | | quota-backend-bytes | 2GB | 6–8GB | Avoids quota alarms | | auto-compaction-retention | None | 1h | Steady DB size | | heartbeat-interval | 100ms | 10× RTT | Fewer spurious elections | | snapshot-count | 100000 | 10000 | Faster restart recovery | | Defrag cadence | Manual | Nightly | Reclaims fragmented space | | vm.swappiness | 60 | 0 | Eliminates swap-induced latency |

Production lesson: A large etcd cluster at Datadog famously had cascading API server slowdowns traced entirely to a shared disk between etcd WAL and OS logs. Separating the WAL to a dedicated NVMe disk reduced p99 write latency from 80ms to 4ms. Disk isolation is the highest-ROI single change you can make.

Common mistake: Running etcdctl defrag --endpoints=all in a single command. This defragments all members simultaneously, causing all of them to become briefly unavailable at the same time — which breaks quorum and makes etcd completely unavailable. Always defrag one member at a time with a sleep between each.

Self-assessment: Can you explain why etcd is I/O-bound rather than CPU-bound? Can you calculate correct heartbeat and election timeout values for a cluster with 5ms inter-node RTT? Can you write the compaction + defrag script from memory and explain why the order matters?

Follow-up questions: Q8 (etcd monitoring), Q9 (Diagnose slow etcd scenario), Q10 (etcd HA topology)


Finished reading?

Mark as complete to claim your +25 XP and track progress.