skip to content

A service reads its configuration from Consul's KV store with `GET /v1/kv/config/app/db_url` and finds the JSON response's Value field is a string like `cG9zdGdyZXM6Ly8...` rather than the URL it wrote. Why is that, and which query parameters return the raw value or a whole prefix in one call?

level: juniorimportance: should knowfreq 52%

answer

  1. the wrapper, not the value
  2. encoded for transport, not secured
  3. JSON strings cannot carry arbitrary bytes
  4. one query parameter skips the JSON entirely
  5. another fetches a whole prefix

basics

~20 s

Consul's KV HTTP API wraps every entry in JSON and base64-encodes the Value so arbitrary bytes survive the encoding. Decode it, or append ?raw to get the value verbatim; ?recurse returns every key under a prefix in one response.

solid answer

~40 s

Consul treats a KV value as opaque bytes, so `GET /v1/kv/<key>` returns a one-element JSON array whose `Value` field is base64 — the API cannot assume your value is UTF-8 text rather than a gzipped blob. You have three read shapes. Decode the base64 yourself, which also gets you the metadata: `CreateIndex`, `ModifyIndex`, `LockIndex`, `Session`, `Flags`. Or ask for `?raw`, where the response body *is* the value bytes with no JSON wrapper — that is what shell scripts and `curl` normally want. Or use `?recurse` to pull an entire prefix in one round trip, still base64-encoded, which is how a service loads all of `config/app/` at startup. `?keys` returns key names only. On the CLI, `consul kv get` and `consul kv get -recurse` do the decoding for you.

code

bash · 17 lines
bash
# Write a value
consul kv put config/app/db_url 'postgres://db:5432/app'

# Default read: JSON wrapper, Value is base64
curl -s http://127.0.0.1:8500/v1/kv/config/app/db_url

# Raw read: the body IS the value
curl -s http://127.0.0.1:8500/v1/kv/config/app/db_url?raw

# Whole prefix in one call (still base64 in the JSON)
curl -s 'http://127.0.0.1:8500/v1/kv/config/app/?recurse'

# Key names only, collapsed at the separator
curl -s 'http://127.0.0.1:8500/v1/kv/config/?keys&separator=/'

# CLI decodes for you
consul kv get -recurse config/app/

go deeper

for a junior

Know that the KV API returns JSON with a base64 Value, that ?raw gives you the bytes directly and ?recurse reads a whole prefix. Be able to write and read a key with consul kv put and consul kv get.

for a middle

Explain why the encoding exists — values are opaque bytes, JSON strings are not — and what CreateIndex, ModifyIndex and Flags mean. Show that a key namespace is flat and that a prefix delete needs an explicit recurse.

for a senior

Show judgment about what belongs in KV at all: small, slow-changing coordination data, not blobs or per-request state, given that every write is Raft-replicated and memory-resident. Mention the absence of any version history and how you compensate.

for a principal

Own the key-naming scheme as an authorization and blast-radius boundary, since prefixes are the only unit ACLs, exports and recursive deletes operate on. Decide where the source of truth lives and treat KV as the distribution mechanism.

