Skip to main content

Real Interview Questions

This module contains actual interview questions from infrastructure, observability, and platform engineering roles at top companies. Each question includes detailed solutions and the key insights interviewers are looking for. Think of this module the way a kernel developer thinks about test suites: you don’t just verify the happy path — you test the boundary conditions, the failure modes, and the performance under pressure. These questions work the same way. They test whether you have a working mental model of the kernel, not whether you memorized a man page.
What to Expect: These questions require deep understanding, not memorization
Interview Format: Usually 45-60 minute deep dives into 2-3 topics
Time to Prepare: 10-12 hours to work through all scenarios
A senior engineer would say: “The best interview answers trace the path from user space down to the kernel and back. If you can explain where in the stack a problem lives and why the kernel behaves that way, you’ve already separated yourself from 90% of candidates.”

Company-Specific Patterns

Different companies emphasize different areas:

Observability Company Questions

Question 1: Implement a Syscall Counter (Datadog-style)

Context: You’re asked to implement a tool that counts syscalls by process in production without significant overhead.
The Question: “Design and implement a production-safe syscall counter. It should show the top processes by syscall count in real-time. Discuss the trade-offs of different approaches.”
Interviewer is looking for:
  1. Knowledge of different approaches (strace, perf, eBPF)
  2. Understanding of overhead implications
  3. Production safety considerations
  4. Sampling vs complete counting trade-offs
Key trade-offs to discuss:
  • strace: Per-syscall ptrace, very high overhead (~100x slowdown)
  • perf: Sampling-based, lower overhead, may miss syscalls
  • eBPF tracepoints: Low overhead (~1-5%), production-safe
  • eBPF kprobes: Slightly higher overhead, more flexible
Best approach: eBPF tracepointWhy eBPF over strace or perf? It comes down to how the kernel processes these hooks. strace uses ptrace, which forces a context switch on every single syscall — the kernel stops the traced process, notifies the tracer, waits for it to resume, then re-enters the process. At scale, that is a 100x slowdown. eBPF tracepoints, by contrast, run a small verified program inside the kernel at the tracepoint site — no context switches, no copying data to user space on every event.
User-space component:
  • Periodically read the map (every 1-2 seconds), sort by count, display top N processes
  • Handle PID reuse: cross-reference with /proc/<pid>/stat start time so a recycled PID doesn’t inherit the old count
  • Optionally reset counters each interval for a “rate” view (syscalls/sec per process)
Debugging tip: If your eBPF program fails to load, run bpftool prog load ./syscall_counter.bpf.o /sys/fs/bpf/test and check dmesg for verifier errors. The verifier output tells you exactly which instruction failed and why — usually a missing bounds check or an uninitialized register.
What makes this production-safe:
  1. Bounded map size: Won’t consume unlimited memory
  2. No locks in hot path: Per-CPU increments would be ideal
  3. Graceful degradation: If map full, just skip new PIDs
  4. Low overhead: Tracepoint, not ptrace
Improvements for production:
Overhead estimation (these numbers matter in interviews — know them):
  • ~50-100ns per syscall (compare to ptrace: ~10-50us per syscall)
  • On a busy system (100K syscalls/sec): ~1% CPU overhead
  • On an extremely busy system (1M syscalls/sec): ~5-10% CPU — at this point, consider sampling
  • Memory footprint: 10240 entries * (4 + 8) bytes * NR_CPUS — on a 64-core machine, about 7.5 MB
  • Acceptable for production monitoring, but always set an upper bound on map size
Production gotcha: If your map fills up (all 10240 slots taken), bpf_map_update_elem returns -ENOMEM for new PIDs. Your BPF program silently stops tracking new processes. In production, monitor the map fill level from user space and either increase max_entries or implement an LRU eviction policy with BPF_MAP_TYPE_LRU_PERCPU_HASH.

Question 2: Debug High Latency in Production

