System Resource & I/O Optimization Questions
Tuning how a system uses CPU, memory, disk, and network at the OS and I/O layer. Covers I/O throughput and blocking, buffering and batching, filesystem and kernel-level performance settings, and resource contention between processes. Includes OS-level performance tuning and diagnosing resource saturation on the host.
Given two storage device families, traditional HDD and NVMe SSD, explain which you would choose for: (a) large sequential media archival writes, (b) OLTP workloads with small random writes, (c) time-series ingestion combined with compaction. Discuss cost, latency, IOPS, throughput, and write-amplification trade-offs.
Sample Answer
Direct answer
Choose NVMe (Non-Volatile Memory Express, a solid-state storage interface built for low-latency, high-parallelism flash access) for the online transaction processing (OLTP, transactional database work with many small concurrent random reads/writes) workload and the time-series-with-compaction workload, both are dominated by random I/O and latency sensitivity where NVMe's advantage is largest; a traditional hard disk drive (HDD, spinning-platter mechanical storage) is a reasonable and often more cost-effective choice for large sequential archival writes, where NVMe's latency advantage matters least.
(a) Large sequential media archival writes: HDD
Archival writes are large, sequential, and not latency-sensitive (nobody is waiting on a single archival write to complete within milliseconds); HDDs achieve respectable sequential throughput (commonly in the 150-250MB/s range for modern drives) at a fraction of NVMe's cost per gigabyte, and archival workloads care about cost-per-terabyte far more than latency or IOPS. NVMe's main advantages, low random-access latency and high IOPS, are simply not exercised by a sequential write pattern, so paying its cost premium here buys nothing the workload actually uses.
(b) OLTP with small random writes: NVMe
OLTP workloads are dominated by many small, concurrent, random reads and writes with strict latency expectations (users and transactions waiting in real time); an HDD's mechanical seek time (the physical head movement between non-sequential locations on the platter, typically several milliseconds per seek) makes random I/O dramatically slower than sequential I/O on the same device, often by one to two orders of magnitude in achievable IOPS (I/O operations per second, how many individual read/write operations a device can complete each second). NVMe has no mechanical seek at all and is built specifically for high queue-depth, low-latency random access, making it the clear choice whenever the access pattern is random and latency-sensitive, which OLTP always is by nature.
(c) Time-series ingestion combined with compaction: NVMe
Time-series ingestion itself is often sequential-write-friendly (append-mostly), which might suggest HDD, but compaction (the background process many time-series and log-structured storage engines run to merge and reorganize on-disk data files) generates substantial additional random and mixed read/write I/O on top of the ingestion writes, and that background I/O directly competes with foreground query and ingestion latency. NVMe's high IOPS and low latency absorb this compaction overhead without degrading foreground performance nearly as much as an HDD would, where compaction I/O would visibly compete with and slow down both ingestion and queries.
Trade-offs: cost, latency, IOPS, throughput, write amplification
Cost: HDD wins decisively on cost per gigabyte, which is why it remains the right choice for (a) and for any workload where capacity, not speed, is the binding constraint. NVMe costs meaningfully more per gigabyte but that premium buys latency and IOPS that (b) and (c) cannot function well without. Latency: NVMe's random-access latency is typically in the tens-of-microseconds range versus an HDD's single-digit-to-double-digit milliseconds for a random seek, a difference of roughly two to three orders of magnitude that matters enormously for (b) and (c) and not at all for (a). IOPS: NVMe sustains vastly higher random IOPS than HDD because it has no mechanical seek penalty and supports much deeper command queuing (NVMe's queue depth can reach thousands versus a traditional SATA/SAS HDD's much shallower queue). Throughput: for pure large sequential transfers, the throughput gap between a fast HDD and NVMe is much smaller proportionally than the IOPS/latency gap, which is exactly why (a) tolerates HDD well. Write amplification (the effect where the physical bytes written to flash exceed the logical bytes the application asked to write, because flash must erase and rewrite in larger blocks than it programs in) is an NVMe/SSD-specific concern with no HDD equivalent; a write-heavy random workload like (b) and the compaction-heavy workload in (c) both increase write amplification, which affects the SSD's endurance/lifespan and can reduce effective write throughput as the drive fills, an over-provisioning margin and a filesystem/database tuned to reduce small random writes (batching, larger block sizes where the workload allows it) both help manage this.
Trade-offs and pitfalls
The most common mistake is picking storage based on throughput or capacity alone and ignoring latency and IOPS, which are what actually determine OLTP and compaction-heavy performance; a spec sheet's MB/s number is close to irrelevant for workload (b). The second common mistake is under-provisioning NVMe capacity for a write-heavy workload without accounting for write amplification, which can quietly erode both endurance and effective throughput well before raw capacity is exhausted.
Design and implement a fixed-size buffer strategy for reading lines from a very large file where lines can be longer than the buffer. The API should read the file in chunks and return complete lines to the caller without loading the entire file into memory. Describe how you handle partial lines, multi-byte encodings, and performance trade-offs.
Sample Answer
Approach
Read fixed-size byte chunks from the file, accumulate them in a small internal buffer, split on newline bytes, hand back every complete line found, and carry the trailing partial fragment forward into the next read, growing it across as many chunk reads as it takes for a single line to complete. Decode text only once a line is known to be complete (bounded by a newline or end of file), which is what keeps multi-byte characters from being corrupted by a chunk boundary landing in the middle of one.
Implementation (Python)
def read_lines(path, bufsize=8192):
"""Yield complete text lines from `path`, reading at most `bufsize`
bytes at a time, without ever holding more than one buffered chunk
plus one pending (possibly very long) line in memory.
Lines may be longer than bufsize: a partial line is carried over and
grown across as many chunk reads as it takes until a newline turns
up. Multi-byte UTF-8 sequences are never decoded until they are
known to be complete, so a chunk boundary landing mid-character does
not corrupt the text or raise a spurious UnicodeDecodeError.
"""
pending = b""
with open(path, "rb") as f:
while True:
chunk = f.read(bufsize)
if not chunk:
break
pending += chunk
lines = pending.split(b"\n")
pending = lines.pop() # last element: partial line, or b"" if
# the chunk happened to end on a newline
for raw_line in lines:
yield raw_line.decode("utf-8")
if pending:
yield pending.decode("utf-8")
Output
Tested with bufsize=8, deliberately smaller than every line in the file, including one containing accented and CJK (Chinese/Japanese/Korean multi-byte) characters, against a file containing a short line, a 50-character line (far longer than the 8-byte buffer), a multi-byte-heavy line, and a final line with no trailing newline:
0: 'short'
1: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
2: 'café éééé 中文文文 done'
3: 'last-no-newline'
OK: matches str.split('\n') on the original content
Every line, including the multi-byte one, came back byte-for-byte correct despite the 8-byte read buffer splitting mid-character on nearly every read, and the result matched a plain in-memory str.split("\n") on the original content exactly.
Key points
Partial lines: the pending buffer holds only unterminated bytes; bytes.split(b"\n") gives back every complete segment plus the trailing (possibly empty) remainder in one call, so there is never a separate "did I finish a line" check to get wrong. A line longer than bufsize simply causes pending to keep growing across multiple read() calls until a newline finally appears in some later chunk, the loop does not need special-casing for that case, it falls out of the same logic. Multi-byte encodings: decoding happens only on raw_line, a byte string already known to be bounded by a real newline or true end of file, so any UTF-8 multi-byte sequence inside it is always complete by construction; decoding the raw chunk bytes directly (before splitting) would risk cutting a multi-byte character in half and raising UnicodeDecodeError, or worse, silently producing wrong characters with a more permissive decode mode.
Performance trade-offs
bufsize trades memory against system-call overhead: a small buffer (a few hundred bytes) keeps peak memory tiny but issues many more read() system calls for a large file, each with its own kernel-transition cost; a large buffer (64KB-1MB) amortizes that cost across more data per call at the price of higher peak memory, though still bounded and predictable, unlike loading the whole file. pending += chunk reallocates and copies the accumulated buffer on every chunk for files with unusually long lines; for a workload expected to have many very long lines, a bytearray with in-place extend() avoids the repeated string-concatenation copy that plain bytes concatenation incurs. This implementation also does not require the file to fit any particular line-length assumption up front, unlike a strategy that pre-allocates a fixed-size line buffer and errors on overflow, at the cost of no longer having a hard upper bound on how much memory one single pathologically long line can consume.
Edge cases
An empty file yields no lines. A file with no trailing newline still yields its final line correctly (the if pending: check after the read loop ends). A file consisting of only newlines yields a sequence of empty-string lines, which matches str.split("\n") semantics exactly, verified in the test above. A bufsize of 1 would still be correct, just extremely slow, since the logic makes no assumption about how much of a line arrives per chunk.
Explain NUMA (Non-Uniform Memory Access) and its effects on performance in multi-socket servers. How would you design and tune a service to be NUMA-friendly including CPU/memory pinning, thread affinity, allocator choices, and testing approaches?
Sample Answer
Design principle: shared-nothing per node, not one big shared heap
NUMA (Non-Uniform Memory Access, where each CPU socket has its own local memory and reaching another socket's memory costs extra latency over an inter-socket interconnect) rewards a design that partitions work by node rather than one that lets any thread touch any memory freely. The strongest NUMA-friendly pattern is to shard the service's data and threads one partition per node, so each node's threads mostly touch that node's own memory by construction, instead of trying to retrofit locality onto a shared-everything design after the fact.
Thread and CPU affinity
Pin worker threads to the CPUs of a single node at startup, using pthread_setaffinity_np() in C/C++ or an equivalent affinity API in the runtime, or by launching the process under numactl --physcpubind. Without pinning, the OS scheduler can migrate a thread to a core on a different node mid-run; each such migration risks a burst of remote memory accesses until the thread's working set effectively 'catches up' by being re-touched (and thus reallocated) on the new node.
Allocator choice
- Use a NUMA-aware allocator, for example jemalloc or tcmalloc configured with one arena per node (or per thread, matched to node count), so allocations naturally land on the calling thread's local node instead of a single global arena that anyone can touch.
- Linux uses a first-touch allocation policy for anonymous memory: a page is not actually assigned to a physical NUMA node until the first time something writes to it, regardless of which node's allocator called
malloc(). For large, latency-sensitive structures, allocate explicitly withnuma_alloc_onnode()ormbind()and make sure the FIRST touch genuinely happens on the intended node, since a naive 'warm-up' loop run on the wrong thread can silently place memory on the wrong node before the real workload ever runs.
Testing approach
- Before investing in NUMA-aware code, measure whether the penalty is real on this specific workload and hardware: run the same benchmark under
numactl --membindforced to the local node versus forced to a remote node, and compare. The gap varies a lot by platform and workload, so guessing is a bad use of engineering time; an A/B measurement settles it. - During a real load test, check
numastatdeltas per process to confirm the design's INTENDED locality is what actually happens at runtime, since fragmentation or an allocator falling back under pressure can quietly break locality even when the code was written correctly.
Explain read-ahead and prefetching mechanisms at OS and application levels. When does read-ahead improve performance and when can it harm throughput or latency? Describe how you would tune readahead for a media streaming service serving large sequential reads.
Sample Answer
Direct answer
Read-ahead is the kernel (or an application) speculatively fetching data beyond what was explicitly requested, betting that a sequential access pattern will keep asking for the next chunk. It helps when access really is sequential and predictable, because it hides storage latency by having data ready before it's asked for. It hurts on random-access workloads, where it wastes I/O bandwidth and evicts genuinely useful cached pages fetching data that's never used. For a media streaming service serving large sequential reads, you'd deliberately increase the block-device readahead window well beyond the default.
Structured elaboration
OS-level read-ahead. The kernel detects a sequential access pattern to a file (successive reads at increasing offsets) and issues additional reads ahead of what the application asked for, populating the page cache before those blocks are requested. This is controlled per block device via the readahead setting, viewable and settable with blockdev --getra/--setra /dev/sdX (values are in 512-byte sectors), and persisted across reboots via a udev rule that reapplies the setting when the device is detected.
Application-level prefetching. Applications can give the kernel explicit hints rather than relying purely on heuristic detection: posix_fadvise(fd, ..., POSIX_FADV_SEQUENTIAL) tells the kernel to be more aggressive about read-ahead for this specific file descriptor, and POSIX_FADV_WILLNEED explicitly requests the kernel start prefetching a specific range immediately. Databases commonly do their own index-page prefetching above the OS layer, and media servers or download managers may issue explicit range-request prefetching over the network, a similar idea one layer up the stack.
When it helps. Any workload with a genuinely predictable, sequential access pattern: streaming a large media file, a full table scan, sequential log processing. The core mechanism is hiding storage latency by overlapping "fetch the next chunk" with "process the chunk you already have," so by the time the application asks for the next piece, it's already in cache.
When it hurts. Random-access workloads, most notably databases doing index lookups scattered across a large file, where the "next" blocks read-ahead fetches speculatively are usually never actually requested. This wastes I/O bandwidth on the storage device (competing with real requests) and pollutes the page cache by evicting genuinely hot pages to make room for speculative ones that go unused, hurting both throughput (wasted device bandwidth) and latency (real requests now queue behind speculative ones, and useful cache entries get evicted).
Tuning for a media streaming service. Increase the block device's readahead window well above the default (commonly 128 KB) to something sized for the streaming bitrate and access pattern, for example:
blockdev --setra 8192 /dev/nvme0n1
8192 sectors of 512 bytes each is 4 MB, a much larger anticipatory read window suited to sustaining large sequential streams at high bitrate with fewer, larger I/O operations rather than many small ones. Pairing this with an explicit posix_fadvise(fd, 0, 0, POSIX_FADV_SEQUENTIAL) call from the serving application lets the kernel be aggressive about read-ahead for that file from the very first read, rather than waiting for its own heuristics to detect the sequential pattern after a few reads have already happened. Because this trades page cache capacity for prefetch depth, validate against your actual cache hit ratio on a multi-tenant streaming box: a readahead window large enough to help one stream but big enough to evict other tenants' hot content is a net loss.
Worked example
Default readahead (blockdev --getra typically reporting 256 sectors, 128 KB) on a media server sustaining a 20 Mbps stream means the kernel issues many small anticipatory reads to keep up, each one a separate I/O operation with its own queueing and completion overhead. Increasing that to 8192 sectors (4 MB) means far fewer, larger I/O operations are needed to sustain the same stream, which reduces per-operation overhead and gives the kernel a much bigger buffer of already-fetched data to hide any transient storage latency behind. The trade-off becomes visible on a multi-tenant box: if 200 concurrent streams each get the configured 4 MB working set of read-ahead data cached (matching the 8192-sector/4 MB setting above), that's 200 x 4 MB = 0.8 GB of cache committed to prefetch alone, worth explicitly sizing against total available cache rather than assuming a larger number is free.
Trade-offs and pitfalls
Applying a large readahead window globally to a device that serves both sequential (streaming) and random (metadata lookups, small file serving) workloads is the most common mistake: the random-access traffic pays the read-ahead tax (wasted bandwidth, cache pollution) without getting any of the sequential-access benefit. If the same device serves mixed workloads, prefer per-file posix_fadvise hints from the application over a single global blockdev setting, so the tuning only applies to the traffic that actually benefits from it.
Explain the difference between a buffer and a cache in the context of I/O and memory. Include concrete examples where each is used (socket buffers, OS page cache, application-level caches), describe how they affect latency and throughput, and when you'd prefer one over the other in a backend service.
Sample Answer
Direct answer
A buffer temporarily holds data in transit between two things operating at different speeds, and once that data is consumed it's typically gone; it exists to smooth throughput and absorb bursts. A cache holds a copy of data specifically so it can be reused later, trading memory for avoiding a repeated expensive operation; it exists to cut latency and reduce load on whatever it's standing in front of. In a backend service you generally need both, sized for their different jobs, rather than choosing one over the other.
Structured elaboration
Buffers, and what they're for. A socket buffer (configured via SO_RCVBUF/SO_SNDBUF) holds bytes that have arrived from the network before the application reads them, or bytes the application has written before the kernel has finished sending them. It decouples the producer's rate from the consumer's rate, letting each side proceed at its own pace within the buffer's capacity. The data isn't kept for later reuse; once consumed, it's gone. Effect on throughput and latency: a buffer that's too small causes stalls or drops under bursts (the sender must wait or the packet is lost); a buffer that's too large causes "bufferbloat" (data sits queued for a long time before being processed, adding latency even though nothing is technically being dropped).
Caches, and what they're for. The OS page cache (kernel-managed, holds recently accessed disk blocks) and application-level caches (an in-process LRU, least-recently-used, cache in front of a database, or an external cache like Redis) both exist to serve a repeat request faster than redoing the original work, and secondarily to reduce load on the backing system, fewer disk reads, fewer database queries, which indirectly helps everyone else sharing that backend. Unlike a buffer, the data is deliberately kept around for reuse, governed by an eviction policy (commonly LRU) or a TTL (time-to-live, an expiry after which an entry is considered stale and discarded).
How each affects latency and throughput, side by side. A buffer's main effect is on throughput smoothness and burst tolerance: sized correctly, it prevents drops and stalls from rate mismatches; sized wrong in either direction, it either drops data or adds unnecessary queuing delay. A cache's main effect is on latency for repeat access: a cache hit skips the original expensive operation entirely, and its secondary effect is throughput for the whole system, since offloading repeat requests from the backend frees capacity for everyone else's requests too.
When to prefer one over the other in a backend service. In practice you don't choose between them, you apply each to the problem it actually solves. Size buffers (socket buffers, request queues) to absorb realistic burst and jitter without excessive queuing delay, neither so small that normal bursts cause drops nor so large that queued data sits stale by the time it's processed. Add a cache specifically for data that's read far more often than it changes, where some staleness within your TTL window is acceptable. A common design mistake is using a cache where you actually need a buffer, for example trying to "cache" write-once event data that should really just be queued and drained, or the reverse, failing to add a cache in front of a backend that genuinely can't sustain the real read rate without one.
Worked example
A backend service accepting webhook events over a socket needs an appropriately sized SO_RCVBUF (a buffer) so a burst of simultaneously arriving events doesn't get dropped by the kernel before the application has a chance to read them; that data is processed once and discarded, no reuse involved. The same service, when it needs to look up a customer's plan tier to decide how to route each event, benefits from an application-level cache (an in-process map or an external cache like Redis) in front of the database, since the same customer's plan tier is looked up repeatedly across many events and rarely changes, making it a textbook case for reuse-oriented caching with a short TTL (a few minutes is often enough to keep the data reasonably fresh while eliminating the vast majority of repeat database lookups).
Trade-offs and pitfalls
Oversizing a buffer to "be safe" is a common mistake with a real cost: it doesn't prevent drops any better once the true burst exceeds even the larger buffer, but it does add latency to everything that queues behind a large backlog (bufferbloat), which can make a service feel sluggish under load even though nothing is technically failing. On the cache side, the common mistake is caching data that changes on every access (defeating any reuse benefit while adding cache-management overhead for nothing) or setting a TTL so long that stale data causes real correctness problems downstream.
Unlock Full Question Bank
Get access to all 10 System Resource & I/O Optimization interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.