skip to content

An operator edits a Go daemon's JSON config and reloads it, but the setting has no effect and nothing errors. How do you diagnose it?

level: seniorimportance: should knowfreq 38%

answer

  1. start from the bytes, not the theory
  2. is it even the file the process read
  3. make the decoder report what it skipped
  4. a clean strict pass means something overwrote it
  5. log what the process believes after each load

basics

~20 s

Prove which bytes the process read, then re-decode those bytes with a json.Decoder using DisallowUnknownFields, which names the first key no field matched. If that passes, look for a duplicate key later in the file or a later layer overriding the value.

solid answer

~50 s

Work from the bytes forward. First confirm the process really read the file the operator edited — the path it was started with, the mounted copy inside the container, and whether the reload signal was actually handled — because a change to the wrong copy looks identical from outside. Then take those exact bytes and re-decode them through a `json.Decoder` with `DisallowUnknownFields()` set; if the key is misspelled you get `json: unknown field "..."` naming it, which is usually the whole answer. If the strict decode is clean, the key did match a field, so the value was set and then lost: look for the same key repeated later in the document (last occurrence wins), for a case variant landing on the same field, or for a flag or environment layer applied after the decode. Finally, log the decoded configuration after every successful load so the next occurrence is answered from a log line rather than an investigation.

code

go · 9 lines
go
func checkStrict(b []byte) error {
	dec := json.NewDecoder(bytes.NewReader(b))
	dec.DisallowUnknownFields()
	var c Config
	if err := dec.Decode(&c); err != nil {
		return fmt.Errorf("config: %w", err)
	}
	return nil
}

go deeper

for a junior

Know that a config setting can be ignored with no error at all, and that the first thing to establish is whether the process read the file that was edited.

for a middle

Explain how a strict re-decode with DisallowUnknownFields turns the silence into a named key, and why a clean strict pass changes the hypothesis rather than closing the case.

for a senior

Show an ordered diagnosis — bytes, then decode, then overwrite sources — and the durable fixes: strict decode at startup, lenient logged reload, and logging the effective configuration after every load.

for a principal

Decide where this class of failure gets caught for every service you own — CI validation of rendered config, a validate subcommand, or startup — and who is accountable when leniency lets a bad config through.

## Why this symptom is hard A daemon that loads JSON configuration into a struct has a failure mode with no signal at all: the file parses, the decode succeeds, the process reports a healthy reload, and one setting is simply not there. The operator's mental model — "I changed it, so it changed" — is contradicted by nothing in the logs. Diagnosis is therefore about manufacturing the signal the decode refused to give you. ## Step 1: establish which bytes the process actually decoded Before reasoning about decoding, rule out that the edit never reached the decoder: - Which path was the process given, and is that the file that was edited? A config baked into an image, a mounted copy, and the one on the operator's disk are three different files that look like one. - Did the reload actually run? A hot reload driven by a signal or a file watcher can be missed entirely; the log line that says the reload happened is the thing to look for, and its absence is the answer. - Does the reload path decode into the *same* struct as startup? A reload that fills a smaller struct silently ignores every key only the startup struct knows, which reproduces this symptom precisely on reload and never at boot. The cheap confirmation is to read the file from the process's own point of view — same path, same mount, same user — and hash or dump it. ## Step 2: ask the decoder what it did not understand With the exact bytes in hand, decode them again strictly: ```go func checkStrict(b []byte) error { dec := json.NewDecoder(bytes.NewReader(b)) dec.DisallowUnknownFields() var c Config return dec.Decode(&c) } ``` An error of the form `json: unknown field "maxConnnections"` names the key that no field matched — the misspelling, the renamed setting, the key that belongs to a newer build. This one check resolves the majority of these incidents, and it is worth shipping as a subcommand of the daemon itself so an operator can run it before rolling anything out. Note that it reports the first offender only, so rerun after each fix. ## Step 3: if the strict decode is clean, the value was set and then lost A clean strict decode is informative: every key in the file matched some field. The value therefore reached the struct and something removed it afterwards. The candidates, in the order worth checking: - **A duplicate later in the document.** Keys are applied in order and the last occurrence wins, silently. A file assembled from fragments or rendered by a template that emits a section twice will do this. A key differing only in letter case counts as the same field, so `"maxconns"` further down overwrites `"MaxConns"` above. - **A later precedence layer.** Many daemons apply command-line flags or environment variables after the file. If the flag is set, the file's value is overwritten by design — and the operator editing the file cannot see that from the file. - **The value never being used.** The field is populated and the code that should read it does not, or read it once at boot and cached it. A reload that updates a struct nobody re-reads is indistinguishable from a reload that did nothing. ## Step 4: make the next one self-diagnosing The fix is not only this incident: - **Fail closed at startup.** Decode strictly when the process starts; a typo becomes a startup error that names the key, before the process takes traffic. - **Do not fail closed on hot reload.** A running process that rejects a new file must keep serving with the configuration it already has, and log the offending key loudly. Zeroing the configuration or exiting turns a typo into an outage. - **Log the effective configuration after every load.** Marshal the decoded struct back out (with secrets redacted) and log it. That single line answers "did my change take effect?" without anyone re-deriving it from the decoder's semantics. - **Validate before rollout.** Running the strict decode over the rendered config in CI, or as a `validate` subcommand, moves the discovery to the change author rather than the operator at reload time.

  • The strict re-decode reports nothing. What is your next hypothesis?
    That the key matched and the value was overwritten. Scan the document for the same key — or a case variant of it — appearing again further down, since the last occurrence wins silently. Then check whether a flag or environment layer is applied after the file, and whether the code actually re-reads the field after a reload.
  • Should the daemon refuse to start when strict decoding fails, and refuse to reload too?
    Refuse to start, yes: failing at boot with the key named is cheap and unambiguous. Refuse to reload, no: a serving process should keep its current configuration, log the offending key at error level and stay up. Turning a config typo into an outage is a worse failure than the one you were preventing.
  • What single log line would have prevented this investigation?
    The effective configuration after each successful load — the decoded struct marshalled back to JSON with secrets redacted. It shows the operator exactly what the process believes, so a setting that never landed is visible immediately instead of being inferred from behaviour.

saying these in an interview costs you the question

  • Assumes a successful decode means every key was used
  • Debugs the daemon's behaviour before checking which file it read
  • Never re-decodes the bytes strictly to name the key
  • Forgets a duplicate later in the file overwrites the edit
  • Makes a failed hot reload exit the running process
  • Blames the operator without producing the effective config