Context: A service is experiencing intermittent latency spikes. You need to identify the cause without restarting the service.
The Question: “A production service has p99 latency spikes from 10ms to 500ms every few minutes. How would you debug this? Walk me through your approach.”
Systematic approach:
  1. Characterize the problem:
    • When do spikes occur? (Time correlation)
    • Which requests are affected? (Endpoint, payload)
    • Duration of spikes? (Seconds, minutes)
  2. Gather baseline metrics:
    • CPU utilization (is there contention?)
    • Memory usage (swapping? GC?)
    • Disk I/O (latency, throughput)
    • Network (retransmits, latency)
  3. Narrow down the layer:
    • Application code?
    • Runtime (GC pauses)?
    • Kernel (scheduling, I/O)?
    • Hardware (disk, network)?
Quick triage (a senior engineer runs these in this order — start wide, then narrow):
Deep analysis with bpftrace:
Debugging tip: If you cannot reproduce the latency spike interactively, set up a persistent bpftrace script that writes to a ring buffer file. Use bpftrace -o /tmp/slow_syscalls.log and let it run. When the spike happens, you have the evidence. This is the kernel equivalent of always-on distributed tracing.
Check for GC pauses (if JVM/Go):
Likely causes of intermittent spikes:
  1. Garbage Collection:
    • Symptom: Regular, predictable spikes
    • Detection: GC logs show long pauses
    • Fix: Tune GC, reduce allocation rate
  2. Disk I/O:
    • Symptom: Correlates with writes/fsync
    • Detection: biolatency shows spikes
    • Fix: Async I/O, better storage
  3. Memory Pressure:
    • Symptom: During memory spikes
    • Detection: sar -B shows page faults
    • Fix: Increase memory, reduce footprint
  4. CPU Throttling (containers):
    • Symptom: Regular, consistent spikes
    • Detection: cat /sys/fs/cgroup/cpu.stat
    • Fix: Increase CPU limits
  5. Network Issues:
    • Symptom: Affects network calls
    • Detection: tcpretrans, ss -ti
    • Fix: Check network path, timeouts

Question 3: Container Memory Behavior

The Question: “Explain what happens when a container hits its memory limit. What are the different behaviors, and how would you debug an OOM-killed container?”
When container approaches memory limit:Think of the kernel’s memory cgroup controller like a building with multiple fire alarms. Each trip point triggers a different response — from gentle warnings to full evacuation. Understanding which alarm you hit determines how you debug the problem.
Cgroup v2 memory controls (know these cold for interviews):
  • memory.current: Current usage in bytes — what the cgroup is actually consuming right now
  • memory.max: Hard limit (OOM if exceeded) — the absolute ceiling, no negotiation
  • memory.high: Soft limit (throttling begins) — the kernel starts pushing back but does not kill
  • memory.low: Best-effort protection — the kernel tries not to reclaim from this cgroup when the system is under pressure, but will if there is no other choice
  • memory.min: Hard protection — the kernel will never reclaim below this watermark, even under extreme system-wide pressure. Use this for critical workloads that must not be evicted.
Immediate diagnostics (run these within minutes of the kill — some evidence is ephemeral):
Understanding OOM output (interviewers love when you can read raw kernel output):
Debugging tip: If OOM kills happen repeatedly but anon-rss is small, check shmem-rss. A common culprit is /dev/shm usage inside containers (many ML frameworks and databases use shared memory heavily). Also check memory.stat for kernel and slab entries — kernel memory charged to the cgroup can silently consume your limit.
Memory profiling:
Proper memory sizing (this is where most teams get it wrong):
  1. Profile application under realistic load — not curl localhost, but actual production traffic replayed against a staging environment
  2. Account for peak usage, not average — the OOM killer does not care about your P50; it cares about your P100
  3. Include headroom for GC (JVM needs ~2x heap for GC overhead), file cache (the kernel will use any free memory in the cgroup for page cache), and kernel slab allocations
  4. Monitor memory.high throttling events — they are the early warning that your limit is too tight
