Where to look first
A checklist that finds most problems in minutes.
Part 1: The problem
Kofi was on call when checkout started failing. He opened the orders service’s code, because that’s what he knew best. He read the checkout function twice. He added more logging and deployed it. He restarted the servers. Forty minutes passed.
Then Ana joined the call and asked one question: “What does the app actually get back?” The answer was a timeout — not an error from the code, just silence. That pointed straight at the network. Five minutes later they found it: a security group rule for the database had been removed that evening.
The code had never been the problem. The symptom had said so from the start.
When something breaks, the first job isn’t to fix it. It’s to find out which layer is broken — and the symptom usually tells you, if you read it exactly.
Part 2: The idea
A request passes through several layers: DNS, the network, the load balancer, the server, the app, and the app’s dependencies. Each layer fails in its own way. So the exact thing the client sees tells you where to start.
The four symptoms
| Symptom | What happened | Look first at |
|---|---|---|
| Timed out, while connecting | Nothing answered — not even “no”. Something dropped the packets | Security groups, route tables, network ACLs, the address itself |
| Connection refused | The machine was reached and said “nothing is listening here” | Is the app running? On the right port? Listening on the right address? |
| 5xx | The server side received the request and failed | Load balancer target health, app logs, the app’s dependencies |
| 4xx | The server says the request was wrong | The path, the login, the input. Restarting servers won’t fix it |
This is what each looks like from curl, the command-line tool for making requests:
curl: (28) Failed to connect to 10.0.11.24 port 8080 after 75004 ms: Timeout was reachedcurl: (7) Failed to connect to 10.0.11.24 port 8080 after 2 ms: Couldn't connect to serverNewer versions of curl say “Couldn’t connect to server” for a refusal; add -v to see the words Connection refused. Notice the times, too: a timeout takes seconds or minutes; a refusal comes back almost instantly.
There’s one more kind of timeout. If the connection opens but the app never answers, curl says:
curl: (28) Operation timed out after 5006 milliseconds with 0 bytes receivedHere the network worked. The app, or something the app is waiting on, is stuck. That’s a job for the logs, not the security groups.
What the load balancer’s 5xx codes mean
When the load balancer answers with a 5xx, the number tells you what went wrong between it and your servers:
| Code | Means | Look at |
|---|---|---|
| 502 Bad Gateway | A server answered, but badly: it closed the connection or sent something that isn’t valid HTTP | App crashes and restarts; the target group expecting HTTP when the app speaks HTTPS, or the other way round |
| 503 Service Unavailable | There’s no healthy server to send the request to | Target group → Targets |
| 504 Gateway Timeout | A server didn’t answer in time | The app’s security group; a slow app or a slow dependency |
| 500 Internal Server Error | Usually from your app itself, passed through | The app’s logs |
The places to look, and how to get there
Target health. The load balancer checks every server and says why it’s unhappy with each one.
In the AWS console: EC2Load BalancingTarget Groupsyour target groupTargets
| Health reason | Means |
|---|---|
Target.Timeout | The health check got no answer. Almost always a security group: the app’s group must allow the load balancer’s group on the app’s port |
Target.ResponseCodeMismatch | The app answered, with the wrong status code: often the wrong health check path, returning 404 |
Target.FailedHealthChecks | The checks are failing: the app may be down, or refusing connections |
Elb.InitialHealthChecking | Still being checked for the first time. Wait |
Target.NotInUse | The target’s zone isn’t enabled on the load balancer |
Recent changes. Most incidents follow a change. Check the last deploys, then ask CloudTrail for every change in the last hour.
In the AWS console: CloudTrailEvent history
Filter with Read only set to false. Who changed what shows how.
Logs. In Logs Insights, the first query shows the shape of the problem: when it started, when it stopped, how bad it is.
filter level = "ERROR"
| stats count() as errors by bin(1m), error_type| Part | What it means |
|---|---|
filter level = "ERROR" | only the error lines |
| stats count() as errors | count them |
by bin(1m), error_type | per minute, and per kind of error |
Metrics. On the load balancer’s Monitoring tab: are 5xx rising? Is response time up? Is it all servers or one? Logs and metrics explains which statistic to read.
A server that won’t boot. If it never becomes healthy, you often can’t open a shell on it either, because whatever broke the app broke that too. AWS can fetch the server’s boot output for you, without any network access to it:
In the AWS console: EC2Instancesthe instanceActionsMonitor and troubleshootGet system log
It shows what the boot script printed and where it stopped. Get instance screenshot, in the same menu, shows the server’s screen.
Network ACLs: the other firewall
Security groups are the firewall you’ll use most, but each subnet also has a network ACL. The default one allows everything, so most people never touch it. If someone has changed it, it can cause timeouts that the security groups can’t explain.
| Security group | Network ACL | |
|---|---|---|
| Applies to | One resource, such as a server or a load balancer | A whole subnet |
| Remembers connections | Yes: if a request is allowed in, its reply is allowed out | No: the reply needs its own rule, both ways |
| Can say “deny” | No, only “allow” | Yes |
| Default | Nothing in; everything out | Everything in and out |
The checklist
Work down it in order. Most problems are found by step 4.
| # | Ask | Where |
|---|---|---|
| 1 | What exactly does the client see? Timeout, refused, which status code? | curl -v, the app’s error, the load balancer’s metrics |
| 2 | Everyone, or some? One server, one zone, one route? | Logs grouped by instance_id; target health per server |
| 3 | What changed recently? | Deploy history; CloudTrail with Read only = false |
| 4 | Is the load balancer happy with the servers? | Target group → Targets, and each health reason |
| 5 | What do the app’s logs say? | Logs Insights: errors per minute, then by error_type |
| 6 | Are the dependencies healthy? | Database metrics and connections; other services’ alarms |
| 7 | Is the network path open, end to end? | Security groups, route tables, network ACLs, in the order the request travels |
Part 3: See it
Pick the symptom, answer the follow-up question, and see where it leads. Try every path: they’re the most common ways a web service breaks.
What does the request do?
What to notice: the first choice — timeout, refused, 5xx or 4xx — already rules out most of the system. You never start by reading code. You start by reading the symptom exactly.
Part 4: Do it
Free — everything runs on your own computer.
You’ll make each symptom happen on purpose, on your own machine, so you know them on sight. This lab reproduces the symptoms locally with a tiny web server; the same messages appear, with the same meanings, when the server is in AWS. About 15 minutes.
You need python3 and curl, which come with macOS and Ubuntu, including Ubuntu on WSL. You’ll use two terminal windows.
Make an empty folder and start a web server
In window 1:
Terminalmkdir quickcart-triagePart What it means mkdirmake a directory quickcart-triageits name Terminalcd quickcart-triagePart What it means cdmove into a directory quickcart-triagethe empty folder you just made, so the server has nothing to share Terminalpython3 -m http.server 8080 --bind 127.0.0.1Part What it means python3 -m http.serverrun Python’s built-in web server, which serves the files in this folder 8080listen on port 8080 --bind 127.0.0.1only accept connections from this computer You should seeServing HTTP on 127.0.0.1 port 8080 (http://127.0.0.1:8080/) ...Leave it running. It prints a line for each request it receives.
A healthy request
In window 2:
Terminalcurl -i http://127.0.0.1:8080/Part What it means curlmake a web request -ialso show the response’s status line and headers http://127.0.0.1:8080/this computer, port 8080, the top page The first linesHTTP/1.0 200 OK Server: SimpleHTTP/0.6 Python/3.12.0Your Python version may differ. Below that is a short HTML page. 200: everything worked.
A 4xx: the request was wrong
Terminalcurl -i http://127.0.0.1:8080/checkoutPart What it means curl -imake a request and show the status line http://127.0.0.1:8080/checkouta path that doesn’t exist on this server The first lineHTTP/1.0 404 File not foundThe server is fine. It answered quickly and said the request asked for something that isn’t there. Restarting it would change nothing. In window 1, the server’s own log says the same:
code 404, message File not found.A 5xx: the server side failed
Terminalcurl -i -X POST http://127.0.0.1:8080/Part What it means curl -imake a request and show the status line -X POSTuse the POST method, like submitting a form. This little server can’t handle it http://127.0.0.1:8080/the top page The first lineHTTP/1.0 501 Unsupported method ('POST')A 5xx: the request reached the server, and the server couldn’t do it. For a real app, this is when you read its logs, which is exactly what window 1 now shows:
code 501, message Unsupported method ('POST').Refused: nothing is listening
In window 1, stop the server with Control+C. Then in window 2:
Terminalcurl -v http://127.0.0.1:8080/Part What it means curlmake a web request -vverbose: show each step of the connection, including why it failed http://127.0.0.1:8080/the same address as before Among the lines it prints* connect to 127.0.0.1 port 8080 from 127.0.0.1 port 59283 failed: Connection refused * Failed to connect to 127.0.0.1 port 8080 after 0 ms: Couldn't connect to server curl: (7) Failed to connect to 127.0.0.1 port 8080 after 0 ms: Couldn't connect to serverYour port numbers will differ. 0 ms: your computer answered at once that nothing is on port 8080. The network is fine. The process is gone.
Timed out: nothing answers at all
Terminalcurl --connect-timeout 5 http://10.255.255.1:8080/Part What it means curlmake a web request --connect-timeout 5give up if the connection hasn’t opened after 5 seconds, instead of waiting a minute or more http://10.255.255.1:8080/a private address that nothing on a normal home or office network uses, so nothing will answer You should seecurl: (28) Failed to connect to 10.255.255.1 port 8080 after 5006 ms: Timeout was reached5006 ms: curl waited the whole five seconds and heard nothing — not even a refusal. This is what a security group dropping traffic looks like from outside. On some networks you may see
No route to hostinstead; that’s also the network.Match each symptom to its layer
Without looking back, fill in the right-hand column on paper, then check against the idea:
What you saw Layer to check first 404 File not found, instantly501, instantly, and the server logged itCouldn't connect,Connection refused, 0 msTimeout was reachedafter 5,006 msYou should seeThe request itself; the app and its logs; the process and its port; the network path: security groups, routes and network ACLs.
Clean-up step: Clean up
The server is already stopped. Remove the empty folder from window 1:
Terminalcd ..Part What it means cd ..move up one folder, out of quickcart-triageTerminalrmdir quickcart-triagePart What it means rmdirremove a directory, which only works if it’s empty quickcart-triagethe folder you made You should seeNo output. Nothing is left running, and nothing was created in AWS.
Part 5: Check yourself
A request to the API hangs for 30 seconds, then fails with "Timeout was reached". Where do you look first?
curl says "Couldn't connect to server" instantly, and curl -v shows "Connection refused". What does that tell you?
The load balancer returns 503 to every request. What is the most likely cause?
New servers boot, fail their health checks and get replaced in a loop. You can't open a shell on them. What do you read?
The site was fine an hour ago. Now half the requests fail. Which question should you ask early?
Part 6: Cheat sheet
| Symptom or place | What it means, or where it is |
|---|---|
| Timed out while connecting | The network dropped it: security groups, routes, network ACLs |
| Connection refused, instantly | Reached the machine; nothing listening on that port |
| Connected, then timed out with 0 bytes | The app, or something it waits on, is stuck |
| 502 / 503 / 504 from the load balancer | Bad answer / no healthy targets / target too slow |
| 500 | The app failed. Read its logs |
| 4xx | The request was wrong. Don’t restart anything |
Target.Timeout | Health check never answered. Check the app’s security group |
| What changed? | Deploys, and CloudTrail with Read only = false |
| Won’t boot? | Actions → Monitor and troubleshoot → Get system log |
127.0.0.1 vs 0.0.0.0 | This machine only vs every address |
curl -v | Shows each step of a connection, and why it failed |