Every health check is green and the customer still gets nothing
Can I prove the customer's requests are reaching the application at all?
The ticket
CUSTOMER TICKET: the whole application is unreachable, but your status page says it is fine
Account: Halden Freight (enterprise) Impact: every user, nothing loads at all Started: this morning, after a change went out last night
Nobody can get in. Every page just fails to load, not an error message, it simply returns nothing. We have checked from three different offices and two phones on mobile data, so it is not us.
What I do not understand is that your own status page has been green the whole time, and the engineer we spoke to last night said the deploy came up clean and everything was reporting healthy. Something is clearly wrong somewhere, because we have had no service since about 6am.
Your job
- Work out which layer is failing. This ticket does not tell you, and neither does the folder it is filed under.
- Prove where the customer's request actually stops.
- Restore service with the smallest correct change, then verify the workflow the customer described rather than the one that was already passing.
Working notes
The customer reaches this service at http://127.0.0.1:8100, and the workflow
they mean is GET /customers.
Nothing here tells you which system is involved. Finding that out is part of
the exercise, and it is a real skill: a ticket rarely arrives with the
technology attached. When you have found the configuration that is wrong and
changed it, run tse apply to recreate the services, then tse check.
- Track
- Mixed incidents
- Time
- about 30 minutes
- Difficulty
- Involved
- Tier
- Core
Do these first: Same symptom as the last ticket, different cause, Same symptom as the last ticket, and the labels are correct, Batch job succeeds at the start and fails partway through, A report that was instant is now painful
Start it
In a Codespace or a local clone:
tse start mixed/01-every-check-is-green-and-nothing-worksThat provisions the broken system and prints the ticket above. Investigate with ordinary tools, then run tse check.
Look at the evidence
Real output, captured by running these commands against the broken system and checked against it on every build. It shows you what the evidence looks like. It cannot fix anything, and it will not tell you what is wrong.
Type a command you would reach for, or help.
Enter runs it. Shift and Enter start a new line. The up and down arrows walk back through what you have typed.
Investigation scratchpad
Saved in this browser as you type. Nothing is uploaded. 0 of 7 filled in.
In their words, not yours. Include scope and urgency.
Before running anything: target layer, expected output, two likely causes.
The command or query, and why it is safe to run here.
Three separate lists. This is the step people skip.
One proof sentence, one safe next step, one alternate hypothesis.
Plain language. Impact first. No blame, no speculation.
One gap, one command to repeat tomorrow, one confidence score.
Hints
Each hint gives away a little more. Try to spend a few minutes on your own evidence first, because the recall is what makes it stick.
Hint 1 of 3
Start by taking the customer seriously on both halves of what they said, because both halves are true.
They cannot reach the service. The engineer who looked last night saw everything reporting healthy. People usually resolve that contradiction by deciding one side must be mistaken, and here neither is.
So the useful question is not "who is wrong". It is:
What exactly does a health check prove, and who was asking?
A health check is a question the system asks itself, from inside itself. The customer is asking a different question from much further away. Those two questions can have different answers at the same instant, and when they do, the gap between them is the fault.
Before you look at any configuration, work out how far the customer's request actually gets. Not whether the application is alive, which you already know it is. Where the request stops.
One more thing worth noticing: this ticket did not tell you what technology is involved, and the folder it is filed under will not either. Finding what is running is your first real step, and it is one command.
Hint 2 of 3
docker compose ls will tell you what is running and which files define it.
That is where the configuration lives.
Now separate two claims that people usually merge into one:
| Claim | What proves it | Who is asking |
|---|---|---|
| The application process is alive | The container's state | The platform |
| The application is serving requests | Its own health check | The container, from inside |
| A request from outside arrives | A request from outside | The customer |
The first two are green. You can confirm that in one command. The third is the customer's ticket, and nobody has tested it yet.
So test it, and pay attention to how it fails. There is a real difference between these, and each one points somewhere different:
- Nothing is listening at that address, so the connection is refused outright.
- Something accepted the connection and then gave you nothing back.
- Something answered, and the answer was an error.
Get the exact failure, not a summary of it. Then compare two numbers: the address the customer is sent to, and the address the application says it is listening on when it starts up. The application announces that in its very first log line.
Hint 3 of 3
The connection is accepted and then closed with nothing sent. curl reports
that as an empty reply on some hosts and as a connection reset by peer on
others, depending on how your machine forwards published ports. Either wording
means the same thing here, and both are a world away from a refusal.
That already rules a lot out: something is listening at that address, so this is not a service that failed to start, and it never got far enough to be an application error.
Look at what the published address actually points at:
docker ps --filter 'label=com.docker.compose.project=proveit-docker' \
--format '{{.Names}} {{.Status}} {{.Ports}}'
Then look at what the application itself said when it started:
docker compose -f labs/docker/_stack/compose.yaml \
-f labs/docker/_stack/compose.override.yaml \
logs app | head -1
One of those two numbers is where traffic is being sent. The other is where the application is actually listening. They do not match, which is why the connection is accepted by the forwarding layer and then dies with nothing behind it.
The health check never noticed because it runs inside the container and connects straight to the application, never crossing the mapping that is wrong.
The change is in labs/docker/_stack/compose.override.yaml, and it is one
digit.
Solution
Write your customer update before you read this. Comparing your wording against the model answer is worth more than reading it cold.
Reveal the solution
Solution: every check is green and nothing works
What the evidence proved
| Command | What it proved | What it did not prove |
|---|---|---|
docker compose ls |
What is running, and which files configure it | Nothing about the fault. This was orientation |
docker ps |
The container is up and its health check reports healthy | Nothing about whether anyone outside can reach it |
curl http://127.0.0.1:8100/customers |
The connection is accepted and closed with no reply | Nothing about which side closed it |
| Reported as an empty reply or as a connection reset depending on the host, and neither is a refusal | ||
docker compose logs app |
The application started cleanly and is serving requests on 8080 |
Nothing about which address those requests arrive on |
docker ps --format '{{.Ports}}' |
Traffic to 8100 is forwarded to 8081 inside the container |
The last two lines are the whole diagnosis, and neither means anything alone. The application is listening on one port. The published mapping delivers to a different one. Every request is handed to a port with nothing behind it.
Worth stating plainly, because it is the habit this exercise is built to break: the health check passing was never evidence that the customer could reach anything. It runs inside the container and connects directly to the application, so it never crosses the mapping that is broken. It was telling the truth about a narrower question than the one being asked.
Root cause
Last night's change edited the published address and set the container-side
target to 8081. The application listens on 8080 and always has.
The forwarding layer accepts connections on 127.0.0.1:8100 because the
mapping exists, then tries to deliver them to 8081 inside the container,
where nothing is listening. The connection is closed with nothing sent, which
is why the customer described it as pages returning nothing rather than as an
error.
Everything inside the container stayed healthy throughout, because nothing inside the container was wrong.
Scoped fix
In labs/docker/_stack/compose.override.yaml, point the mapping at the port
the application actually listens on:
services:
app:
ports: !override
- "127.0.0.1:8100:8080"
Then:
tse apply
tse check
!override is there because Compose adds published ports from a second file to
the ones in the first rather than replacing them. Without it you get two
mappings competing for the same published address, which fails in a new and
more confusing way.
Customer update
The outage was caused by a configuration change in last night's release. The address our platform publishes for your service was pointed at the wrong internal port, so connections were accepted and then dropped before reaching the application. That is why pages returned nothing rather than an error, and why our own health checks stayed green: those run inside the service and were not affected by the change. We have corrected the address and confirmed your user list is loading. No data was affected at any point. We are also reviewing why our external checks did not catch this, since your experience should not have been the first signal.
Engineering escalation, if you needed one
Impact: total loss of service for all users from roughly 06:00, every request accepted and closed with no response. Evidence: container healthy throughout; application logs show a clean start and
port=8080; published mapping is127.0.0.1:8100->8081/tcp; requests to8100return an empty reply. Confirmed: application process health, application startup, database availability. Ruled out: application crash, dependency failure, credential or data problem. Suspected cause: the release changed the container-side target of the published port from 8080 to 8081. Request: our health checks cannot see this class of fault, because they run inside the container. Can we add a check that connects from outside the published address, so the next one is caught by us rather than by a customer.
That last request is the part that stops this recurring. The fix takes one digit. The monitoring gap is what let it run from 06:00 until a customer complained.
Check your understanding
Three questions on what the evidence here proved, and what it pointedly did not. Wrong answers explain themselves, and so do right ones.
tse quiz
Check your understanding
Three questions on what the evidence proved and what it did not. Every answer explains itself, including the right one.
3 questions, none answered yet.
Why this one exists
A health check answers a narrower question than the customer asked. It runs inside the thing it is checking, so it proves the application is serving itself, not that anyone outside can reach it. Reachability is its own layer with its own evidence.
In an interview
Almost every candidate has a first move, and this rewards noticing when the first move came back clean. Saying "the process is up, its own health check passes, and the request never reaches it, so the problem is between the customer and the process" is a genuinely different sentence from "it looks fine to me", and it is the one that moves a ticket forward.
Commands introduced
docker compose lsdocker port
Evidence layers
- published address
- container state
- application logs
- what the application says it bound to