Kubernetes recommendations:
Application-level protections:
  • Set JVM heap below container limit: -Xmx should be ~75% of memory.max to leave room for non-heap memory (thread stacks, native memory, class metadata). A common formula: -Xmx = container_limit * 0.75
  • Use memory-aware allocators like jemalloc or tcmalloc that return memory to the OS more aggressively than glibc’s default allocator (which holds onto freed pages via brk)
  • Implement backpressure mechanisms: when memory usage exceeds a threshold, stop accepting new work. This is cheaper than being OOM-killed and restarting.

Infrastructure Company Questions

Question 4: Network Stack Performance (Cloudflare-style)

The Question: “Explain the journey of a packet from the NIC to the application. Where are the performance bottlenecks, and how would you optimize for high packet rates?”
Common bottlenecks:
  1. Interrupt overhead:
    • Each interrupt: ~1-2μs
    • At 1M pps: 100% CPU just handling interrupts
    • Solution: NAPI, interrupt coalescing
  2. Memory allocation:
    • sk_buff allocation per packet
    • Solution: Page pools, recycling
  3. Lock contention:
    • Socket lock for each packet
    • Solution: SO_REUSEPORT, RSS
  4. Cache misses:
    • Packet data not in cache
    • Solution: Busy polling, NUMA awareness
  5. Context switches:
    • Waking application per packet
    • Solution: Batching, busy polling
Hardware level:
Kernel level:
Application level:
Ultimate performance: XDP (eXpress Data Path):
  • Process packets at the driver level, before sk_buff allocation — sk_buff allocation is one of the most expensive per-packet operations in the kernel (~100ns each)
  • Achievable throughput: 10M+ pps on a single core (vs ~1M pps through the normal stack)
  • Used by Cloudflare (DDoS mitigation), Meta (load balancing with Katran), and Cilium (Kubernetes networking)
  • Trade-off: you must write your packet processing logic in eBPF, which means no TCP/IP stack, no sockets — you are essentially writing a custom NIC firmware in a safe sandbox

Question 5: CPU Isolation for Low Latency

The Question: “We need sub-millisecond latency for a trading system. How would you configure Linux to minimize jitter?”
Kernel sources:
  • Timer interrupts (every 1-4ms)
  • RCU callbacks
  • Kernel threads (kworker, ksoftirqd)
  • System call overhead
Hardware sources:
  • SMI (System Management Interrupt)
  • Cache pollution
  • NUMA remote access
  • Power management (C-states)
Boot parameters (each parameter removes a different source of jitter — know why each one matters):
CPU affinity:
IRQ affinity:
Verify isolation:
Measure latency:

System Design with Kernel Awareness

Question 6: Design a Container Metrics Collector

The Question: “Design a system to collect CPU, memory, and I/O metrics from 10,000 containers on each host with minimal overhead.”
Batch cgroup file reading:
Debugging tip: If your metrics collector shows stale values, verify you are reading from offset 0 on each cycle. A common bug is using read() without lseek(fd, 0, SEEK_SET) — after the first read, the file position is at EOF, and subsequent reads return 0 bytes. pread avoids this entirely.
eBPF for CPU tracking:
Polling approach (10K containers, 1-second interval):
  • 30K file reads per second
  • ~100μs per read = 3 seconds of CPU
  • Too much overhead!
Optimized polling:
  • Keep FDs open: eliminate open/close
  • Batch reads with io_uring
  • Stagger collection across time
  • Result: ~100ms of CPU per second
eBPF approach:
  • Constant overhead regardless of container count
  • ~1-2% CPU for tracing hooks
  • Scales to any number of containers
Hybrid approach:
  • eBPF for high-frequency (CPU, I/O): ~1% overhead
  • Polling for low-frequency (limits, configs): ~0.1% overhead
  • Total: ~1.1% CPU overhead for 10K containers

Debugging Scenarios

Scenario 1: Container Not Starting

