In a log pipeline, when does interning repeated strings pay off — and when does the table become the leak?
answer
- what exactly does the saving scale with
- you pay per occurrence, you save per duplicate
- who holds the canonical instance, and for how long
- which fields are unbounded by construction
- bounding the table costs you the identity fast path
basics
~20 sInterning keeps one canonical instance per distinct value, so it pays when cardinality is small and bounded while occurrences are enormous. It backfires on high-cardinality fields: the table retains every distinct value forever, turning short-lived garbage into a permanent leak.
solid answer
~50 sInterning replaces every newly built value with a canonical instance from a lookup table, so the billion occurrences of five hundred service names collapse to five hundred character buffers plus a billion references. That is the payoff, and it scales with distinct-count times value-size, not with occurrence count — the occurrences were only references either way. Two costs run the other direction. First, you pay a hash and a content comparison on **every** occurrence to save memory only on the duplicates, so interning a field that is nearly unique is pure overhead. Second, and worse, the table is usually a strong, permanently held structure: intern request identifiers or user-supplied text and every distinct value that would have died young is now retained forever. My rule: intern only closed, low-cardinality fields, measure distinct-count first, and bound the table so a cardinality surprise degrades instead of exhausting memory.
go deeper
Be ready to explain the basic mechanic: a table holds one canonical copy per distinct value, and newly built duplicates are discarded in favour of the stored one so all holders share it.
Explain what the saving is proportional to — distinct values times size, not total occurrences — and that the lookup itself costs a hash plus a comparison on every single occurrence.
Show the failure mode from production: a permanently retained table fed by a high-cardinality field grows with cumulative traffic and never recovers, and you should be able to describe how you would confirm that from a heap breakdown.
Own the policy rather than the trick: which fields are eligible, who reviews additions, whether the table is bounded, and the explicit trade that a bounded table buys memory safety at the price of the identity fast path.
## What interning is **Interning** (canonicalization, deduplication) is: maintain a table of distinct values; on constructing a value, look it up; if an equal value is present, discard the new object and use the stored one; otherwise store the new one. Afterwards, every holder of that value references **one** shared object. Immutability is the precondition that makes this legal at all — sharing one object among unrelated holders is only safe if none of them can modify it. The payoff for a log-ingest pipeline is easy to size. Suppose events carry a service name drawn from about 500 distinct values, seen a billion times an hour, at ~30 characters each. Without interning, up to a billion separate 30-character buffers exist transiently; with interning, 500 buffers exist and the billion events hold references. Note carefully what the saving is proportional to: **distinct-count times value size** for what remains, versus **live occurrences times value size** for what is avoided. If your occurrences die immediately after being written to a socket, the memory you "saved" was never live at once and the win is much smaller than the arithmetic above suggests. The real wins come when occurrences are retained: an in-memory index, a batching buffer, a cardinality-tracking aggregator holding millions of event records at once. A second payoff follows: once a field is canonicalized, grouping and comparison on that field can use identity — one reference comparison instead of a character walk — provided every value on both sides went through the table. In a group-by over billions of rows that is a genuine hot-loop win. ## The two costs, and the direction of each **Cost 1 — CPU on every occurrence, savings only on duplicates.** The lookup itself is a hash of the full value plus a confirming content comparison, i.e. O(n) in the value's length, paid *per occurrence*. You pay that billion times to deduplicate down to 500. For a low-cardinality field the trade is excellent. For a field where nearly every value is distinct, you paid the full cost and deduplicated nothing — the table just grew. **Cost 2 — the table retains everything, so garbage becomes a leak.** This is the failure the question is aimed at, and the wrong answer it targets is "interning is free memory savings". A canonical table must hold a reference to each canonical instance, or it could not return it next time. Unless entries are held weakly or evicted, **every distinct value ever interned is retained for the process's lifetime**. Intern a service name and you retain 500 short strings, forever, which is fine. Intern a request identifier, a full URL with query parameters, a user-supplied search term, or an error message with an embedded timestamp, and you have converted a stream of short-lived garbage into monotonically growing permanent memory. The symptom is a process whose memory grows roughly linearly with traffic and never recovers, with the heap dominated by one enormous table — and it typically appears *after* someone "optimised memory" by interning more fields. There is a nastier variant: cardinality that is low in staging and unbounded in production. A field like `tenant_id` looks like a closed set of a dozen values until the platform onboards fifty thousand tenants, and a field like `endpoint` looks closed until someone starts templating identifiers into the path. ## The policy that actually holds up 1. **Measure distinct-count first, on production data**, not occurrence count. Interning is worth it when distinct-count is small relative to live occurrences; a cardinality estimate over a real sample answers this in minutes. 2. **Intern only closed, long-lived sets** — service names, log levels, region codes, metric names, enumerated status values. These are chosen from a fixed vocabulary and their permanent retention is a fixed, small cost. 3. **Never intern anything that carries an identifier, a timestamp, a user's input, or a full path with parameters.** Those are unbounded by construction. 4. **Bound the table.** A fixed-capacity cache with eviction, or weakly-held entries, converts a cardinality surprise from an out-of-memory crash into a graceful loss of deduplication. The cost is that identity comparison is then no longer sound, because an evicted value can reappear as a new object — you must pick one or the other, and that choice is the interesting part of the answer. 5. **Intern at the boundary, once**, where values are decoded, rather than sprinkling lookups through the code. One place to measure, one place to disable. Ecosystems differ on the defaults here in a way worth knowing: Java and C# expose an explicit, permanently-retained canonical pool for strings while also automatically canonicalizing compile-time literals, Python canonicalizes some short identifier-like strings by an unspecified internal rule, and Go leaves deduplication entirely to library code. In every one of them the analysis above is the same; only who owns the table changes. ## Answering it out loud Name the payoff (distinct-count times size, plus an identity fast path), name the two costs (per-occurrence CPU; permanent retention), then give the decision rule: measure cardinality, intern only closed vocabularies, bound the table, and be explicit that a bounded table forfeits the identity comparison.
- Which fields in an event stream would you refuse to intern, and why?Anything unbounded by construction: request and trace identifiers, full paths with query parameters, user-supplied search text, messages with embedded timestamps or numbers. Each is near-unique per event, so the lookup cost is paid in full and deduplicates nothing, while every distinct value is retained permanently — you have turned a garbage stream into monotonic memory growth.
- If you bound the intern table with eviction, what capability do you give up?Sound identity comparison. Once an entry can be evicted, a value that reappears is canonicalized to a *new* object, so two equal values may fail an identity check — intermittently, depending on cache pressure. A bounded table is a memory optimisation only; if you want the identity fast path, the table must be unbounded and therefore restricted to a closed vocabulary.
- How would you tell, from a running process, that an intern table has become the problem?Memory grows roughly linearly with cumulative traffic and never returns after load drops, and a heap breakdown shows one table dominating, holding the bulk of live character data with millions of entries. The confirming detail is that entry count tracks distinct values seen since start-up rather than anything about current load.
- Interning saves memory only for duplicates — so why intern at all if events are discarded immediately?Then you probably shouldn't. The saving is proportional to how many occurrences are *live at once*; if each event is serialized and dropped, few duplicates coexist and you have paid a per-occurrence hash and comparison for almost nothing. Interning earns its cost when occurrences are retained — in-memory indexes, batch buffers, aggregators holding millions of records.
saying these in an interview costs you the question
- Calls interning free memory savings with no downside
- Forgets the canonical table retains entries for the process lifetime
- Interns high-cardinality identifiers to save memory
- Sizes the win from occurrence count instead of distinct count
- Keeps an identity fast path after adding eviction to the table
- Ignores the per-occurrence hash and comparison cost