## What a KV entry actually is Consul's key/value store maps a key — an ordinary string, where `/` is just a character and not a directory separator — to an opaque byte value plus bookkeeping fields. A read of one key returns a single-element JSON array: ```json [ { "LockIndex": 0, "Key": "config/app/db_url", "Flags": 0, "Value": "cG9zdGdyZXM6Ly9kYjo1NDMyL2FwcA==", "CreateIndex": 812, "ModifyIndex": 1904 } ] ``` The `Value` is base64 because JSON strings must be valid Unicode and Consul never inspects what you stored. A gzipped payload, a DER-encoded certificate or a stray 0x00 byte would all be unrepresentable as a raw JSON string, so the API encodes unconditionally. This is *transport* encoding, not confidentiality — anyone who can read the key can decode it with one command. Treat a secret in plain KV as readable by everyone whose ACL token grants read on that prefix. ## The read shapes - **Default** — JSON array with metadata. Use it when you need the indexes (for check-and-set writes or long polling). - **`?raw`** — the response body is the value bytes verbatim, no wrapper and no metadata. Perfect for `curl ... -o app.conf`. A missing key still returns 404. - **`?recurse`** — treats the path as a prefix and returns every entry beneath it, still base64. One round trip for a whole config namespace instead of one call per key. - **`?keys`**, optionally with `?separator=/` — key names only, collapsed at the separator, which is how UIs render a folder-like tree over a flat store. The CLI mirrors these: `consul kv get key`, `consul kv get -recurse config/app/`, `consul kv get -keys`, and `consul kv get -detailed` to see the index fields. ## Writes and deletes `PUT /v1/kv/<key>` takes the value as the request body and answers with the literal body `true` or `false` — a *successful HTTP call* can still report a failed write, which surprises people the first time. A write is a whole-value replacement: there is no append and no partial update. Optional parameters are `?flags=`, `?cas=` and `?acquire=`/`?release=`. `DELETE /v1/kv/<key>` removes exactly that key. Because keys are not folders, deleting `config/app` does **not** touch `config/app/db_url`; a prefix delete needs `?recurse` (`consul kv delete -recurse config/app/`). This is the single most common operational surprise in the KV API, in both directions — people delete a "folder" and nothing happens, or they add `-recurse` to the wrong prefix and remove far more than they meant to. ## The metadata fields `CreateIndex` is the Raft index at which the key was created; `ModifyIndex` the index of its last modification. Those are not timestamps — they are positions in the cluster's replicated log, and they are what make optimistic concurrency (check-and-set) and change notification (blocking queries) possible without any clock. `LockIndex` counts how many times a session has acquired the key, and `Session` names the session currently holding it. `Flags` is an opaque 64-bit integer your application may set for its own purposes; Consul stores it and never interprets it. ## Sizing, and what does not belong here Every KV write goes through Raft, is replicated to every server and is held in server memory. There is a per-value size limit (`kv_max_value_size`, 512 KB by default) and hitting it means the write is rejected, not truncated. Even well under the limit, the KV store is for small, slowly-changing coordination data: connection strings, thresholds, feature toggles, a rendered config fragment. It is not a blob store, not a queue and not a place for per-request state. It is also not versioned. A write overwrites the previous value with no history, and a delete leaves nothing behind — so recoverability has to come from outside, from a scheduled `consul kv export` or from keeping the desired state in a repository that a job syncs into Consul. ## Reading it from an application Two habits separate a working integration from a fragile one. First, do not poll on a timer: the API supports a long poll keyed on `ModifyIndex`, so you can be woken on change instead of asking every five seconds. Second, cache the last good value — a service that hard-fails at boot because the KV read timed out has turned its config store into a hard dependency of every restart. Every request should carry an ACL token, sent as the `X-Consul-Token` header (or via `CONSUL_HTTP_TOKEN` for the CLI), scoped to the prefix that service actually needs.

  • What happens if you try to store a 2 MB value in Consul KV?
    The write is rejected rather than truncated: Consul enforces a per-value ceiling controlled by `kv_max_value_size`, 512 KB by default. The limit exists because every KV write is replicated through Raft to every server and held in memory, so large values inflate snapshot size, replication cost and server memory for the entire cluster. Store a pointer — an object-store URL or a digest — rather than the payload.
  • The keys look hierarchical. Are they really a tree?
    No. The store is flat and `/` is an ordinary character. `?recurse` and `?keys&separator=/` simulate a tree by prefix matching, which is why deleting `config/app` leaves `config/app/db_url` untouched unless you pass `?recurse`. Design keys so a prefix maps cleanly onto an ACL boundary — `config/<team>/<service>/` — because the prefix is the only unit you can grant, list or delete as a group.
  • What is the Flags field for?
    It is an opaque 64-bit integer stored alongside the value that Consul never interprets. Applications use it as a small tag — a content-type marker, a schema version, a client library's own signal. Consul preserves it across writes only if you resend it, since a `PUT` replaces the entry, so a tool that ignores `Flags` will silently reset it to 0.

saying these in an interview costs you the question

  • Says the base64 encoding means the value is encrypted
  • Treats Consul KV as a general database for large blobs
  • Expects deleting a prefix to remove the keys beneath it
  • Polls the KV endpoint on a timer instead of long-polling
  • Thinks a 200 response always means the write applied

context