Situation: A container fails to start with “permission denied” but works as root.
Debugging tip: When you see “permission denied” in a container, check security layers in this order: (1) seccomp (check dmesg | grep seccomp), (2) capabilities (check grep Cap /proc/<pid>/status), (3) DAC/Unix permissions, (4) LSM (audit logs). This matches the kernel’s evaluation order and avoids chasing the wrong layer.
  1. Seccomp blocking syscall:
    • Solution: Add syscall to profile or use --security-opt seccomp=unconfined
  2. Missing capability:
    • Solution: --cap-add=SYS_ADMIN (or specific capability)
  3. SELinux/AppArmor denial:
    • Solution: Check audit logs, update policy
  4. User namespace UID mapping:
    • Solution: Check /etc/subuid, /etc/subgid
  5. Read-only filesystem:
    • Solution: --read-only with appropriate tmpfs mounts

Scenario 2: High Memory Usage Mystery

Situation: Container shows 2GB used, but application reports only 500MB heap. This is one of the most common debugging scenarios in container platforms, and the answer lies in understanding what the kernel counts as “memory used by this cgroup” versus what an application considers “my memory.”
Common causes of the discrepancy (in order of likelihood):
  1. Page cache (accounts for 60-80% of these cases):
    • Files read by the application are cached in memory by the kernel
    • Shows in memory.current but NOT in the application’s heap metrics
    • The good news: these pages are reclaimable — the kernel will evict them under pressure
    • The bad news: they still count toward memory.max and can trigger OOM if the limit is tight
  2. Memory-mapped files (shared libraries, data files):
    • Every .so loaded by the application is mmap’d into the process address space
    • Only the resident pages (those actually touched) consume physical memory
    • A 200MB library might only have 20MB resident — but those 20MB are charged
  3. Slab memory (kernel allocations on behalf of this cgroup):
    • Network buffers (sk_buff), file system metadata (dentry, inode caches), socket structures
    • A container running a web server with 10K connections could have 50-100MB of kernel slab memory
    • This is completely invisible to the application but fully charged to the cgroup
  4. Shared memory / tmpfs:
    • /dev/shm usage, POSIX shared memory, and tmpfs mounts are charged as shmem
    • Many databases and ML frameworks allocate large shared memory segments
    • Charged once to the cgroup even if multiple processes access it
A senior engineer would say: “When someone tells me their container is using more memory than expected, I check memory.stat first, not the application metrics. The kernel’s accounting is authoritative. The application only knows about its own heap — it has no visibility into page cache, slab, or shared memory charged to its cgroup.”

Quick Reference: Commands for Interviews


Key Interview Tips

Think Out Loud

Explain your reasoning as you work through problems. Interviewers want to see your thought process. Say “I am starting at the application layer and working down because…” rather than jumping to a tool.

Start Simple

Begin with the simplest approach, then discuss trade-offs and optimizations. “The naive approach is X, which works but has O(n) overhead. Here is how we improve it…”

Know the Stack

Be ready to go from application to syscall to kernel to hardware. A senior engineer would say: “The read() syscall enters the kernel via entry_SYSCALL_64, dispatches to ksys_read, which calls vfs_read, which calls the filesystem’s read handler, which may block on disk I/O…”

Practice Debugging

Work through real debugging scenarios on actual Linux machines. The muscle memory of knowing which tool to reach for and how to interpret its output is what separates candidates who “know about” Linux from those who “work with” Linux.
A pattern that impresses interviewers: When asked to debug something, state your hypothesis before you run the command. “I suspect this is a scheduling issue because the spikes are periodic, so I will check perf sched latency first.” This shows you are reasoning, not just running commands from a list.

Interview Deep-Dive

