Advanced breakpoint types in Visual Studio, VS Code and Rider: conditions, hit counts, filters, tracepoints/logpoints, function, dependent, temporary and data breakpoints, with their performance cost.
A plain breakpoint stops every time; the others let me stop only when it matters. A conditional breakpoint stops when an expression is true, such as order.Number == "A-102", or when a value changes. A hit count stops on, say, the 500th iteration. A filter limits it to one thread or process. A tracepoint, called a logpoint in VS Code, prints a message with values and keeps running, like a log line I don't have to commit. Function breakpoints stop on a method by name, and data breakpoints stop when a field changes. The cost is speed: conditions are evaluated by the debugger on every hit, so in a hot loop they can make the app crawl.
"A plain breakpoint stops every time; the advanced types let me stop only when it matters. A conditional breakpoint stops when an expression is true, such as
order.Number == "A-102", or when a value changes. A hit count stops on the Nth hit, every Nth hit, or after N hits, and a filter limits it to one machine, process or thread.A tracepoint, called a logpoint in VS Code, logs a message with expressions and keeps running, like a log line I don't have to commit. Function breakpoints stop on a method by name, and dependent and temporary breakpoints control when a breakpoint is active. A data breakpoint stops when a specific object's field changes; in Visual Studio I set it from Locals, Autos or Watch, and only a few can be active because they use CPU debug registers. When one shows two threads touching the same data, I move on to the Threads window and Parallel Stacks.
The trade-off is speed. Each hit still suspends the process while the debugger evaluates the condition, then resumes if it's false, so the mistake I see is a conditional breakpoint in a hot loop turning a one-second run into minutes; there I add a temporary
ifwithDebugger.Break()instead. Conditions are also function evaluation inside my process, with the same side-effect risks as the Watch window, so I keep them to plain comparisons. And I choose the simplest tool: a plain breakpoint or a log line is fine when it's cheap.I frame the bug as a predicate, which input, which iteration or which thread, and let the debugger find it for me."
| Type | Use it when | Where |
|---|---|---|
| Condition: is true | The bug happens for one specific input, e.g. customerId == 4711 |
Visual Studio, VS Code, Rider |
| Condition: when changed | You want to stop the first time a value changes, e.g. retryCount |
Visual Studio |
| Hit count | It fails on a later iteration: = 500, is a multiple of 100, >= 1000 |
Visual Studio, VS Code, Rider |
| Filter | Only one thread or process matters: ThreadId = 12, ProcessId = 4321 |
Visual Studio |
| Tracepoint / logpoint | You want values over time without stopping or editing code | Visual Studio (Actions), VS Code (logpoints), Rider (log message / evaluate and log) |
| Function breakpoint | You know the method name but not the file, or it is in a library with symbols | Visual Studio, VS Code, Rider |
| Dependent breakpoint | Stop at B only after A has been hit, e.g. only in the second request | Visual Studio, Rider |
| Temporary breakpoint | Stop once and then remove itself | Visual Studio |
| Data breakpoint | Something changes a field and you do not know who | Visual Studio (from Locals, Autos or Watch: Break when value changes) |
For a conditional breakpoint the process still stops on every hit; the debugger evaluates the condition and resumes if it is false. That round trip is far slower than the code itself, so a condition on a line executed a million times can turn a one-second run into minutes. For hot code, write the check temporarily in code:
if (order.Number == "A-102") System.Diagnostics.Debugger.Break();
and remove it before committing.
A message handler corrupted one order in roughly every few thousand. A conditional breakpoint on the order number found the bad message, and a data breakpoint on Order.Status showed a retry path in another class setting it back to Pending. A tracepoint logging {message.Id} {order.Status} $TID confirmed two threads processed the same message, which explained the race.
The interviewer checks whether you can debug efficiently beyond the basics.
Green flags
Red flags
thread reaches the breakpoint address
|
v
process suspends (ALL managed threads)
|
v
debugger checks, in order:
filter (machine / process / thread) -- no match --> resume
hit count -- not yet --> count it, resume
condition (evaluated IN your process)-- false --> resume
|
v
tracepoint action? -- yes --> format message, write to Output
| "continue execution" ticked? --> resume
no
v
stop in the IDE
The important point: a false condition still costs a full suspend, an evaluation and a resume. The IDE doesn't show a pause, so it looks free, but it isn't.
| Type | Stops when | Cost per hit | Best for |
|---|---|---|---|
| Plain | Every time the line runs | Pause until you continue | Sequential code you run once or twice |
| Condition: is true | An expression is true | Suspend + evaluate + resume | One bad input among many |
| Condition: when changed | The expression's value differs from the last hit | Same | "When does this counter start climbing?" |
| Hit count | The Nth hit, every Nth, or from N onwards | Suspend + count + resume | A failure on a known iteration |
| Filter | Only on a chosen machine, process or thread | Suspend + check + resume | Multi-process or multi-threaded apps |
| Tracepoint / logpoint | Never stops (if "continue" is ticked); writes a message | Suspend + format + resume | A timeline of values without changing code |
| Function | A method by name is entered | Like plain | Code in another file or a library with symbols |
| Dependent | Only after another breakpoint has been hit | Like plain once enabled | "Only in the second request" |
| Temporary | Once, then removes itself | Like plain | A quick one-off stop |
| Data | A specific field's value is written | Near zero until it fires (hardware) | "Who changed this?" |
IDE support: Visual Studio has all of them. VS Code with C# Dev Kit has conditions, hit counts, logpoints and function breakpoints. Rider has conditions, hit counts, log messages, dependent breakpoints and its own field watchpoints.
The exact numbers vary by machine, but the shape is consistent:
loop of 1,000,000 iterations, simple body
no breakpoint ~ milliseconds
conditional breakpoint ~ minutes (one round trip per iteration)
tracepoint on every hit ~ minutes (round trip + string formatting)
in-code if (...) Debugger.Break() ~ milliseconds until it fires
For hot code, the in-code check is the right tool. Add it temporarily and remove it before committing.
you: "Break when value changes" on order._status (from Locals or Watch)
|
v
debugger finds the field's memory address in that object
|
v
programs a CPU debug register (x64 has 4) to trap writes
|
v
any code writes to that address --> CPU trap --> debugger stops
| on the line AFTER the write
v
you read the call stack to see who wrote it
Consequences:
Order.In Visual Studio, {expression} is evaluated in the current frame, and these keywords add context:
| Keyword | Value |
|---|---|
$FUNCTION |
Current method |
$CALLER |
Calling method |
$CALLSTACK |
Full call stack (expensive) |
$TID / $TNAME |
Thread id / name |
$PID / $PNAME |
Process id / name |
$TICK |
Tick count, for rough timing |
Output goes to the debugger's Output window (VS Code: Debug Console), not to your application's logs, so it is gone when the session ends.
A condition runs inside your process through function evaluation, with the same risks as the Watch window:
| Condition | Hidden effect |
|---|---|
orders.Any(o => o.IsLate) |
Enumerates the collection on every hit; an IQueryable runs a database query |
customer.Orders.Count > 5 |
A lazy-loaded navigation property can run SQL |
cache.GetOrAdd(key, ...) |
Changes state |
count++ > 10 |
Changes your variable |
order.Number == "A-102" |
Safe: a field or property read with no side effects |
Keep conditions to plain comparisons of locals and simple properties.
Do you know WHICH input is bad? --> condition: is true
Do you know WHEN (iteration number)? --> hit count
Is only one thread or process relevant?--> filter
Do you need values over time? --> tracepoint (keep it short)
Is the field corrupted by "someone"? --> data breakpoint
Is the line hit millions of times? --> temporary if (...) Debugger.Break()
Will you need this again in production?--> a structured log event in code
Stopping changes a concurrent system: other threads stop too, locks are held longer, leases and timeouts expire, and message brokers redeliver. When a bug depends on timing, prefer tracepoints with $TID, data breakpoints (which only fire on the bad write), or logging with correlation ids. A filter on a thread id helps when one worker thread is the interesting one.
The breakpoint types are features of the IDEs, not of .NET itself. Expect improvements in Visual Studio, Rider and C# Dev Kit releases rather than in .NET 11.
order.Number == "A-0750", and compare the time.if (order.Number == "A-0750") Debugger.Break(); and compare again.{order.Number} {order.Amount} on $TID with "Continue code execution" ticked, and read the Output window._status on one order in Locals, choose Break when value changes, continue, and read the call stack when it fires.A loop to practise on:
using System.Diagnostics;
var orders = Enumerable.Range(1, 1_000)
.Select(i => new Order($"A-{i:0000}", i * 1.5m))
.ToList();
var sw = Stopwatch.StartNew();
decimal total = 0;
foreach (var order in orders)
{
total += order.Amount; // set conditional breakpoints and tracepoints here
if (order.Amount > 1_000m)
{
order.MarkForReview(); // data breakpoint target: Order._status
}
}
Console.WriteLine($"Total {total} in {sw.ElapsedMilliseconds} ms");
public sealed class Order(string number, decimal amount)
{
private string _status = "New";
public string Number { get; } = number;
public decimal Amount { get; } = amount;
public string Status => _status;
public void MarkForReview() => _status = "Review";
}
Breakpoint settings to try (Visual Studio Conditions and Actions):
Condition (Is true): order.Number == "A-0750"
Condition (When changed): total > 100000
Hit count: is a multiple of 100
Filter: ThreadId = 1
Action (tracepoint): {order.Number} amount {order.Amount} in $FUNCTION on $TID
The in-code alternative for a hot loop (remove before committing):
if (order.Number == "A-0750") Debugger.Break();
Mistake: putting a conditional breakpoint in a hot loop.
Every hit still suspends the process, evaluates the condition and resumes, so a line hit a million times turns a one-second run into minutes, and you conclude the app is slow.
Instead: for hot code, add a temporary if (condition) Debugger.Break(); in code, or use a hit count, and remove it before committing.
Mistake: verbose tracepoints on hot lines.
A tracepoint also suspends and formats its message. Several expressions or $CALLSTACK on a busy line is almost as slow as a conditional breakpoint.
Instead: keep messages short, add a condition or hit count, and use $CALLSTACK only on rare lines.
Mistake: conditions with side effects.
Conditions run in your process. LINQ enumerates collections, IQueryable runs SQL, lazy-loaded properties hit the database, and count++ changes state.
Instead: compare locals and simple properties only, such as order.Number == "A-102".
Mistake: expecting data breakpoints to work like watchpoints on every instance. They watch one field in one object, only a few can be active because they use CPU debug registers, and they disappear when the object is garbage collected. Instead: pause on the object you care about, set the data breakpoint from Locals or Watch, and use a conditional breakpoint on the setter if you need every instance.
Mistake: reading the wrong line when a data breakpoint fires. The debugger stops just after the write, so the highlighted line may not be the one that changed the value. Instead: look at the previous statement and the call stack.
Mistake: pressing F5 hundreds of times on a plain breakpoint. It is slow and you easily overshoot the bad iteration. Instead: use a hit count or a condition that describes the bad case.
Mistake: relying only on Console.WriteLine and redeploying for every question.
Each question costs a code change and a rebuild, and the lines often get committed by accident.
Instead: use a tracepoint for a one-off question. Use structured logging in code for anything you will need in production.
Mistake: debugging timing bugs with stopping breakpoints.
Stopping changes thread interleaving, so the race disappears or a timeout appears that isn't the real bug.
Instead: use tracepoints with $TID, data breakpoints, or logs with correlation ids.
Mistake: committing Debugger.Break() checks.
With a debugger attached they stop every developer and test run; without one, depending on the platform, they can prompt to attach a debugger or end the process.
Instead: keep them local and temporary, guard with #if DEBUG if you must, and remove them before the pull request.
Mistake: forgetting old breakpoints. Breakpoints are saved with the solution's user settings, so stale conditions slow later sessions in ways that are hard to explain. Instead: review the Breakpoints window (Ctrl+Alt+B in Visual Studio) and delete or disable ones you no longer need.
Advanced breakpoints are about reducing the number of times a human has to look. A senior engineer frames the bug as a predicate ("which input, which iteration, which thread") and lets the debugger find it.
They also weigh the context. In a highly concurrent or real-time system, stopping one thread stops all of them, timeouts and leases expire, and message brokers redeliver, so stopping changes the system they are observing. There they prefer tracepoints, data breakpoints that fire only on the bad write, or structured logging with correlation ids. For a simple sequential bug, a plain breakpoint is faster than configuring anything advanced. The trade-off is setup time and debugger overhead against the number of manual stops saved.
And they recognise when the same question will come up again in production, and turn the tracepoint into a permanent, structured log event instead.
At a breakpoint, Locals shows the variables in scope, Autos shows the ones used around the current line, and Watch shows expressions I pick and keep across steps. The Immediate window, which is the Debug Console in VS Code, lets me evaluate expressions, call methods and even assign new values to test an idea. The catch is that evaluating an expression runs real code in the paused process. A property getter with side effects, a LINQ query or a lazy-loaded EF Core navigation can change state or hit the database. When that matters I add the nse (no side effects) format specifier, as in customer.Orders, nse, or read backing fields directly. I also remember evaluation can time out because other threads are frozen.
When it hangs, I press Break All so every thread stops where it is. In the Threads window I look for threads sitting in Monitor.Enter, Lock.Enter or Task.Wait, and switch to each one to read its call stack. Parallel Stacks shows all thread stacks merged into one graph, so two threads each holding a lock and waiting for the other's lock stand out immediately. That's the classic opposite-order deadlock. In production I can't attach, so I take a dump with dotnet-dump and run clrstack -all and syncblk. The fix is a consistent lock order, smaller locks, or removing sync-over-async.
Last updated 1 Nov 2026.