Fixing etcdserver: request is too large — Kubernetes ConfigMap Size Limit Exceeded
Threat/Impact Level: HIGH | Downtime Risk: HIGH | Time to Fix: 15–45 mins depending on refactor strategy
TL;DR
- What broke: etcd hard-rejects any single write request exceeding ~1.5MB (default
--max-request-bytes). Your ConfigMap crossed that threshold and the API server returned a 500/etcd RPC error, halting the rollout. - How to fix it: Split the ConfigMap, strip binary blobs, or externalize large data to object storage/Secrets with a CSI driver. Patch
--max-request-bytesonly as a last resort — it shifts the problem. - Shortcut: Use our Client-Side Sandbox above to auto-refactor this — paste your ConfigMap YAML and get a split/optimized version generated locally without leaking your config.
The Incident (What Does the Error Mean?)
Error from server: etcdserver: request is too large
or in etcd logs:
WARN etcdserver/v3_server.go:xxx rejected request, too large size=1612800 max=1572864
etcd enforces a hard ceiling on individual RPC payload size. The default is 1572864 bytes (1.5MiB). The Kubernetes API server serializes the entire ConfigMap object — data, binaryData, metadata, annotations — into a single etcd Put request. If that serialized blob exceeds the limit, etcd drops it at the transport layer. The API server propagates this as a generic error. Your Deployment, StatefulSet, or Helm release is now stuck. Any controller waiting on that ConfigMap version will loop or crash-backoff.
The Attack Vector / Blast Radius
This is a control plane availability failure, not a data-plane issue. The blast radius:
- Rollout freeze: Any workload referencing the oversized ConfigMap via
envFromorvolumeMountwill fail to reconcile. Kubernetes controllers will retry on exponential backoff, generating noise that masks other real errors. - etcd compaction pressure: Repeated failed large writes still generate WAL entries in some etcd versions before the reject gate. Under sustained retry storms, this accelerates disk I/O and compaction cycles.
- API server latency spike: The API server serializes the full object before the etcd rejection. In high-traffic clusters (>50 req/s to the ConfigMap endpoint), this serialization overhead on every retry contributes to API server p99 latency degradation — other unrelated workloads feel it.
- Helm/ArgoCD deadlock: Helm stores release state in Secrets, but if your Helm chart includes a ConfigMap that triggers this error,
helm upgradewill partially apply resources and leave the release in apending-upgradestate. Manual intervention required. - Raising
--max-request-bytesis not a fix — it raises the ceiling on every etcd node, increases snapshot size, slows leader elections, and pushes the problem to a higher threshold you'll hit again.
How to Fix It
Basic Fix — Split the ConfigMap
Identify which keys are bloated:
kubectl get configmap <name> -n <namespace> -o json | \
python3 -c "import json,sys; d=json.load(sys.stdin); \
[print(f'{len(v.encode())/1024:.1f}KB\t{k}') for k,v in d.get('data',{}).items()]" | sort -rn
Then split by logical domain:
- # Single monolithic ConfigMap — 1.8MB
- apiVersion: v1
- kind: ConfigMap
- metadata:
- name: app-config
- data:
- app.properties: "<50KB of app config>"
- nginx.conf: "<200KB of nginx config>"
- seed-data.json: "<1.5MB of static JSON data>"
+ # ConfigMap 1 — app config only (~50KB)
+ apiVersion: v1
+ kind: ConfigMap
+ metadata:
+ name: app-config-core
+ data:
+ app.properties: "<50KB of app config>"
+ ---
+ # ConfigMap 2 — nginx config only (~200KB)
+ apiVersion: v1
+ kind: ConfigMap
+ metadata:
+ name: app-config-nginx
+ data:
+ nginx.conf: "<200KB of nginx config>"
+ ---
+ # seed-data.json: REMOVE from ConfigMap entirely.
+ # Load from object storage (S3/GCS) via init container or
+ # mount a PVC seeded by a Job. See Enterprise section below.
Enterprise Best Practice — Externalize Large Blobs
Static datasets, CA bundles, ML model configs, and seed JSON do not belong in etcd. etcd is a coordination store, not a file system.
Option A: Init container pulling from object storage
- # Mounting 1.5MB JSON from ConfigMap volume
- volumes:
- - name: seed-data
- configMap:
- name: app-config
+ # Init container fetches from S3 at pod start
+ initContainers:
+ - name: fetch-seed-data
+ image: amazon/aws-cli:2.x
+ command:
+ - sh
+ - -c
+ - aws s3 cp s3://my-bucket/seed-data.json /shared/seed-data.json
+ volumeMounts:
+ - name: shared-data
+ mountPath: /shared
+ volumes:
+ - name: shared-data
+ emptyDir: {}
Option B: Kubernetes Secrets Store CSI Driver (for sensitive large configs)
- # binaryData in ConfigMap for TLS bundles
- binaryData:
- ca-bundle.crt: "<base64 encoded 800KB CA chain>"
+ # Reference via SecretProviderClass — data stays in Vault/ASM
+ volumes:
+ - name: ca-bundle
+ csi:
+ driver: secrets-store.csi.k8s.io
+ readOnly: true
+ volumeAttributes:
+ secretProviderClass: ca-bundle-provider
Option C: Last resort — raise etcd limit (document the debt)
- # kube-apiserver flag (default)
- --max-request-bytes=1572864
+ # Raise to 8MB — only if you've exhausted split/externalize options
+ # Document this in your runbook as technical debt
+ --max-request-bytes=8388608
+ # Also raise on each etcd node:
+ # --max-request-bytes=8388608 in etcd config
⚠️ If you're on managed Kubernetes (EKS, GKE, AKS): You cannot directly modify etcd flags. GKE and AKS silently cap this. EKS exposes no etcd configuration surface. Splitting the ConfigMap is your only path.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
1. OPA/Gatekeeper Policy — Block Oversized ConfigMaps at Admission
# ConstraintTemplate: deny ConfigMaps exceeding 512KB
package kubernetes.configmap.sizelimit
violation[{"msg": msg}] {
input.review.object.kind == "ConfigMap"
total := sum([count(v) | v := input.review.object.data[_]])
total > 524288
msg := sprintf("ConfigMap data exceeds 512KB limit (%d bytes). Split or externalize large keys.", [total])
}
2. Pre-commit / CI Lint Step
# Add to your CI pipeline before kubectl apply
for f in $(find ./k8s -name '*.yaml'); do
size=$(python3 -c "
import yaml, sys, json
with open('$f') as fh:
docs = list(yaml.safe_load_all(fh))
for d in docs:
if d and d.get('kind') == 'ConfigMap':
total = sum(len(v.encode()) for v in (d.get('data') or {}).values())
if total > 524288:
print(f'FAIL: {f} ConfigMap data={total} bytes > 512KB limit')
sys.exit(1)
")
done
3. Helm Chart Guardrails
- # values.yaml — no size validation
- configData: |
- {{ .Files.Get "large-seed.json" | b64enc }}
+ # Add a Helm test or pre-install hook that validates size
+ # Use .Files.Glob and fail loudly in _helpers.tpl:
+ {{- if gt (len (.Files.Get "large-seed.json")) 524288 }}
+ {{- fail "large-seed.json exceeds 512KB. Use an init container or external store." }}
+ {{- end }}
4. Checkov Rule (IaC Scanning)
Checkov doesn't ship a native ConfigMap size check, but you can add a custom check:
# custom_checks/configmap_size.py
from checkov.common.models.enums import CheckResult
from checkov.kubernetes.checks.resource.base_resource_check import BaseK8Check
class ConfigMapSizeCheck(BaseK8Check):
def __init__(self):
super().__init__(name="ConfigMap data must not exceed 512KB",
check_id="CKV_K8S_CUSTOM_001",
supported_entities=['ConfigMap'])
def scan_resource_conf(self, conf):
data = conf.get('data') or {}
total = sum(len(str(v).encode()) for v in data.values())
return CheckResult.PASSED if total <= 524288 else CheckResult.FAILED
Key rule: Treat the 512KB mark as your internal limit — giving you 3x headroom before the etcd 1.5MB hard wall. By the time a ConfigMap hits 512KB in code review, it's already an architectural smell.