What the interviewer is testing: Whether you understand the VFS layer’s file descriptor table, how the kernel tracks open files per process, and your ability to debug a live production issue without restarting the service.Strong answer framework:Start by understanding the kernel’s fd tracking. Every process has a struct files_struct (pointed to by task->files) that contains an fd table — an array of struct file * pointers. Each open() allocates the lowest available fd and creates a struct file backed by a dentry/inode pair in the VFS. If close() is never called, the struct file is never freed, the dentry reference count stays elevated, and the inode stays pinned in memory.Diagnosis steps:
What impresses interviewers: Mention that the per-process fd limit (ulimit -n, backed by RLIMIT_NOFILE) defaults to 1024 on many systems. When you hit it, every open(), socket(), accept(), and pipe() returns -EMFILE. This cascading failure is how a file descriptor leak in one component can crash an entire service — the process cannot open any new files, including log files to report the error.Common mistake: Candidates suggest “just restart the process.” In production, restarting a stateful service (database, queue) can cause data loss or require lengthy recovery. The right answer is to identify the leak, apply a mitigation (raise the fd limit temporarily), then deploy a fix.
What the interviewer is testing: Deep understanding of process creation mechanics, virtual memory management, and the COW optimization that makes Unix process creation practical.Strong answer framework:When fork() is called, the kernel’s do_fork() (or kernel_clone() in modern kernels) creates a new task_struct — the kernel’s representation of a process. But it does NOT copy the parent’s physical memory. Instead, it duplicates the page table entries and marks every writable page as read-only in both the parent and the child. This is copy-on-write (COW).When either process later tries to write to a COW page, the CPU raises a page fault (because the PTE is marked read-only). The kernel’s page fault handler (do_wp_page()) detects the COW condition, allocates a new physical page, copies the content, and updates the faulting process’s page table to point to the new page with write permission. The other process still points to the original page.When exec() is called, the kernel’s do_execve() completely replaces the process’s address space. It:
  1. Releases all existing VMAs (which drops the refcounts on all those COW pages)
  2. Parses the ELF binary header to determine the program’s segments
  3. Maps the text segment (code) as read-only + executable
  4. Maps the data segment as read-write
  5. Sets up a new stack, copies argv and envp onto it
  6. Points the instruction pointer to the ELF entry point
Why COW matters: Without COW, fork() would copy all of the parent’s memory. A 2GB process forking would require 2GB of allocation and copying before the child can even call exec(), which immediately discards all of it. COW makes fork() nearly instant (just duplicating page tables, ~100us for a large process) and defers the actual copy to only the pages that are actually written.What impresses interviewers: Mention the MAP_PRIVATE flag on mmap’d files uses the same COW mechanism. Also mention that this is why vfork() exists — it is an even cheaper fork that shares the parent’s address space entirely (no page table copy), but the child MUST call exec() or _exit() immediately. posix_spawn() is the modern alternative that avoids the fork+exec overhead entirely.
What the interviewer is testing: Understanding of CFS bandwidth control, the relationship between CPU limits and cgroup throttling, and why averages lie in systems with bursty workloads.Strong answer framework:The CFS (Completely Fair Scheduler) bandwidth controller works on a period basis, not an average basis. When you set a Kubernetes CPU limit of 500m (half a core), the kernel translates this to cpu.max = "50000 100000" — meaning the cgroup gets 50,000 microseconds of CPU time per 100,000-microsecond period.Here is where averages are deceptive. If your application is bursty — idle for 80ms, then needs 100% of a core for 20ms — it will consume its entire 50ms quota in the first 20ms of burst, then be throttled for the remaining 80ms of that period. The average CPU usage over a minute might show 10%, but the application experiences hard throttling during every burst.How to detect it:
How to fix it:
  1. Increase the CPU limit — the most straightforward fix, but increases cost
  2. Set requests = limits (Guaranteed QoS class) — gives the pod a dedicated CPU core via cpuset pinning, which eliminates the CFS bandwidth controller entirely
  3. Increase only the period — some Kubernetes distributions expose --cpu-cfs-quota-period. A longer period (e.g., 200ms) allows longer bursts before throttling
  4. Remove the limit entirely (set requests only) — controversial, but it means the pod runs in Burstable QoS and is never throttled. The risk is noisy-neighbor effects on shared nodes.
