Organize the answer around ownership, limits, failure, and recovery. Definitions become interview-ready when they survive a concrete production scenario.
Question set
10 detailed answers
01Walk through a full DNS resolution: from the app's getaddrinfo to the authoritative server. Where are the caches along the way, and how do you debug each hop?
middle
Short answer: The app calls getaddrinfo → libc consults nsswitch.conf (/etc/hosts first) → the local stub resolver (systemd-resolved with its own cache) → a recursive resolver (ISP/CoreDNS) caching by TTL → root → TLD → the zone's authoritative server.
In depth:
- nsswitch.conf and /etc/hosts — before any DNS: the line
hosts: files dnsmeans /etc/hosts wins over the network. - Stub resolver — systemd-resolved (127.0.0.53) keeps its own cache; flush it with
resolvectl flush-caches. - Recursive resolver — caches answers by TTL and — often forgotten — NXDOMAIN too (negative caching, TTL taken from the SOA).
- Recursion — root → TLD (.com) → the zone's authoritative server.
Debugging hop by hop: dig example.com (system resolver), dig @8.8.8.8 (a specific resolver), dig +trace (full recursion from the root, bypassing every cache), getent hosts (the app's actual path, through nsswitch).
app → getaddrinfo → nsswitch.conf → /etc/hosts
→ stub (systemd-resolved, cache)
→ recursive resolver (caches by TTL)
→ root → TLD → authoritative
⚠️ Common mistake: debugging with dig alone and skipping nsswitch and /etc/hosts (dig talks to DNS directly, bypassing both) — and negative caching: "I deleted the record, but the NXDOMAIN is still living out its TTL."
02curl by IP works, but by hostname it fails — intermittently. How do you diagnose it?
middle
Short answer: "Intermittently" is the DNS signature: one dead address in the A-record set, a cached negative answer, split-horizon (different resolvers return different answers), or Kubernetes search-domain expansion (ndots). The method: dig against every resolver in the chain and compare answers and TTLs.
In depth:
- A dead address in the set — DNS returns several A records round-robin; if one backend is down, every Nth request fails.
- Negative caching — a resolver cached NXDOMAIN/SERVFAIL and serves it until the TTL expires, even though the record now exists.
- Split-horizon — internal and external resolvers see different zones; which one your machine got is a resolv.conf question.
- ndots in Kubernetes — a name with fewer than 5 dots is first expanded through the search domains (svc.cluster.local etc.): extra queries, timeouts, occasionally surprising name matches.
dig +short api.example.com # system resolver
dig +short api.example.com @10.0.0.2 # each resolver from resolv.conf
dig +trace api.example.com # what the authoritative serves
dig api.example.com # compare the ANSWER set and TTLs
⚠️ Common mistake: concluding "IP works, so the network is fine" and going off to debug the application. An intermittent failure by name is round-robin over a broken set or a cache race — not a code bug.
03Describe the TLS handshake. Why is SNI needed, and how do multiple certificates coexist on one IP?
senior
Short answer: The client sends ClientHello (versions, ciphers, and the hostname in SNI) → the server replies with its certificate and key-exchange parameters → both sides derive session keys, and traffic is then encrypted symmetrically. SNI exists because TLS happens BEFORE HTTP: the Host header hasn't been sent yet, so the server has nothing else to pick a certificate by.
In depth:
- ClientHello — TLS versions, cipher list, SNI in plaintext; in TLS 1.3 the key share comes along too.
- ServerHello + certificate — the server picks a cipher and the certificate matching the SNI.
- Key exchange — (EC)DHE provides forward secrecy; session keys are derived from the shared secret.
- Why SNI specifically — the hostname has to travel in the handshake: HTTP's Host header arrives inside the already-encrypted channel. That's how dozens of virtual hosts with different certificates share one IP:443.
| Where to terminate TLS | Pros | Cons |
|---|---|---|
| At the load balancer | cheap, central certificate management | plaintext traffic inside the perimeter |
| At the pod / mTLS | end-to-end encryption, mutual authentication | CPU, certificate distribution and rotation |
⚠️ Common mistake: the expired certificate is the classic outage. "We'll renew by hand" is the wrong answer: ACME/cert-manager for auto-renewal plus an alert 2–3 weeks before expiry.
04L4 vs L7 load balancing: what can each layer see, and what can it do?
junior
Short answer: An L4 balancer sees only IPs and ports — it spreads TCP/UDP flows, fast and cheap, but the protocol is opaque to it. L7 parses HTTP: routing by path and headers, retries, TLS termination, cookie affinity — at the cost of CPU spent parsing.
In depth:
| L4 | L7 | |
|---|---|---|
| Sees | IP:port, SYN | method, path, headers, cookies |
| Routing | round-robin/hash over flows | /api → one pool, /static → another |
| Retries & timeouts | none — can't tell where a request ends | per-request retries, circuit breaking |
| TLS | passthrough only | termination, X-Forwarded-For |
| Cost | near zero, millions of pps | HTTP parsing: CPU and latency |
Examples: L4 — IPVS, AWS NLB; L7 — nginx, Envoy, AWS ALB. A common cascade: L4 on the edge → L7 inside.
⚠️ Common mistake: expecting retries or path-based routing from L4 — it doesn't see HTTP and fundamentally can't tell where one request ends and the next begins.
05The logs say "nf_conntrack: table full, dropping packet". What is conntrack, and how do you fix this?
senior
Short answer: conntrack is netfilter's connection-state table; NAT and the stateful firewall are built on it. Every NAT'd flow takes an entry; once the table is full, new packets are silently dropped. The fix: raise the limit, remove extra NAT hops, tame the DNS traffic.
In depth:
- Why the table exists — for the reply packet to get the reverse translation, the kernel remembers every flow: addresses, ports, state, timeout.
- UDP counts too — every DNS query creates an entry that lingers ~30 s after the answer. Microservices without a DNS cache spawn thousands of entries — the classic Kubernetes production incident.
- Diagnosis — compare the counter to the limit and see what's eating the table:
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
conntrack -L -p udp --dport 53 | wc -l # how much DNS ate
sysctl -w net.netfilter.nf_conntrack_max=1048576 # the tactical fix
- Strategically — fewer NAT hops, a local DNS cache (NodeLocal DNSCache), an eBPF dataplane (Cilium) instead of kube-proxy/iptables — flows bypass conntrack entirely.
⚠️ Common mistake: cranking the limit and closing the ticket. The limit is symptomatic: the cause is usually a DNS storm or a redundant NAT, and it will be back as traffic grows.
06How does iptables process a packet: which tables and chains, and where does DNAT happen?
middle
Short answer: A packet walks the chains in a fixed order: PREROUTING (nat table — DNAT happens here) → routing decision → INPUT or FORWARD (filter) → POSTROUTING (nat — SNAT/masquerade here). Reply packets don't traverse the rules again: conntrack applies the reverse translation automatically.
In depth:
- DNAT before routing — the destination address must be rewritten before the kernel decides where the packet goes: to itself (INPUT) or in transit (FORWARD).
- filter in INPUT/FORWARD — this is where ordinary firewall rules live.
- SNAT/masquerade on the way out — in POSTROUTING, once the egress interface is known.
- conntrack — the DNAT rule fires only on a flow's first packet; the rest and the reply traffic are translated from the conntrack entry.
packet ─► PREROUTING (nat: DNAT) ─► routing decision
│ local?
├─ yes ─► INPUT (filter) ─► process
└─ no ──► FORWARD (filter)
└─► POSTROUTING (nat: SNAT) ─► out
Publishing a container port (-p 8080:80) is exactly a DNAT rule in PREROUTING plus masquerade for the return path.
⚠️ Common mistake: looking for DNAT in the filter table — then being surprised that tcpdump past the translation point shows already-rewritten addresses: NAT happened before where you're looking.
07Over the VPN, small requests go through, but large uploads hang. How do you diagnose it?
senior
Short answer: The classic MTU black hole: the tunnel eats part of the frame (an overlay shrinks the effective MTU; VXLAN costs 50 bytes), and Path MTU Discovery doesn't work because a firewall drops ICMP "fragmentation needed". Small packets squeeze through; large ones vanish silently.
In depth:
- Mechanics — the app sends packets sized for MTU 1500, the tunnel adds headers, the packet no longer fits; with DF set it gets dropped and an ICMP type 3 code 4 should be sent back — but ICMP is blocked, so the sender never learns.
- Signature — the TCP handshake and small responses are fine (small segments), data transfer hangs. Exactly "curl works, upload hangs".
- Test — binary-search the size with fragmentation forbidden:
ping -M do -s 1472 host # 1472 + 28 = 1500: does it pass?
ping -M do -s 1422 host # narrow down to the real path MTU
- Fix — MSS clamping at the tunnel boundary (
--clamp-mss-to-pmtu), or lower the interface MTU, or allow the needed ICMP through the firewall.
⚠️ Common mistake: failing to connect "depends on size" with MTU and going off to debug the application. A size-dependent failure through a tunnel is MTU until proven otherwise.
08The server has thousands of sockets in TIME_WAIT — is that a problem?
middle
Short answer: By itself — no: TIME_WAIT is the normal state of whichever side closed the connection first (it waits 2×MSL, usually 60 s, so stray late segments don't land in a new connection). It only hurts on the CLIENT side under high connection churn: ephemeral ports run out.
In depth:
- Why the state exists — protection from wandering segments of the old connection and reliable teardown: the final ACK can get lost.
- When it hurts — a client (a proxy, a service opening a connection per request) piles up tens of thousands of TIME_WAITs and exhausts the ~28k ephemeral ports per (dst IP, dst port) tuple — new connects fail with EADDRNOTAVAIL.
- The proper fix — keep-alive and connection pooling: stop opening a connection per request.
- Knobs —
tcp_tw_reuseis safe for outbound connections (reuse guarded by TCP timestamps); widenip_local_port_range.
ss -s # summary: how many timewait
ss -tan state time-wait | awk '{print $4}' | sort | uniq -c | sort -rn | head
⚠️ Common mistake: recommending tcp_tw_recycle — it broke clients behind NAT (timestamps from different machines interleave) and was removed from the kernel as of 4.12.
09The load balancer's health check is green, yet users are getting 502/504. How is that possible?
middle
Short answer: The health check probes something other than what traffic lives on: /healthz responds while /api dies on a dependency. The other classic is a keep-alive idle-timeout mismatch: the balancer holds the connection longer than the app, reuses an already-closed socket, and gets a 502.
In depth:
- Probe ≠ real path — a dependency-free check stays green while the real request dies on the database or an external API. You need a readiness probe that touches real dependencies (carefully: don't build a cascade).
- The keep-alive race — the app closes an idle connection after 5 s, the balancer considers it alive for 60 s: the next request rides into a closed socket → 502. The rule: the app's idle timeout must exceed the balancer's.
- 502 vs 504 — different diseases: 502 — the backend answered "wrongly"; 504 — it didn't answer at all.
| Code | Meaning | Typical cause |
|---|---|---|
| 502 | malformed/invalid backend response | keep-alive race, process crash, RST |
| 504 | no response within the timeout | hung worker, DB timeout, overload |
⚠️ Common mistake: trusting a green health check. It answers "is the process alive", not "is it serving real requests".
10HTTP/1.1 vs HTTP/2 vs HTTP/3: what problem does each solve, and what is head-of-line blocking?
middle
Short answer: Each version removes its own layer of head-of-line blocking (HOL). In HTTP/1.1 requests on a connection are strictly sequential; HTTP/2 multiplexes streams over one TCP connection, but one lost packet stalls all streams (TCP-level HOL); HTTP/3 (QUIC over UDP) gives independent streams and 0-RTT.
In depth:
| HTTP/1.1 | HTTP/2 | HTTP/3 (QUIC) | |
|---|---|---|---|
| Transport | TCP | TCP | UDP |
| Parallelism | 1 request per connection (browsers open ~6) | stream multiplexing | independent streams |
| HOL | at the HTTP level: a request queue | gone from HTTP, remains in TCP | none: a loss stalls only its own stream |
| Extras | simplicity, debuggable by eye | HPACK, priorities | 0-RTT, connection migration across networks |
- HOL in 1.1 — a slow response holds everyone behind it; pipelining is broken in practice.
- TCP HOL in h2 — TCP guarantees byte order: a hole in the stream blocks delivery of all streams, even when their bytes have already arrived.
- QUIC — reliability per stream, with the handshake merged into TLS 1.3.
⚠️ Common mistake: "h2 solved HOL completely." It removed HOL at the HTTP level, but on a lossy network h2 over a single TCP connection can lose even to 1.1 with six connections.
Source notes
References and review policy
RecallDeck’s interview answers are editorial material, reviewed against maintained official documentation where a primary reference is available. Tool selections use direct provider links and contain no affiliate placements. Features can change after the review date.
From reading to recall
Practice the full interview loop.
RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.