Breakpoints not hit¶
Symptom: the session starts, the program runs to completion, and nothing ever stops.
First: what did the adapter say?¶
Open the Breakpoints panel — Ctrl+B. Every breakpoint carries a circle:
| Circle | State | Meaning |
|---|---|---|
| Filled | verified | the adapter accepted it — the problem is elsewhere |
| Yellow ring | rejected | the adapter refused it |
| Dimmed ring | pending | no answer yet |
That single glyph splits the problem in half.
The breakpoint was rejected (yellow ring)¶
The adapter would not bind it. Causes, in order of likelihood:
The line is not executable. Blank lines, comments, declarations, a closing brace. Move to a line that does something.
The code was optimised away. Inlined functions and eliminated branches have no code to stop at. Rebuild without optimisation:
- Rust: debug build, not
--release - C / C++:
-g -O0 - Go: keep
-gcflags=all=-N -lif you overrode the build
There is no debug information. A binary stripped of symbols, or a dependency compiled without them, cannot host a breakpoint.
The breakpoint is pending (dimmed ring)¶
No answer has arrived. Whether that is a problem depends on the language:
- PHP: normal before the first request. The set is visible in the window, but until Xdebug connects there is nobody to ask. Send a request.
- Everything else: if it stays pending after the session has started, the adapter is not answering — see Adapter fails to start.
Note also that an adapter is allowed to move a breakpoint to a nearby line. When it does, the line you asked for gets no confirmation and stays pending, even though a breakpoint does exist a line or two away.
The breakpoint is verified but never fires¶
The adapter accepted it, so the question is now about your program, not the debugger.
The code does not run. The most common answer, and the least welcome one. Put
a breakpoint on the first line of main, or on the function that calls this one,
and work forwards.
A second process is running the code. This one is easy to miss:
- Django, Flask, uvicorn with an autoreloader fork a worker the debugger is not
attached to — add
--noreloador the equivalent; - a task queue or a worker pool executes the code somewhere else entirely.
The request finished before debugging started (PHP). Xdebug connects at the start of a request; one already in flight cannot be debugged retroactively. Send another.
The PHP test command timed out. For a Docker-based :DebugTest, Bugsaur
waits 30 seconds for the test container to connect to Xdebug. If the GUI reports
that Docker may be unavailable or hung, check that Docker Desktop/OrbStack is
running, then inspect the containers with docker compose ps and try the test
command manually. If testCommand exits unsuccessfully, the error also shows
the exact command, exit status and captured Docker/Compose output.
The condition is never true. A conditional breakpoint with an expression that never evaluates true behaves exactly like a breakpoint that does not work. Temporarily remove the condition to check.
A test run ignored your saved breakpoints. :DebugTest and --test-at use
only the breakpoints of that request — by design. Set the breakpoint in the same
session.
An explicit --break replaced the set. --break does not add to the saved
set, it replaces it entirely.
It stops, but there are no variables¶
A different problem with the same root: the frame has no debug information.
| Cause | Fix |
|---|---|
| Release / optimised build | rebuild in debug |
| The frame is library or runtime code | select one of your own frames in the Call Stack |
Rust --release |
debug build |
| Go with a custom build | keep -gcflags=all=-N -l |
Checklist¶
□ Is the circle filled, yellow or dimmed?
□ Is the line executable?
□ Is the build unoptimised?
□ Does the code actually run — does a breakpoint in main fire?
□ Is there a second process (autoreloader, worker)?
□ Is a condition attached that is never true?
□ Is this a test run, which ignores saved breakpoints?
□ Did --break replace the saved set?