What impresses interviewers: Mention that nr_throttled / nr_periods is the metric to watch, not average CPU usage. A ratio above 5% indicates that the application is being actively harmed by its CPU limit. Also mention that in multi-threaded applications, a limit of 1000m (1 core) does NOT mean “use one core” — it means “use 100ms of CPU time per 100ms period, spread across ANY number of cores.” A 4-thread application with a 1-core limit will burn through its quota in 25ms and be throttled for 75ms.
What the interviewer is testing: Knowledge of the OOM scoring algorithm, cgroup-scoped OOM behavior, and practical experience with tuning OOM behavior in production.Strong answer framework:The OOM killer is invoked when the kernel cannot free enough memory to satisfy an allocation. The core function is out_of_memory() in mm/oom_kill.c. In a cgroup context, the OOM killer is scoped to the cgroup that exceeded its limit — it will not kill processes outside the cgroup.The kernel scores each process using oom_badness(), which calculates:
This raw score is then adjusted by oom_score_adj (a value from -1000 to +1000, set via /proc/<pid>/oom_score_adj):
  • oom_score_adj = -1000: Never kill this process (OOM-immune)
  • oom_score_adj = 0: Default scoring
  • oom_score_adj = +1000: Always kill this process first
The final score is visible in /proc/<pid>/oom_score (range 0-1000). The process with the highest score is killed.In container environments:
  • Kubernetes sets oom_score_adj based on QoS class: Guaranteed = -997, Burstable = 2-999, BestEffort = 1000
  • This means BestEffort pods are killed first, then Burstable, and Guaranteed pods are killed last
  • You can also set memory.oom.group = 1 in cgroup v2 to kill ALL processes in the cgroup together (useful for ensuring a clean restart rather than partial kills that leave the container in a broken state)
What impresses interviewers: Mention that the OOM killer is a last resort. Before it fires, the kernel has already tried: (1) reclaiming page cache, (2) reclaiming slab caches, (3) writing dirty pages to disk, (4) swapping anonymous pages. Only when all of these fail does the OOM killer activate. Also mention memory.oom.group in cgroup v2 and why it matters for multi-process containers like those running an init system or a sidecar pattern.
What the interviewer is testing: End-to-end understanding of the VFS layer, page cache, block I/O layer, and the difference between write() returning and data being durable.Strong answer framework:The path from write() to disk traverses four major kernel subsystems:1. VFS layer (ksys_write -> vfs_write): The kernel resolves the fd to a struct file, which points to the filesystem’s file_operations struct. It calls the filesystem’s .write_iter handler (e.g., ext4_file_write_iter).2. Page cache (generic_perform_write): The filesystem writes data into page cache pages. It finds (or allocates) the page corresponding to the file offset, copies the user’s data into the page, and marks the page as dirty. At this point, write() returns to user space. The data is in memory but NOT on disk.3. Writeback (writeback_single_inode): The kernel’s flusher threads (or the sync syscall) periodically walk the list of dirty inodes and submit their dirty pages to the block layer. The default writeback delay is 30 seconds (dirty_writeback_centisecs), but it can be triggered earlier if the percentage of dirty pages exceeds dirty_ratio.4. Block I/O layer (submit_bio -> device driver -> disk): The block layer converts page writes into block I/O requests (struct bio), merges adjacent requests (elevator/IO scheduler), and submits them to the device driver. The driver programs the hardware (DMA for NVMe, SCSI commands for SAS) and the data is written to the physical medium.Critical insight: write() returning success does NOT mean the data is on disk. It means the data is in page cache. If the machine loses power before writeback completes, the data is lost. This is why databases call fsync() — which forces the dirty pages for that file through the entire path to the disk’s persistent storage. fsync() does not return until the drive’s write cache has been flushed.What impresses interviewers: Mention O_DIRECT as the bypass for page cache (used by databases that manage their own caching), O_DSYNC for synchronous data writes (like write + fdatasync on every call), and that NVMe drives with power-loss protection can safely report fsync completion before data hits NAND because the drive’s capacitors can flush the write cache during power loss. Also mention that io_uring changes the game by allowing the kernel to batch and submit these operations asynchronously without per-syscall overhead.

Next: Hands-on Projects →