Real production troubleshooting interview questions with detailed answers on outages, high CPU, disk full, latency and Kubernetes failures.
100questions with answers
Confirm the impact, tell people, and stabilise the system before you hunt for the root cause. Panicking and diving straight into logs is the most common mistake.
A good first five minutes looks like this:
Interviewers like this answer because it shows you separate "stop the bleeding" from "find the cause".
Mitigation makes the user impact stop quickly; a root cause fix removes the underlying defect so it does not return. During an incident you mitigate first, and you fix the root cause afterwards.
Examples of mitigation: rolling back a release, failing over to another region, restarting a stuck service, adding capacity, turning off a feature flag. These do not explain why the problem happened.
Root cause work (finding the bug, fixing the config, adding a missing index) can take hours or days and belongs in the postmortem and follow-up tasks.
The trap is spending 40 minutes proving the cause while customers are still down, when a 2-minute rollback would have ended the impact. The opposite trap is mitigating and then never doing the follow-up, so the incident repeats.
Search the kernel log for OOM killer messages. When the system runs out of memory, the kernel kills a process and writes the details to the kernel log, not to your application log.
dmesg -T | grep -i -E "out of memory|killed process"journalctl -k | grep -i "killed process"The message shows which process died, its memory use, and often the memory state of the whole machine.
Two points often missed:
oom_score, which usually favours large processes.Fixes: find and fix the leak, raise the memory limit, set sensible limits for other services, or add memory.
Find what is using the space, free some of it safely, then fix the reason it filled up.
df -h # which filesystem is fullsudo du -xh --max-depth=1 /var | sort -h | tail # drill down, one level at a timeThe usual culprits are logs, old Docker images and container logs, temp files, core dumps and package caches.
Safe ways to free space:
journalctl --vacuum-size=500M to shrink the systemd journal.docker system df then prune unused images (carefully, on a known host).truncate -s 0 file instead of rm, because deleting an open file does not free the space until the process closes it.Then prevent a repeat: set up log rotation, add a disk usage alert at 80%, and find out why the growth happened.
The filesystem has probably run out of inodes, not bytes. An inode is the record the filesystem keeps for each file. Millions of tiny files can use up all inodes while using very little space.
df -i # check IUse% on each filesystemsudo find /var -xdev -type f | cut -d/ -f1-4 | sort | uniq -c | sort -n | tailTypical sources are session files, mail queues, cache directories, build artefacts, and a runaway process creating tiny files.
Fix: delete the files you identified (using find ... -delete rather than rm *, which can fail with "argument list too long"). Long term, clean up automatically, move the data to a better storage design, or recreate the filesystem with more inodes if the workload really needs it.
Interviewers use this question to check that you do not stop at df -h.
The machine has run out of physical memory and is moving memory pages to disk, which is thousands of times slower than RAM. When it constantly swaps pages in and out, this is called thrashing.
vmstat 1 5 # watch the si (swap in) and so (swap out) columnsfree -hps aux --sort=-rss | headHaving some swap used is not a problem by itself. Continuous si and so activity is the danger sign.
Steps: find the memory-hungry process, reduce its load or restart it, then fix the underlying cause (a leak, a limit set too high, or too little RAM for the workload). Adding swap only delays the problem and slows the machine down further.
On Kubernetes nodes, swap is normally disabled so the scheduler's memory accounting stays accurate.
A 502 means the proxy could not get a valid response from the upstream (the backend app). The proxy itself is working; the thing behind it is not.
Check in this order:
ss -tlnp, systemctl status)curl -v http://upstream:port/ from the proxy.sudo tail -f /var/log/nginx/error.logThey point to different places, so the status code is your first clue.
| Code | Meaning | Usual cause |
|---|---|---|
| 500 | The app hit an unhandled error | A bug, bad data, a failing dependency |
| 502 | The proxy got a bad or no response from the upstream | Backend crashed, wrong port, connection reset |
| 503 | Service unavailable | No healthy backends, overloaded, in maintenance, rate limiting |
| 504 | The proxy timed out waiting for the upstream | Slow backend, slow dependency, timeout too short |
The first sentence of your answer to a "what does 5xx mean" question should name who generated it: 500 usually comes from the app, 502 to 504 usually from a proxy or load balancer in front of it. Always check which component generated the response, because a load balancer can return its own 503 without ever touching the app.
This points to a DNS problem: the name is not resolving to the right address from where you test. Check what the machine actually resolves.
dig +short api.example.com # what does DNS return?getent hosts api.example.com # what does the OS resolver return (includes /etc/hosts)?cat /etc/resolv.conf # which DNS server and search domains are used?Common causes:
/etc/hosts overriding DNS.Note that dig bypasses /etc/hosts and the OS cache, so also test with getent to see what applications actually get. Many programs also cache DNS themselves (the JVM does, by default under certain settings).
"Connection refused" means the host answered and said no, while "timed out" means nothing answered at all. The two point to different causes.
nc -vz host 443In practice: refused means "go look at the service on that host". Timed out means "look at the network path and the firewall rules". This one distinction saves a lot of time, so say it in your first sentence.
Test from the outside in: name, path, port, listener, firewall, application.
dig +short host.nc -vz host 8080 or curl -v.ss -tlnp | grep 8080. If it shows 127.0.0.1:8080, the app only accepts local connections and must bind to 0.0.0.0.iptables -S or nft list ruleset, plus tools like ufw or firewalld.Cloud flow logs (like VPC Flow Logs) show REJECT entries for blocked traffic. The usual cause of "works locally, fails remotely" is binding to localhost.
Another process is already listening on that port, or the previous instance has not released it yet. Find which one.
sudo ss -ltnp | grep ':8080'sudo lsof -i :8080Possible reasons:
TIME_WAIT after a restart. Servers normally avoid this with the SO_REUSEADDR option.hostPort conflicts with something on the node.Fix: stop or reconfigure the conflicting process, or change the port. Do not just kill -9 something you have not identified, because it may be an important service.
Read the container's logs and exit code first. Most failures are visible there.
docker ps -a # see the exit statusdocker logs <container> # the application's outputdocker inspect <container> --format '{{.State.ExitCode}} {{.State.OOMKilled}}'docker run -it --entrypoint sh <image> # explore the image yourselfCommon causes:
CMD or ENTRYPOINT, or the file is not executable.exec format error: the image was built for another CPU architecture (for example arm64 on an amd64 host).If the application runs in the background and exits (like a daemonised process), the container stops. Run it in the foreground.
The most common causes are unused images and containers, build cache, and container logs that grow without limit.
docker system df # shows space used by images, containers, volumes, cachedocker system prune # removes stopped containers, dangling images, unused networksdocker builder prune # removes build cachesudo du -sh /var/lib/docker/containers/*/*-json.log | sort -h | tailThe default json-file log driver has no size limit, so one chatty container can fill the disk. Set limits:
{ "log-driver": "json-file", "log-opts": { "max-size": "50m", "max-file": "3" } }Be careful with docker volume prune and docker system prune -a: they can delete data or images you still need. Check what will be removed first, and avoid running them blindly on production hosts.
CrashLoopBackOff means the container keeps starting and crashing, and Kubernetes waits longer between each restart (the "backoff"). The restart is not the bug. Find out why the container exits.
kubectl describe pod <pod> # events, last state, exit code, probe failureskubectl logs <pod> --previous # logs from the crashed run, not the new oneRead the exit code: 1 usually means an application error, 137 means killed (often OOM), 143 means it received SIGTERM.
Frequent causes: a missing config map, secret or environment variable, a wrong command or image, an application bug on startup, a failing liveness probe that kills a healthy but slow app, or an unreachable dependency at startup.
If there is no log output, use kubectl debug or run the same image with a shell override to test.
Pending means the scheduler has not placed the pod on a node yet, or the pod cannot start. The reason is almost always in the events.
kubectl describe pod <pod> # read the Events section at the bottomTypical messages and causes:
Remember that the scheduler uses requests, not actual usage. A cluster can look half empty and still be unable to schedule a pod with large requests.
The node cannot download the container image. The event message tells you which of a few causes it is.
kubectl describe pod <pod> # look for "Failed to pull image" and the reasonCheck:
imagePullSecret (or the node role needs registry permissions).Test from the node itself with the container runtime's pull command to separate cluster problems from node problems. As a good practice, use specific version tags or digests instead of latest, and mirror critical images in your own registry.
You are calling the API faster than its rate limit allows. Slow down, spread out your calls, and reduce the number of calls.
Short term: retry with exponential backoff and jitter (most SDKs have this built in, so make sure it is enabled and tuned).
Medium term:
Throttling is often shared across an account and region, so one misbehaving job can break other teams' automation. Add rate limiting on your side so you do not depend on the provider to protect you.
If a server's clock is wrong, anything that depends on time breaks in confusing ways. Typical examples are TLS certificate errors, expired tokens, failed request signatures and mixed-up logs.
Examples:
timedatectl status # is the clock synchronised?chronyc tracking # offset from the time source (with chrony)Fix: make sure an NTP service (chrony or systemd-timesyncd) is running and can reach a time server. In clouds, use the provider's time service. VMs that were paused or restored from a snapshot can have large jumps.
Several layers can block traffic, so check each one in turn instead of guessing: security groups, network ACLs, host firewalls and routes.
Checklist for a cloud network:
iptables -S, nft list ruleset, ufw status, firewall-cmd --list-all.REJECT entry shows traffic was blocked at the cloud layer.nc -vz destination-ip 5432A timeout suggests a drop by a firewall, while an immediate refusal suggests the port is closed on the host.
A zombie process is a child process that has finished but still has an entry in the process table because its parent has not collected its exit status. It shows as Z or <defunct> in ps.
ps -eo pid,ppid,state,cmd | awk '$3=="Z"'A few zombies are harmless: they use no CPU or memory, only a process table slot. A large number can exhaust the process ID table and prevent new processes from starting.
You cannot kill a zombie, because it is already dead. Instead, fix the parent: make the parent call wait(), or restart or kill the parent so that init (PID 1) adopts and reaps the zombies.
In containers, the main process often runs as PID 1 and does not reap children. Use a small init such as tini (docker run --init, or shareProcessNamespace and an init in Kubernetes) so zombies are cleaned up.
On a shared branch like main, use git revert, which creates a new commit that undoes the bad one. Do not rewrite shared history.
git revert <bad-commit-sha> # new commit that reverses the changegit push origin mainFor a merge commit, specify which parent to keep: git revert -m 1 <merge-sha>.
Then redeploy so production gets the reverting commit, or roll back the deployment to the previous artefact, which is often faster.
Why not git reset --hard and force push? That rewrites history that others have already pulled, which breaks their local branches and can drop other people's commits. reset is fine on your own local, unpushed work.
For a quick production fix, rolling back the deployment is often the fastest action, and the revert commit follows to keep the repository consistent.
Assign clear roles, use one channel, and share short regular updates that separate facts from guesses. Confusion costs more time than the technical problem.
Common roles:
Good practice:
The wrong approach is many people changing things in parallel with no coordination, which makes it impossible to know what helped or hurt.
A good postmortem explains what happened, why the system allowed it, and what will change, without blaming individuals. Its purpose is learning and prevention.
Typical sections:
Blameless does not mean nobody is accountable. It means you assume people acted sensibly with the information they had, and you ask "how did the system let this happen?" rather than "who did it?" People who fear blame hide information, and then you cannot learn.
The common weakness is a list of vague actions like "be more careful". Good actions change the system.
Walk the request path from the outside in, and test each layer until one fails. This turns a vague complaint into a specific broken component.
Practical steps:
dig +short example.com # does DNS resolve to the right IP?curl -v https://example.com/health # TLS, status code, timingcurl -v --resolve example.com:443:<backend-ip> https://example.com/health # bypass the LBFirst confirm the problem is not only on the user's side (try from another network). Then check whether DNS is right, whether the load balancer sees healthy targets, whether the app answers directly, and whether the app's dependencies (database, cache) respond. The first layer that fails is where you dig deeper.
Work out whether the problem is the network path, the SSH service, or the whole machine being starved of resources.
ping or nc -vz host 22 work? A timeout points to a firewall, security group or routing issue. "Connection refused" means the host is up but nothing listens on port 22.If you must recover by rebooting, capture evidence first when possible (a snapshot, console output). The common wrong answer is "just reboot it". That clears the symptom and destroys the evidence you need for the cause.
Find which process uses the CPU, then find out what type of CPU time it is, then connect it to what changed.
top # press 1 for per-core view, then check %us %sy %wa %stpidstat 1 5 # per-process CPU over timetop -H -p <pid> # which thread inside the processRead the CPU line carefully:
strace -c -p <pid>.Then correlate: did it start with a deploy, a cron job, a traffic increase or a new customer query? Mitigation is usually restarting the process, scaling out, or throttling the heavy workload. The CPU number alone never tells you the cause.
Look at "available" memory and per-process RSS over time, not at "free" memory. Linux deliberately fills spare RAM with the page cache, so low "free" is normal and healthy.
free -h # read the 'available' columnps aux --sort=-rss | head # biggest processes by resident memoryA real leak looks like this: one process's RSS grows steadily with no plateau, it drops only when the process restarts, and it correlates with traffic or runtime. Page cache growth, by contrast, shrinks automatically when applications need memory.
To dig deeper: check smem or /proc/<pid>/smaps for the breakdown, use a language-specific heap profiler (heap dumps for Java, pprof for Go, tracemalloc for Python), and compare a fresh process against an old one.
A short-term mitigation is a scheduled restart or a memory limit that restarts the process. That is a bandage; the leak still needs a fix.
A 504 means the proxy waited for the backend longer than its timeout. Raising the timeout is rarely the right first move because it hides slowness and ties up more connections.
Steps:
When a raise is acceptable: the request is genuinely long (a report, an export). Even then, a better design is to return quickly and process the work asynchronously.
A common trap: the load balancer idle timeout (for example 60 seconds) is lower than the app's real processing time.
Roll back by default when the change is the likely cause and rollback is safe and fast. Fix forward only when the fix is small, well understood, and rollback is risky or impossible.
Roll back when:
Consider fixing forward when:
The trap is "let's just push a quick fix", which stacks an untested change on a broken system. Restore service first, then investigate in a calm state.
Treat the deploy as the prime suspect, reduce impact first, then prove what changed. "Green health checks" only prove the app answers the health endpoint, not that real traffic works.
Prevention: use canary or progressive delivery with automatic rollback tied to real error and latency metrics, and make health checks meaningful.
"Nothing changed" is never true: something outside the repository changed. Look for the first real error, then check what the pipeline depends on that you do not control.
Steps:
Common culprits: an unpinned dependency (latest) that released a breaking version, and an expired credential. Prevention: pin versions, use lock files, cache or mirror dependencies.
Measure first: find which stage or step takes the time, then fix the biggest one. Optimising a step that takes 20 seconds out of 20 minutes is wasted effort.
Common fixes:
A trap: caching without a correct key, which makes builds fast but wrong. Always make caches invalidate when the inputs change.
Revert the configuration to the last known good version first, then work on why a bad config could reach production.
Response: find the exact change (config in version control makes this a one-line diff), revert it, restart or reload only what is needed, and confirm recovery with real traffic.
Prevention:
nginx -t, linting, dry runs).A frequent root cause is that config is edited by hand on servers. Treat config as code.
Renew and deploy the certificate on every place that serves it, then verify from outside. Afterwards, find out why the renewal did not happen.
Check the expiry:
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -datesFix: issue a new certificate, install it (including the intermediate chain), and reload the service, load balancer or ingress. Remember every endpoint that has its own copy: CDN, load balancers, internal services, mail servers.
Why it failed: a broken renewal job (cert-manager or certbot), failed ACME challenge, changed DNS, expired API credentials, or a manual process nobody owned.
Prevention: automate renewal, and monitor the certificate expiry date with alerts at 30, 14 and 7 days. Alerting on the renewal job alone misses cases where the job silently never ran.
The process has reached its limit of open file descriptors. Sockets and pipes count as files too, so this affects busy network services.
cat /proc/<pid>/limits | grep "open files" # the process's real limitls /proc/<pid>/fd | wc -l # how many it uses nowlsof -p <pid> | awk '{print $5}' | sort | uniq -cDecide whether it is a limit that is too low or a leak. If the count grows steadily and never falls, something is not closing connections or files. If it is stable near a low limit like 1024, raise it.
Raising the limit: for systemd services set LimitNOFILE=65535 in the unit file (the shell's ulimit does not apply to services). For containers, set --ulimit nofile.
Raising the limit on a leak only postpones the failure, so also fix the leak (unclosed HTTP clients, missing close() calls).
Exit code 137 means the process was killed by signal 9 (SIGKILL), because exit codes above 128 are 128 plus the signal number (128 + 9 = 137). Most often it means the container ran out of memory.
docker inspect <container> --format '{{.State.OOMKilled}}'If this prints true, the container hit its memory limit and the kernel killed it. In Kubernetes, kubectl describe pod shows Reason: OOMKilled under the last state.
Other reasons for 137: someone ran docker kill or kill -9, or an orchestrator killed the container after it ignored a stop request for too long.
Related codes worth knowing: 143 is SIGTERM (a normal graceful stop), 139 is a segmentation fault (SIGSEGV), and 1 is a generic application error.
Fix OOM by raising the limit or reducing memory use. Check that the runtime respects the container limit (older JVMs read host memory).
The container used more memory than its limit, so the kernel killed it. You either need to give it more memory or reduce what it uses.
kubectl describe pod <pod> # Last State: Terminated, Reason: OOMKilled, Exit Code: 137kubectl top pod <pod> --containersSteps:
requests and limits.The memory limit counts everything in the container, including page cache from files and native memory. Setting the limit exactly at the average usage guarantees kills at the peaks.
Check whether the Service has endpoints. A Service forwards traffic only to pods that match its selector and are ready.
kubectl get svc,endpoints <service>kubectl get pods --show-labelskubectl describe svc <service>If the endpoints list is empty:
selector does not match the pod labels (a typo is very common).If endpoints exist but traffic fails:
port and targetPort do not match the container's real port.127.0.0.1 inside the pod.Test from inside the cluster with a debug pod: kubectl run tmp --rm -it --image=busybox -- wget -qO- http://service:port. Test the pod IP directly to separate Service issues from app issues.
The new pods are failing their readiness probe (or crashing), so the Deployment will not remove old pods. That is the rolling update protecting you.
kubectl rollout status deployment/<name>kubectl describe pod <new-pod> # probe failures in Eventskubectl logs <new-pod>Check the probe settings:
initialDelaySeconds allows. Use a startupProbe for slow starters.Mitigation: kubectl rollout undo deployment/<name> returns to the previous ReplicaSet. Because of maxUnavailable and maxSurge, a stuck rollout normally keeps most old pods serving, so users are often still fine while you debug.
The kubelet evicts pods when a node runs low on a resource such as memory, disk or inodes, to protect the node. Eviction is the node defending itself.
kubectl describe pod <pod> # the Message explains the pressure typekubectl describe node <node> # check DiskPressure / MemoryPressureCommon cases: the node's ephemeral storage is full (container logs, emptyDir volumes, image layers) or memory is low.
Which pods go first: pods using more than their requests, and those with the lowest QoS class (BestEffort, then Burstable). Pods with requests equal to limits (Guaranteed) go last.
Fix: set realistic requests and limits, set ephemeral-storage limits, rotate logs, and clean up the node. Evicted pods remain as records, so delete them with kubectl delete pods --field-selector=status.phase=Failed.
The trap is deleting evicted pods without fixing the pressure: new ones will be evicted again.
A 404 usually means the ingress controller found no matching rule, while a 503 means a rule matched but there is no healthy backend to send traffic to.
For a 404:
Host header or path does not match any Ingress rule.ingressClassName points to another controller, so your controller ignores it.For a 503:
kubectl describe ingress <name>kubectl get endpoints <service>kubectl -n ingress-nginx logs deploy/ingress-nginx-controllerAlso confirm that TLS secrets exist in the same namespace as the Ingress. Test by curling the controller with the right Host header: curl -H "Host: app.example.com" http://<controller-ip>/.
The database reached its maximum number of client connections. Find out who is holding them before you raise any limit.
For PostgreSQL:
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;SELECT client_addr, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;Look for many connections in idle or idle in transaction, and for one service or host using most of them.
Common causes: connection leaks in the app, too many app instances each with a large pool (instances times pool size can exceed the DB limit), long transactions, or a retry storm after an earlier failure.
Fixes: correct the pool sizes, fix leaks, set timeouts for idle transactions, and add a pooler such as PgBouncer.
The trap is just raising max_connections. Each connection uses memory and the database often gets slower with more of them, so a pooler is usually the better answer.
Check from the application's point of view, layer by layer, and compare with what changed.
nc -vz db-host 5432 from the app host. Check security groups, firewalls, routes.The error text matters: "timeout" suggests network, "connection refused" suggests nothing is listening, "authentication failed" suggests credentials, and "too many connections" suggests capacity.
Replication lag means the replica applies changes more slowly than the primary produces them. The result is stale reads, and a risk of data loss if the primary fails and you promote the replica.
Common causes:
Check lag with pg_stat_replication (PostgreSQL) or SHOW REPLICA STATUS (MySQL). Cloud databases expose a lag metric you should alert on.
Mitigation: stop heavy queries on the replica, resize it, throttle the writes, or route lag-sensitive reads to the primary. Applications that need "read your own writes" must not read from a lagging replica right after a write.
Yes. If the application's connection pool is exhausted, requests wait in line for a free connection before they ever reach the database, so the database looks quiet while users wait.
Signs:
Causes:
Fix the leak or the slow query first. Making the pool bigger is the least effective fix, and it can overload the database. Always expose pool metrics (active, idle, waiting) so you can see this directly.
Check how Redis is configured to behave at its memory limit, then find what is using the memory. The behaviour depends on maxmemory and the eviction policy.
redis-cli INFO memory # used_memory, maxmemory, evicted_keysredis-cli CONFIG GET maxmemory-policyredis-cli --bigkeys # find the largest keys (run on a replica if possible)noeviction, Redis refuses writes when full and clients see "OOM command not allowed". That is correct behaviour for data you cannot lose.allkeys-lru, Redis deletes keys to make room, so you may see a falling cache hit rate instead of errors.Common causes: keys without a TTL, a few huge keys (large lists or hashes), and a growth in traffic.
Fixes: add TTLs, remove or split big keys, raise memory, or add shards. Choose the policy on purpose: a cache can evict, but a session store or queue usually must not.
Consumer lag means the consumers read more slowly than producers write. First find out whether consumers are slow, stuck, or constantly rebalancing.
kafka-consumer-groups.sh --bootstrap-server broker:9092 --describe --group my-groupLook at the lag per partition:
max.poll.interval.ms).Fixes: speed up processing, add consumers (up to the number of partitions), fix the slow dependency, tune batch size, or add partitions with care (it changes key ordering). A poison message that fails repeatedly can also freeze one partition, so use a retry topic and dead-letter topic.
Find out whether messages are arriving faster than they are processed, or whether processing has stopped. The age of the oldest message tells you more than the queue depth.
Check:
Actions: scale workers if the bottleneck is processing power, fix or bypass the slow dependency, move failing messages to a dead-letter queue after a few attempts, and throttle or pause producers if needed.
Watch for the trap in visibility timeouts: if a message takes longer than the timeout, it becomes visible again and is processed twice, which makes the backlog worse. Make handlers idempotent.
First check whether the targets really are broken, or whether the health check itself is wrong. A bad health check can take down a healthy fleet.
Check:
curl the health check URL from a host in the same network./health queries the database and the database blips, every target goes unhealthy at once, and then the load balancer has nothing to send traffic to.Good practice: keep the health endpoint cheap and focused on "can this instance serve traffic". Many load balancers have a fail-open mode that sends traffic anyway when all targets are unhealthy, so know how yours behaves.
The load balancer is balancing connections or requests in a way that does not match real work, so look at connection behaviour and balancing rules.
Common causes:
Fixes: use least outstanding requests balancing, set a maximum connection age so clients reconnect and rebalance, use client-side or L7 load balancing for gRPC, reduce reliance on sticky sessions, and spread hot keys.
Check per-instance metrics, not only the fleet average, because the average hides the imbalance.
Check the whole chain: is the metric reported, is the scaling rule triggered, and is there capacity to add? Any broken link stops scaling.
resources.requests.cpu to be set on the pods.Pending because the cluster autoscaler cannot add nodes.kubectl describe hpa <name> # shows current metrics, conditions and eventsAlso check whether the scaled-out instances were created but failed health checks, which looks like "it did not scale" from the outside.
AWS runs two checks: the system status check tests AWS's underlying hardware and network, and the instance status check tests your operating system inside the instance.
fstab entry or exhausted memory.For instance check failures, use console output and the instance screenshot to see boot messages. If the OS will not boot, stop the instance, detach its root volume, attach it to a rescue instance, fix the files, and attach it back.
Take a snapshot before risky repair steps. Prevent repeats with an alarm on StatusCheckFailed and with automation that replaces failed instances instead of repairing them by hand.
Find the exact denied API call and the identity that made it, then work out which policy layer denied it. CloudTrail records both.
Steps:
The trap is only editing the role's policy. The block is often an SCP, a resource policy or a KMS key policy in another place.
A 403 on S3 means the request was understood but denied by some layer. Check from the identity side and the bucket side.
Likely causes:
kms:Decrypt or kms:GenerateDataKey on that key.Use CloudTrail or S3 server access logs to see the reason, and test with aws s3api head-object using the same credentials. Note that S3 may return 403 instead of 404 when the caller lacks s3:ListBucket, which hides whether an object exists.
Do not panic and do not edit the state by hand. Find out why it failed, check whether another run still holds the lock, then re-run plan.
If the apply failed halfway, some resources were created and recorded in state, others not. Terraform saves what it did, so run terraform plan again: it shows the remaining difference. Fix the cause (a permissions error, a quota, an invalid value) and apply again.
If you get "Error acquiring the state lock":
terraform force-unlock <LOCK_ID>.If the lock stays after a crashed run, force-unlock is the correct fix. If a real apply is still running and you unlock it, two applies can corrupt state.
Always use a remote backend with locking, and keep versioning on the state storage so you can restore an earlier version.
Drift means the real infrastructure no longer matches what Terraform's state and code describe, usually because someone changed it manually in the console or another tool changed it.
terraform plan -refresh-only # shows what changed outside Terraform without proposing code changesDecide for each difference:
apply to put the infrastructure back to the code.terraform apply -refresh-only to accept the new real values in state).terraform import or an import block, or delete it.Prevention: limit console write access, run scheduled plan jobs that alert on differences, and make the pipeline the only way to change infrastructure. Also note that some attributes change by design (tags added by a cloud service), which you can handle with ignore_changes.
The servers have drifted apart in configuration, packages or data. Compare the good and the bad server, then fix the way servers are built so drift cannot happen.
Compare:
dpkg -l or rpm -qa.diff or checksums on key files, or ansible --check --diff to show what would change.sysctl -a, plus limits and mounted filesystems.After you find the difference, ask how it got there: a manual hotfix, a failed run of the config tool, an automatic update that happened on only some hosts, or an old base image.
The best fix is immutable infrastructure: replace the server from a known image instead of repairing it, so every server is identical and any manual change disappears at the next replacement.
OutOfSync means the live cluster state differs from what Git says it should be, while Degraded means the resources exist but one or more is unhealthy (for example a Deployment whose pods are not ready). They are separate: an app can be synced and degraded.
argocd app diff <app> # what differs between Git and liveargocd app get <app> # sync status and health per resourcekubectl describe <resource> # then look at the unhealthy resource's eventsCauses of OutOfSync: someone changed a resource manually with kubectl, a controller mutates a field (like replicas set by an HPA, or an injected sidecar), or a sync failed because of an invalid manifest or an immutable field.
Fixes: sync again, fix the manifest in Git, or tell Argo to ignore fields that other controllers manage (ignoreDifferences). With selfHeal on, Argo reverts manual changes, which is usually what you want in production.
For Degraded, debug the unhealthy workload like any Kubernetes problem.
Look at the release status and history, then roll back to the last working revision. Then find out why the upgrade failed.
helm status <release> -n <ns>helm history <release> -n <ns>helm rollback <release> <revision> -n <ns>Common causes:
helm template or helm lint and --dry-run --debug.clusterIP or a StatefulSet volume claim template).pending-upgrade because an earlier run was interrupted.Prevention: use helm upgrade --atomic so a failed upgrade rolls back automatically, and run helm diff in CI to see what will change before it happens.
Restore access first, usually by making the app use the new secret (or briefly the old one, if it is still valid), then fix the rotation process.
Typical causes: the application read the secret at startup and still uses the old value, the new value was not delivered to every consumer (some pods, a CI job, an old server), or the old secret was revoked before everything switched.
Immediate steps: confirm the error is authentication in the logs, restart or reload the application so it reads the new secret, and check every consumer of that credential.
Prevention:
The trap is rotating by "change it now, fix what breaks".
Test the handshake yourself with openssl s_client and read what the server presents and why the client rejects it.
openssl s_client -connect example.com:443 -servername example.com -showcertsCommon causes:
Always test from the failing client's point of view, because browsers hide problems (like a missing intermediate) that libraries and tools do not.
Measure latency and throughput between the two hosts, and then look for packet loss and retransmissions. Do not blame the network without numbers.
ping -c 20 host # round-trip time and lossmtr -rw host # per-hop latency and lossiperf3 -c host # throughput between the two hostsss -ti dst host # TCP details: retransmits, rtt, congestion windowThings to check:
Often the network is fine, and the real issue is making too many sequential calls, so check the number of round trips as well as the time per trip.
No. Loss at a single intermediate hop that does not continue to the final destination is usually just that router limiting its replies to probes, and it does not affect real traffic.
How to read it:
mtr -rwc 100 example.comRun more probes (100 or more) for a stable view, and test in both directions, since paths can differ. Confirm with real traffic measures such as TCP retransmits (ss -ti, netstat -s), and check interface drops with ethtool -S and ip -s link on your own hosts.
Find out exactly where the alerting chain broke, then add alerts based on user impact rather than on internal causes only.
Possible failures:
Improvements:
Add a fix for detection to the postmortem, because "we learned about it from customers" is a finding in its own right.
Treat the noisy alert as a bug. A page should mean "a human must act now". If it does not, change or remove it.
Ask for each noisy alert:
Track the alerts per shift and the share that needed action. Constant false alarms cause alert fatigue, and then real incidents get ignored.
Also fix the cause of the repeated issue: a recurring alert often points to a known problem that nobody has prioritised.
Follow the log path hop by hop and find where the data stops: application, collector agent, transport, storage and search.
The trap is searching only in the log UI. Check the agent's own health and metrics first.
Check its state, what system call it is blocked in, and what it is waiting on (disk, network, a lock or another process).
ps -o pid,state,wchan:20,cmd -p <pid> # state and kernel wait channelsudo strace -f -p <pid> # live system calls (stuck in read, futex, connect?)sudo lsof -p <pid> # open files and connectionssudo cat /proc/<pid>/stack # kernel stack (if allowed)Interpretation:
futex or pthread_cond_wait: waiting on a lock, possibly a deadlock.read or recvfrom on a socket: waiting for a remote service that never answered, so look at that connection in lsof and ss.D: blocked in I/O, probably a hung disk or network filesystem.For managed runtimes, take thread dumps (jstack, kill -3 for Java, py-spy dump for Python), and take several a few seconds apart to see what is not moving.
Check when it rebooted and what the logs say just before the restart, then separate software, hardware and cloud causes.
uptime -s ; last -x reboot shutdown | headjournalctl --list-bootsjournalctl -b -1 -e # end of the previous boot's logPossible causes:
kdump produced a crash dump.panic_on_oom, or a hung system that a watchdog rebooted.If the previous boot's log is missing, enable persistent journald storage so the next reboot leaves evidence.
First decide which memory is growing: the Java heap, or non-heap memory (metaspace, threads, native buffers). Then capture evidence while the problem is present.
Steps:
jstat -gcutil <pid> 1000. A heap that stays high after full garbage collections suggests a leak.jmap -dump:live,format=b,file=heap.hprof <pid>, or start the JVM with -XX:+HeapDumpOnOutOfMemoryError so you get one automatically at the crash.-Xmx plus metaspace, thread stacks and native memory. Otherwise the container is OOM-killed without a Java OutOfMemoryError.Use -XX:MaxRAMPercentage for container-aware sizing, and enable Native Memory Tracking if the heap looks normal but the process keeps growing.
Revoke and rotate the key immediately, then investigate whether it was used. Removing it from the repository is not enough, because bots scan public repositories within minutes.
Steps, in order:
Prevention: pre-commit secret scanning, repository secret scanning and push protection, short-lived credentials (roles and OIDC federation instead of long-lived keys), and least-privilege permissions to reduce the blast radius.
Confirm the outage, limit its effect on your users, and communicate. You cannot fix the vendor, but you can control how much their failure hurts you.
Immediate steps:
Design lessons: every external call needs a timeout, retry limits and a fallback plan, and critical dependencies deserve a second provider or an acceptable degraded mode. Check your own SLA, since your availability cannot exceed that of a hard dependency.
Prefer actions that change the system over actions that rely on people remembering. Rank ideas by how strongly they prevent, detect or limit the problem.
A useful order, strongest first:
For every action ask: "if this exact situation happened again, would this have stopped or shortened it?" Give each action an owner, a deadline and a place in the normal backlog. Actions without owners are a common reason incidents repeat.
Collect enough data continuously so that the next occurrence explains itself, and look for patterns in when it happens.
Look for correlations in the timing:
Capture evidence automatically:
top, ss -s, a thread dump and iostat output).Compare a bad period with a good one, side by side. The wrong approach is waiting for the problem to happen again and starting to look then, because it will already be gone.
Linux load average counts processes that are running and processes stuck in uninterruptible sleep (state D, usually waiting for disk or network storage). So a high load with idle CPUs usually means processes are blocked on I/O.
vmstat 1 5 # look at 'b' (blocked) and 'wa'iostat -x 1 5 # high await and %util on a deviceps -eo state,pid,cmd | awk '$1=="D"' # list processes in D stateTypical causes: a slow or throttled disk (for example a cloud volume that hit its IOPS limit), a hung NFS or network mount, a failing disk, or a storage backend problem.
The common wrong answer is to say "the CPU is idle so the server is fine". Load average is about demand for the system, not only CPU. Fix the storage bottleneck, not the CPU.
The most likely cause is deleted files that a running process still holds open. The filesystem cannot release the space until the last file handle is closed, so df still counts it while du cannot see the file.
sudo lsof +L1 # open files with link count 0 (deleted)sudo ls -l /proc/<pid>/fd | grep deletedThis often happens when someone runs rm on a big log while the application is still writing to it, or when log rotation does not signal the process to reopen its log file.
Fixes:
: > /proc/<pid>/fd/<n>.Other reasons for a mismatch: data hidden under a mount point (files written to a directory before a volume was mounted over it), and reserved blocks on ext4. Fix the rotation setup so it does not happen again.
First find where in the request path the time is spent, then look at what changed. Do not guess a cause before you know the slow layer.
The trap is looking only at the application server's CPU. Many latency problems live in a dependency or in queueing, not in the app's own computation.
Averages hide tail latency. If 1% of requests take 5 seconds, the average barely moves, but with many requests per page or per session, almost every user hits a slow one.
Look at p95, p99 and p99.9 using histograms, and break them down by instance, endpoint and time.
Common causes of slow tails:
Method: find slow requests in traces, then compare them with fast ones (same endpoint, which host, which dependency). If a request fans out to many backends, the slowest backend decides the total, so tails get amplified.
Never report only the average in an incident.
Split the failing requests by every dimension you can until the pattern appears. Intermittent errors almost always have a hidden pattern.
Slice the access logs and metrics by:
Other classic causes: a deployment with mixed versions, one backend with a stale DNS or config, connection reuse races (a keepalive connection closed by one side just as the other sends), and retries from a downstream dependency.
If errors are too rare to catch, add more logging, or capture traffic with tcpdump on the suspect host. The wrong approach is to look only at overall averages, since 2% disappears into the noise.
Rolling back the code may be unsafe if the old version cannot work with the new schema or data. Decide based on what the migration did and whether the old code still functions.
Options:
The real answer is prevention: use the expand and contract pattern. First add new columns or tables while old code still works (expand), deploy code that uses both, migrate the data, and only later remove the old structure (contract). Each step is then reversible.
The trap is a destructive migration (drop or rename column) in the same release as the code change.
Test each resolver separately and look for packet loss, because intermittent DNS failures usually come from one bad resolver in the list or from dropped UDP packets.
for i in $(seq 1 50); do dig +time=1 +tries=1 +short example.com @10.0.0.2 || echo FAIL; doneRun the loop against each resolver in /etc/resolv.conf and compare. Also check:
ndots: each name can create several lookups, multiplying load (common in Kubernetes).tcpdump -i any port 53 and look for queries without answers.Mitigations: a local caching resolver on each node, fewer lookups, more reliable resolvers, and TCP fallback for large responses.
You are likely running out of ephemeral ports. Every outbound TCP connection to the same destination IP and port needs a unique local port, and a closed connection keeps its port reserved in TIME_WAIT for about 60 seconds.
ss -s # summary, includes timewait countsysctl net.ipv4.ip_local_port_range # default is roughly 28,000 portsss -tan state time-wait | wc -lIf each request opens a new connection, around 28,000 connections per minute to one destination exhausts the range.
Fixes, best first:
ip_local_port_range.The wrong fix is tuning dangerous tcp_tw_* settings first. Connection reuse in the application is the real solution.
NotReady means the control plane stopped getting healthy heartbeats from the node's kubelet. Find out whether the node, the kubelet, the network or the runtime failed.
kubectl describe node <node> # Conditions: MemoryPressure, DiskPressure, PIDPressure, Ready# then on the node (if reachable):systemctl status kubeletjournalctl -u kubelet --since "30 min ago"systemctl status containerddf -h; free -hCommon causes: the kubelet crashed, the container runtime is down, the disk or memory is exhausted, the CNI network plugin failed, or the VM itself died or lost network.
What happens to pods: after the node stays unreachable (about 5 minutes by default through taint-based eviction), the control plane marks its pods for eviction and controllers recreate them on healthy nodes. Pods with local data or single replicas suffer downtime.
Mitigation: cordon the node, drain it if it responds, and replace it rather than repairing it, especially in autoscaled clusters.
Common causes are the ndots:5 setting multiplying lookups, an overloaded CoreDNS, and conntrack races on UDP.
Pods get ndots:5 in /etc/resolv.conf. Any name with fewer than five dots (like api.example.com) is first tried with every search domain (api.example.com.default.svc.cluster.local, and so on) before the real name, so one lookup becomes four or five, with failures in between.
Check:
kubectl exec -it <pod> -- cat /etc/resolv.confkubectl -n kube-system top pods -l k8s-app=kube-dnskubectl -n kube-system logs -l k8s-app=kube-dnsFixes:
api.example.com.) or lower ndots in the pod's dnsConfig.Many apps look slow only because of DNS, so measure the lookup time separately.
Find the offending query and what is blocking it, stop the damage, then fix the cause. The fix for the outage and the fix for the query are separate steps.
Find it:
-- PostgreSQL: longest running queriesSELECT pid, now() - query_start AS runtime, state, wait_event_type, queryFROM pg_stat_activity WHERE state <> 'idle' ORDER BY runtime DESC LIMIT 10;-- who blocks whomSELECT pid, pg_blocking_pids(pid) AS blocked_by, query FROM pg_stat_activity WHERE cardinality(pg_blocking_pids(pid)) > 0;MySQL has the slow query log and SHOW PROCESSLIST.
Immediate response: cancel or kill the runaway query (pg_cancel_backend), or kill the blocker holding a lock. Shift reads to a replica if possible.
Typical root causes: a missing index after the table grew, a query plan that changed after new statistics, a lock held by a long transaction, or an unbounded query from a new feature. Check with EXPLAIN (ANALYZE). Add the index with CREATE INDEX CONCURRENTLY so you do not block writes.
A cache stampede happens when a popular cache entry expires and many requests all miss at once and hit the database together. The database gets overwhelmed, slows down, and the cache stays empty longer, which makes it worse.
Prevention techniques:
A related problem is a cold cache after a deploy or cache restart, where everything misses at the same time. Put a rate limit in front of the database so it can survive a cold start.
The wrong answer is simply setting a longer TTL. That only delays the stampede.
A retry storm is when failing requests are retried by many clients, multiplying the load on an already struggling service so it cannot recover.
During the incident: reduce incoming load (rate limit, shed traffic, disable the retrying feature), and let the service breathe before restoring traffic gradually.
Prevention in code and design:
A common wrong answer is "add more retries to improve reliability". Retries help with brief glitches but cause outages when the failure lasts.
A cascading failure is when a problem in one component spreads to others, usually because callers wait on a slow dependency, run out of threads or connections, and then fail themselves.
How it spreads: service A waits for slow service B, A's worker threads fill up waiting, A stops answering its own callers, and the failure moves up the chain.
Stop it during the incident:
Design defences:
The trap is fixing only the visible failing service while the real cause is the missing timeouts upstream.
Scaling is reactive, so by the time CPU is high and new instances finish starting, the spike has already hurt users. Reduce the total reaction time and scale on earlier signals.
The delay has several parts: metric collection delay, the rule's evaluation period, instance or pod startup time, image pull, and application warm-up.
Improvements:
The trap is to only tune thresholds. If startup takes five minutes, no threshold will save you from a spike that grows in thirty seconds.
Confirm the scope, move traffic away from the affected zone or region, and rely on capacity that already exists. Do not depend on actions that need the broken part to work.
Steps:
The trap is an untested failover plan. Practise it with game days, and keep runbooks that do not depend on the failing region (for example the CI system or the secrets store in the same region).
Stop and do not apply. A destroy and create on a stateful resource means data loss unless you prove otherwise, so find out exactly why Terraform wants to do it.
Read the plan: it marks the attribute that "forces replacement". Typical reasons:
Fixes: for renames or refactors use a moved block or terraform state mv. Revert the code change if the attribute should not change. Take a backup or snapshot before any risky change.
Safeguards: set lifecycle { prevent_destroy = true } on critical resources, turn on deletion protection in the cloud service, and require human review of plans that contain destroys.
Suspect an MTU or path MTU discovery problem. Small packets pass, but larger packets are dropped somewhere on the path because they exceed a link's maximum size and the "fragmentation needed" message never reaches the sender.
This often appears with VPNs, tunnels, overlay networks (VXLAN, IPsec, GRE) and some cloud links, which add headers and reduce the usable MTU.
Test:
ping -M do -s 1472 host # don't fragment, 1472 + 28 header bytes = 1500tracepath host # reports the path MTUIf 1472 fails but a smaller size such as 1400 works, you have found it.
Typical symptoms: the TCP connection opens fine (small packets), TLS handshakes stall (the certificate message is big), and then large responses hang.
Fixes: allow ICMP "fragmentation needed" through firewalls, lower the MTU on the interface or tunnel, or use TCP MSS clamping on the gateway. Blocking all ICMP is the classic cause of this problem.
High cardinality means a metric has a huge number of unique label combinations, and every combination is a separate time series that Prometheus must hold in memory. Memory use grows with the number of active series.
Typical cause: a label with unbounded values, such as user IDs, request IDs, full URLs with parameters, or pod names that change constantly.
Find the culprits:
# PromQL: which metrics have the most seriestopk(10, count by (__name__) ({__name__=~".+"}))# and watch prometheus_tsdb_head_seriesThe TSDB status page in the Prometheus UI also lists top metrics and labels.
Fixes:
/users/{id} instead of raw paths).metric_relabel_configs.sample_limit or label_limit on scrape jobs so one bad target cannot hurt everyone.Adding RAM only delays the problem, so find the label first.
Enable GC logging and compare pause times with the latency spikes. If the spikes line up with GC pauses, the collector is the cause.
For Java:
java -Xlog:gc*:file=gc.log:time,uptime ...Look for long "stop the world" pauses, frequent full GCs, a heap that stays almost full after collection, and a very high allocation rate.
Typical causes and fixes:
The same idea applies to Go, .NET and others: check the runtime's GC metrics against latency graphs.
The trap is blaming the network or the database before checking the process's own pauses. Also note that a container CPU limit can throttle GC threads, so check CPU throttling metrics too.
When all worker threads are blocked, the process can still accept connections but never finish any work. Thread dumps show what each thread waits for.
Take three thread dumps a few seconds apart (jstack <pid> or kill -3 for Java, py-spy dump for Python, SIGQUIT for Go goroutines). Then look at the pattern:
jstack reports detected deadlocks directly.Mitigation is a restart (take the dumps first). Fixes: set timeouts on every outbound call, use separate pools (bulkheads) per dependency, bound queue sizes so work is rejected instead of piling up, and keep lock ordering consistent.
The trap is restarting before capturing a dump, which destroys the only evidence.
Stop further damage, preserve what you can, and restore from the best available source, while communicating clearly. Avoid panic commands that could make recovery harder.
Afterwards, run a blameless postmortem. The system allowed one person to delete production, so add guards: deletion protection, separate roles for destructive actions, approval for risky operations, and backups that are tested and stored separately from the production account.
An untested backup is only a hope. Restores fail because backups were never verified: they were corrupted, incomplete, encrypted with a lost key, or missing pieces such as transaction logs.
Typical causes:
During the incident, try an earlier backup, and use any replica or snapshot that still exists.
Prevention:
Isolate it, preserve evidence, and involve your security team. Do not immediately wipe or reboot it, because that destroys the evidence you need to learn what happened.
The trap is "clean it and move on". You will not be sure the attacker is gone.
Look at who is sending the traffic and what they ask for. The traffic pattern tells you whether it is a marketing success, a bot, or an attack.
Analyse the logs and edge metrics by:
Responses:
Also confirm that your autoscaling and database can survive it, because unbounded autoscaling during an attack can produce a huge bill. Decide in advance who may enable emergency protections.
Find out what is different about production, and use the data production gives you. Common differences explain most "only in prod" bugs.
Typical differences:
How to investigate:
Avoid making changes directly on production without a plan, and avoid exposing sensitive data in debug output.
Yes. They follow the scenarios that interviewers commonly use for DevOps, SRE and platform roles, such as outages, resource exhaustion and failed deployments. The answers are written in our own words with the reasoning behind each step.
It suits DevOps, SRE, platform and cloud engineers at junior to senior level. Easy questions cover core ideas, while hard questions are open scenarios where the answer depends on the evidence you find.
State the first thing you check and why, explain what each result would tell you, and say how you would mitigate before finding the root cause. Interviewers care more about your method than about one specific command.