A senior .NET interview is not a harder version of a mid-level interview. It is a different interview. Nobody is going to ask you what dependency injection is. They are going to describe a service that is timing out while the CPU sits at 15%, hand you no logs, and watch how you think.
That shift catches people out. You can have eight years of shipping solid features and still lose a senior loop, because the questions stop asking what a thing is and start asking what you would do when the thing is on fire and you are not sure why. The answers that win are the ones that name a mechanism, admit what they do not know yet, and describe the next measurement rather than the next guess.
This is 35 senior .NET interview questions in the format that actually gets used: a real scenario, how I would answer it, the answer that quietly gets you rejected, and the follow-up the interviewer chains next. Everything here is accurate for .NET 10 and C# 14.
These are deliberately not the topic questions. There is no REST design, no LINQ operator trivia, no middleware ordering. Those live on the topic pages linked at the bottom. This page is the runtime, the production floor, and the judgment calls - the parts of the stack most developers never have to touch until the day they suddenly do. Let’s get into it.
What Actually Changes at Senior Level
Three things change, and knowing them is worth more than memorising any single answer.
The questions stop having one right answer. A mid-level question has a correct response. A senior question has a correct process. “Your p99 is 6 seconds and your p50 is 40ms” has no single cause. The interviewer is watching whether you narrow the space or start guessing fixes.
You are expected to say “I would measure that.” At mid level, admitting uncertainty reads as a gap. At senior level, asserting a cause without evidence reads as a much bigger one. The strongest answers name the tool and the number they would look at first.
Judgment counts more than recall. Knowing that CQRS exists is table stakes. Being able to say when you would refuse to introduce it, and why, is the thing being tested. Interviewers are trying to work out whether you will make their codebase better or just more fashionable.
There is one currency check running underneath all of it. If you talk about BinaryFormatter, the .NET Upgrade Assistant, or tuning Server GC as though DATAS does not exist, the interviewer quietly concludes you stopped reading release notes a few versions ago. Those specific traps are covered below.
This page is part of my .NET interview prep series. For the topic-by-topic sets, start at the .NET interview questions hub, then go deep on ASP.NET Core internals, EF Core, LINQ, and .NET Web API.
Drill these at senior level in the free mock interview
Reading answers and producing them under pressure are different skills. Take a free, auto-scored mock interview at Senior level - instant score, per-topic breakdown, and a model answer for every question. No signup to start.
Runtime, Memory, and GC
This is the section that separates people who have run .NET in production from people who have written .NET. Almost no interview-prep content covers it, which is exactly why it gets asked.
Q1. Your API’s Memory Climbs All Day and Drops on Restart. Walk Me Through Finding the Leak.
Senior
First I would establish whether it is actually a leak or just a heap that has not been pressured yet. .NET does not return memory to the OS eagerly, so a working set that grows on a box with plenty of free RAM can be perfectly healthy. The question I want answered is whether live objects are growing, not whether committed memory is growing.
So: dotnet-counters monitor -n MyApi and watch dotnet.gc.last_collection.heap.size broken down by generation across a few hours. If gen 2 keeps climbing after collections, something is holding references. If only the working set climbs while gen 2 stays flat, it is fragmentation or native memory, not a managed leak.
Once I know it is managed, I take two heap snapshots an hour apart with dotnet-gcdump collect -p <pid> and diff them. The diff tells me which type grew. Then the question becomes what is rooting it, and the usual suspects are a static collection, a long-lived event handler subscription that never unsubscribes, an IMemoryCache with no size limit or expiry, and a captured closure inside something registered as a singleton.
Red flag answer: “I’d add more memory to the container and set up an auto-restart.” - That is a mitigation, and sometimes the correct short-term one, but offering it as the diagnosis says you have never actually found a leak.
Follow-up: “The diff shows a million byte[] instances. How do you find who is holding them?”
Q2. Is Your API Running Server GC or Workstation GC, and Does It Matter?
Senior
ASP.NET Core apps default to Server GC. The runtime’s own default for a standalone app is Workstation, but for hosted apps the host chooses, and the web host picks Server. That matters because the two behave very differently: Server GC gives you a heap and a dedicated collection thread per logical CPU and collects them in parallel, which is faster on the same heap size but uses considerably more memory and CPU.
The part worth knowing, and the reason this gets asked, is that Workstation GC is always used on a machine with only one logical CPU, no matter what you configure. Set ServerGarbageCollection to true all you like; on a single-core container the runtime ignores you. So a service that behaved one way on a 4-core node can behave differently after someone tightens the CPU limit, and nothing in your config changed.
The other half is density. If you are running many instances of a small service on one box, Server GC’s per-core threads start fighting each other, and Workstation GC with concurrent collection disabled can genuinely be faster.
Red flag answer: “Server GC is always better for servers.” - It is the right default, but on constrained or densely packed containers it is often the wrong one, and it is not even honoured on a single core.
Follow-up: “How would you verify which flavour a running process actually chose?”
Q3. What Is DATAS, and Why Should You Care in .NET 10?
Senior
DATAS is Dynamic Adaptation To Application Sizes. It makes the GC size the heap roughly in proportion to the application’s live data instead of holding a large heap because the machine happens to be large. It was introduced in .NET 8 behind a switch and has been on by default since .NET 9, for Server GC only. Workstation GC is unaffected.
The reason it comes up in interviews now is that .NET 10 is the LTS release, so a large number of teams are meeting DATAS for the first time while upgrading from .NET 8. The behaviour change is real: memory usage drops substantially, and the trade is a small throughput cost. DATAS targets a 2% Throughput Cost Percentage by default. It also starts the process with a single heap and grows from there, so startup and the first minutes of a traffic ramp can look slower than they used to.
If a service regressed on throughput right after a .NET 9 or 10 upgrade, DATAS is the first thing I would test, by setting System.GC.DynamicAdaptationMode to 0 and re-running the load test. Not to leave it off - to confirm the cause.
Red flag answer: “DATAS is the new GC in .NET 10.” - It is not a new collector and it did not arrive in .NET 10. It is an adaptation mode on Server GC that has been default since .NET 9.
Follow-up: “Your memory went down but p99 went up after upgrading. How do you decide whether that trade is acceptable?”
Q4. What Lands on the Large Object Heap, and Why Does That Hurt You?
Senior
Anything 85,000 bytes or larger goes on the Large Object Heap. In practice that means big arrays and big strings: a 20,000-element int[], a buffer you allocated per request, a serialised payload you built in memory.
Two things make the LOH painful. First, it is collected only with gen 2, so LOH pressure drags you into full collections. Second, it is not compacted by default, so a workload that allocates differently sized large buffers fragments it, and you end up with a heap that is mostly holes - committed memory you are paying for and cannot use.
One caveat worth knowing, because it is the kind of detail that separates people who have read about the LOH from people who have debugged it: in containers on .NET Core 3.0 and later, the LOH is compacted automatically. So if you are running in Kubernetes, fragmentation is much less likely to be your problem, and the gen 2 pressure is the part that still bites.
The fix is almost never to tune the GC. It is to stop making the allocations: ArrayPool<T>.Shared for reusable buffers, streaming instead of materialising whole payloads, and RecyclableMemoryStream in place of MemoryStream for large bodies. Raising System.GC.LOHThreshold exists and can be right in narrow cases, but reaching for it first is a smell.
Red flag answer: “You can force LOH compaction with GCSettings.LargeObjectHeapCompactionMode.” - You can, and it is occasionally the correct emergency lever, but it triggers a blocking full compacting collection. Offering it as the routine answer signals you would rather configure than fix.
Follow-up: “How would you find which code path is allocating on the LOH?”
Q5. Gen 0 Collections Are Supposed to Be Cheap. So Why Is Your p99 Spiking?
Senior
Because a cheap collection that happens constantly is not cheap, and because a gen 0 collection is not the only thing that stops your threads.
The usual mechanism is promotion. Gen 0 is cheap when the objects in it are dead by the time it runs. If your request handler allocates objects that stay alive until the response is written - which is normal - they survive gen 0, get promoted to gen 1, then gen 2, and now you are paying for full collections you did not expect. Allocation rate turns into gen 2 pressure.
The number I would look at is dotnet.gc.pause.time against request latency, plus the collection counts per generation. If gen 2 count is climbing under load, the p99 spikes almost certainly line up with those pauses. And if the process is near its memory limit, the GC gets more aggressive about full compacting collections once physical memory load crosses roughly 90%, which in a container means 90% of the container limit, not the host’s.
Red flag answer: “Gen 0 is cheap so GC isn’t the problem.” - This is the trap in the question. It is checking whether you know promotion exists.
Follow-up: “How do you reduce allocations in a hot request path without rewriting everything?”
Q6. Your Process Holds 8 GB but the Heap Snapshot Only Accounts for 2 GB. Where Is the Rest?
Senior
Managed heap size and process working set are different numbers, and the gap has a short list of explanations.
Fragmentation is the first. Since .NET 7 the GC organises the heap into regions rather than segments on 64-bit Windows and Linux, but committed-but-unused space still shows up in the process footprint, and dotnet.gc.last_collection.heap.fragmentation.size will tell you how much.
Native memory is the second, and in my experience the more common one in real services. Native database drivers, image or PDF libraries, gRPC and HTTP/2 buffers, and any SafeHandle-wrapped resource live outside the managed heap. A leak here is invisible to a gcdump entirely.
Third is simply that the GC has not been asked to give it back. Memory that has been released to the heap but not decommitted to the OS still counts toward working set.
The way I would split those apart is to compare dotnet.process.memory.working_set against the managed heap size over time. If managed is flat and working set climbs, stop looking at gcdumps and start looking at native handles and unmanaged libraries.
Red flag answer: “That’s just the GC being lazy, it’ll come back.” - Sometimes true, but saying it without checking the fragmentation counter or considering native memory is the answer of someone who has never had a container OOM-killed.
Follow-up: “How would you confirm a native leak rather than a managed one?”
Concurrency and the Thread Pool
Async/await mechanics belong on the C# page. This section is about what happens when the pool itself is the bottleneck, which is a production problem, not a language one.
Q7. Your API Times Out Under Load but CPU Sits at 15%. What’s Happening?
Senior
That combination - slow responses, plenty of idle CPU - is the signature of thread pool starvation. Work items are queued and there are no free threads to run them, so requests wait in line while the machine looks bored.
The mechanism is almost always sync-over-async: something on the request path calls .Result, .Wait(), or .GetAwaiter().GetResult() on a task. That blocks a pool thread instead of releasing it. Under load, every concurrent request consumes a thread and holds it, the pool runs dry, and the runtime responds by injecting more threads - slowly, because after the initial burst it adds roughly one or two per second.
To confirm it rather than assume it, I would run dotnet-counters monitor -n MyApi and watch dotnet.thread_pool.thread.count. Starvation looks like that number climbing to two or three times the core count and then creeping upward while CPU stays low. Often dotnet.thread_pool.queue.length is large and dotnet.thread_pool.work_item.count is low at the same time: lots of pending work, little completing.
Worth knowing: .NET 6 changed the pool’s heuristics to ramp up faster for certain blocking task APIs. That shortened these episodes considerably, so on modern .NET starvation often shows as a latency spike during ramp-up rather than a permanent stall. It did not remove the underlying problem.
Red flag answer: “I’d raise ThreadPool.SetMinThreads.” - That is the classic band-aid. It masks the symptom during ramp-up and does nothing about the blocked threads. If you offer it, offer it as a stopgap while you fix the blocking call.
Follow-up: “You’ve confirmed starvation. How do you find which line is blocking?”
Q8. You’ve Confirmed Starvation. How Do You Find the Blocking Call?
Senior
Two routes, and which one you pick depends on whether the problem is constant or intermittent.
If it happens on every request, dotnet-stack report -n MyApi dumps every thread’s stack straight to the console. I would look for stacks that end in ThreadPoolWorkQueue.Dispatch() and PortableThreadPool+WorkerThread.WorkerThreadStart() - those frames identify a pool thread - and then read the top of the stack to see what it is parked on. A blocked sync-over-async call shows up as Task.SpinThenBlockingWait and ManualResetEventSlim.Wait sitting above your own controller or handler method.
If it only happens every few minutes, stacks are a lottery, so I would collect a trace instead:
dotnet trace collect -n MyApi --clrevents waithandle --clreventlevel verbose --duration 00:00:30That captures the WaitHandleWait event, which .NET 9 added specifically for this. It fires whenever a thread blocks - on Task.Result, Task.Wait, lock, Monitor.Enter, SemaphoreSlim.Wait and friends. Open the resulting .nettrace in PerfView or the community .NET Events Viewer, group by stack, and the offending call path is usually the top entry.
Red flag answer: “I’d attach a debugger and step through.” - You usually cannot attach to production, and stepping does not reproduce a load-dependent problem anyway.
Follow-up: “The blocking call is inside a third-party library you can’t change. Now what?”
Q9. Pick Between lock, SemaphoreSlim, and Interlocked for a Shared Counter, and Defend It.
Mid
For a plain counter, Interlocked.Increment. It is a single atomic CPU instruction with no kernel transition and no risk of a thread being blocked at all. Nothing else comes close for that specific shape.
lock is the right default when you need to keep several operations consistent together - read a value, decide, write it back. It is cheap when uncontended and it is the clearest thing to read. Its hard limitation is that you cannot await inside it.
SemaphoreSlim is the one to reach for when the critical section contains async work, because WaitAsync yields the thread rather than blocking it, or when you want to permit N concurrent holders rather than one. The cost is that it is heavier than lock and you have to be disciplined about releasing it in a finally.
The judgment being tested is whether you reach for the cheapest tool that fits. Guarding an int with a SemaphoreSlim is not wrong, it is just several orders of magnitude more expensive than it needs to be.
Red flag answer: “I’d use lock around the await.” - You cannot; the compiler rejects it. If someone says this, it usually means they have never written contended async code.
Follow-up: “Your counter is now a dictionary of counters. Does your answer change?”
Q10. ConcurrentDictionary.GetOrAdd Ran Your Factory Twice. Why, and Does It Matter?
Senior
Because GetOrAdd is atomic about insertion, not about the factory. If two threads miss on the same key simultaneously, both will run the value factory. Exactly one of the results wins and gets stored; the other is discarded.
Whether that matters depends entirely on what the factory does. If it computes a value, you wasted some CPU and nobody notices. If it opens a connection, starts a background task, or registers something with an external system, you have just created an object that nothing will ever dispose - a genuine resource leak that only appears under concurrency, which is exactly the kind of bug that survives every test suite you have.
The fix is to make the stored value cheap and lazy: store Lazy<T> in the dictionary and let Lazy<T> provide the run-once guarantee. GetOrAdd may still create two Lazy<T> wrappers, but only the winner’s Value is ever accessed, so the expensive work happens once.
Red flag answer: “ConcurrentDictionary is thread-safe, so that can’t happen.” - Thread-safe means the data structure will not corrupt. It does not mean your callback runs once.
Follow-up: “Same question for IMemoryCache.GetOrCreate. Does it have the same problem?”
Q11. You Need to Call a Downstream API 500 Times. How Do You Do It Without Taking It Down?
Senior
The naive answer is Task.WhenAll over 500 tasks, which fires all 500 more or less at once and is a fine way to get yourself rate-limited or to knock over a service that was sized for normal traffic.
What I would actually reach for is Parallel.ForEachAsync with an explicit MaxDegreeOfParallelism, because it is built for exactly this and handles the async case properly. Setting it explicitly is the whole point: left alone, it defaults to Environment.ProcessorCount, and core count is exactly the wrong basis for I/O-bound work. The right value comes from what the downstream can take.
If I need more control than that, a SemaphoreSlim sized to the concurrency limit, acquired with WaitAsync inside each task, does the same job and composes with retry policies more naturally.
Two things I would raise unprompted. Pass a CancellationToken through, because a batch of 500 that cannot be cancelled will outlive the request that started it. And be careful about combining bounded concurrency with retries - if the downstream starts failing and every one of your in-flight calls retries, you have built a load amplifier pointed at a service that is already struggling.
Red flag answer: “Task.WhenAll handles it.” - It runs them; it does not bound them. The question is specifically about the bound.
Follow-up: “Where would you put the retry policy relative to the concurrency limit?”
Diagnosing Production
Nothing in this section is about writing code. It is about what you do at 3am with a service you cannot attach a debugger to, and it is where senior interviews spend more time than candidates expect.
Q12. Production Is Slow. You Can’t Attach a Debugger and You Can’t Deploy. What Do You Reach For?
Senior
The .NET diagnostics CLI tools, in roughly this order, because they escalate from cheapest to most invasive.
dotnet-counters first, always. It is nearly free, it runs against a live process, and it answers the triage question: is this CPU, memory, GC pauses, thread pool, or lock contention? Those five buckets cover most incidents, and knowing which one you are in changes everything you do next.
dotnet-stack report if the counters point at threads. Immediate console output, no file to move around, tells you what every thread is currently doing.
dotnet-trace when the problem is intermittent and you need a window of history rather than a snapshot. This is also where you go for the WaitHandleWait events when chasing blocking.
dotnet-gcdump when it is memory, because it gives you a heap graph you can diff against a later one.
dotnet-dump last. A full process dump is the most complete artefact and the most disruptive to collect, and you will be analysing it offline.
The framing I would give is that each step should be justified by what the previous one showed. Going straight to a full dump because “it has everything” is how you end up with 8 GB of file and no hypothesis.
Red flag answer: “I’d check the logs.” - Fine as a first instinct, but if the logs had the answer you would not be in this conversation. The question is what you do when they do not.
Q13. You Get Four Numbers on a Dashboard for a .NET API. Which Four?
Senior
Request rate, error rate, latency at p99, and saturation. That is the RED/USE framing, and the reason I would defend it is that those four detect nearly every incident shape between them, and each one tells you something the others cannot.
What makes this a senior question is the two follow-through points. First, latency has to be a percentile, never an average. A mean happily hides the fact that 1% of your users are timing out, and that 1% is the group that emails support. Second, saturation is the .NET-specific one: for a .NET API I want thread pool queue length and GC pause time, because those are the two resources that go into distress silently while CPU and memory still look fine.
If I were allowed a fifth, it would be dependency latency, split by dependency. Most “our API is slow” incidents are actually “something we call is slow”, and having that split saves the first twenty minutes of every investigation.
Red flag answer: “CPU, memory, disk, network.” - Those are host metrics. They tell you about the machine, not about whether the service is doing its job, and thread pool starvation is invisible in all four.
Follow-up: “You’ve got p99 latency. How do you decide what the alert threshold should be?”
Q14. A Bug Appears in Production Once a Week and Never Locally. How Do You Catch It?
Senior
I would stop trying to reproduce it and start trying to capture it, because a once-a-week bug will consume a month if you chase it interactively.
The approach is to make the failure leave evidence behind. That means correlation IDs flowing through every log line and every downstream call so a single failed request can be reassembled after the fact, and sampling that is biased toward errors - trace 100% of failures even if you sample 1% of successes, because tail-based sampling is the difference between having the trace you need and having a million traces of requests that worked.
Then I would add a targeted capture. If the failure has a detectable signature - a specific exception, a latency over a threshold - I would arrange for a dump or a trace to be collected automatically when it fires, rather than hoping someone is watching at the right moment.
The other half of the answer is to narrow the space while waiting. Once a week is a clue in itself. It suggests something periodic: a scheduled job, a cache expiry, a token refresh, a certificate rotation, a deployment window, a batch that only runs on a certain day. I would line the failure timestamps up against every scheduled thing in the system before assuming it is random.
Red flag answer: “I’d add more logging and wait.” - Half right, and it is what most people do, but without correlation and without error-biased sampling you usually end up with more volume and the same blind spot.
Follow-up: “The timestamps line up with your nightly job. How do you prove causation rather than coincidence?”
Q15. Your Logs Are 40 GB a Day and You Still Can’t Answer “Why Was This Request Slow?”
Senior
Because volume is not the same as signal, and 40 GB of the wrong thing answers nothing.
Three failures usually produce this. The logs are unstructured, so you can grep them but you cannot aggregate them - $"Order {id} took {ms}ms" is a string, and you cannot ask it “show me p99 by endpoint”. Structured logging with real properties turns the same line into something queryable.
They have no correlation, so a single request’s journey is scattered across services and instances with nothing tying it together. Without a trace or correlation ID you cannot reassemble the story, only read fragments of it.
And they log the wrong altitude - entry and exit of every method, which is where most of the 40 GB comes from, but no timings around the boundaries that actually vary: the database call, the HTTP call, the cache lookup, the lock acquisition.
What I would want instead is distributed tracing for the “where did the time go” question, with logs carrying the trace ID so the two link up, and log levels that let me raise verbosity for one tenant or one endpoint without raising it for everyone.
Red flag answer: “We should log less.” - Correct instinct, wrong conclusion. The problem is not the amount, it is that none of it is queryable or correlated.
Q16. A Dependency Upgrade Broke Production but Every Test Passed. What Was Missing?
Senior
The honest answer is that the tests were testing our code and the change was in the seam between our code and the world.
The usual shapes: the tests mocked the dependency, so they asserted our assumptions rather than the library’s actual behaviour, and the assumptions were what changed. Or a transitive dependency moved - the package we upgraded pulled a different version of something three levels down, and nothing in the test suite exercised that path. Or the behaviour that changed is only visible under real conditions: connection pooling, timeout defaults, serialisation of an edge-case value, TLS negotiation, culture-sensitive parsing.
What I would put in place afterwards is a contract or integration test against the real dependency for the handful of behaviours we actually rely on - with Testcontainers if it is infrastructure - plus a canary or staged rollout so the first exposure to production traffic is 1% of it rather than 100%.
I would also say plainly that no test suite catches everything, and the useful question is how fast you noticed and how fast you rolled back. If the answer to both is “hours”, the gap is in observability and deployment, not in testing.
Red flag answer: “We need better test coverage.” - Coverage is a number that would not have moved here. The gap was the kind of test, not the quantity.
Follow-up: “How would you decide which dependencies deserve a contract test and which don’t?”
Q17. p50 Is 40ms. p99 Is 6 Seconds. Where Do You Look First?
Senior
That gap is the interesting part. It means the common path is healthy and something is happening to a small subset of requests - so I am not looking for slow code, I am looking for something that occasionally stops otherwise-fine code.
The candidates I would work through, roughly in order of how often they turn out to be the cause: GC pauses, which hit whatever request is unlucky enough to be in flight, and show up in dotnet.gc.pause.time correlated against the latency spikes. Thread pool queuing, where the request was fine but waited to start. Lock contention, visible in dotnet.monitor.lock_contentions. Connection pool exhaustion on the database or HTTP client, where the work is fast but acquiring a connection is not. Data skew - the p99 is one tenant with a hundred times more rows than everyone else, and the code is fine.
The distinguishing move is to check whether the slow requests are a random sample or a pattern. If they cluster by endpoint, tenant, or payload size, it is data. If they are scattered uniformly, it is something process-wide - GC, pool, or a noisy neighbour.
Red flag answer: “I’d optimise the slowest endpoint.” - The p50 says the endpoints are fine. Optimising code that already runs in 40ms will not move a p99 caused by a stop-the-world pause.
Follow-up: “The slow requests all belong to one tenant. What now?”
Performance and Measurement
The single most reliable senior signal in a technical interview is whether you distinguish between a change you believe is faster and a change you have measured.
Q18. You Say Your Change Made the Endpoint Three Times Faster. Prove It.
Senior
For a method-level change, BenchmarkDotNet, because it handles the things a stopwatch loop gets wrong: it warms up so you are not measuring JIT compilation, it runs enough iterations to get a distribution rather than a number, it reports variance so you can tell a real difference from noise, and with [MemoryDiagnoser] it reports allocations - which is often where the actual win is.
For an endpoint-level change, a benchmark is the wrong instrument entirely. I would run a load test at realistic concurrency against both versions and compare percentile latency and throughput, because the thing I changed might be faster in isolation and irrelevant under contention.
The part I would say out loud is the comparison discipline: same hardware, same data volume, same warm state, one variable changed. A “3x faster” that came from a warm cache on the second run is not a result, and an interviewer who has been burned by that before is specifically listening for whether you mention it.
Red flag answer: “I timed it with a Stopwatch in a loop.” - It is the honest instinct, and it is how most people start, but without warmup and iteration counts you are frequently measuring JIT and GC timing rather than your code.
Follow-up: “Your benchmark says 3x. Production shows no change. What happened?”
Q19. Why Do Microbenchmarks Lie?
Senior
Because a benchmark removes exactly the conditions that make production slow.
The JIT will happily optimise away work whose result you never use, so a benchmark can measure nothing at all and report an impressive number. Branch predictors and CPU caches get unrealistically warm when you run the same tiny input a million times - real traffic has cold caches and unpredictable branches. There is no contention: no other threads, no GC pressure from the rest of the app, no lock queue. And the input is uniform, where production data has outliers that are usually the whole problem.
The specific way this bites: you make a method 40% faster in a benchmark, ship it, and nothing moves - because that method was 2% of request time, and the other 98% is waiting on a database call. Amdahl’s law is doing the work there, not the benchmark being wrong exactly, but the benchmark being asked the wrong question.
So my position is that microbenchmarks are the right tool for comparing two implementations of the same thing, and the wrong tool for deciding whether that thing is worth optimising. Profiling decides what to optimise; benchmarking decides how.
Red flag answer: “You just need more iterations.” - More iterations fixes noise. It does not fix measuring something that does not matter.
Q20. Where Do You Actually Spend Optimisation Effort on a Slow API? Give Me the Order.
Senior
I would work outside in, because that is the order the wins are usually sized in.
Measure first and find where the time actually goes. Nothing before this step.
Then the database, because in a typical CRUD-shaped API most of the wall clock is there: a missing index, a query returning far more rows than the endpoint needs, or an N+1. These are usually the largest single wins available and the cheapest to make.
Then the network shape - how many round trips the endpoint makes, sequential calls that could run concurrently, payloads bigger than the client needs.
Then caching, deliberately, once I know what is expensive and how stale it is allowed to be.
Then the code - allocations, serialisation, hot-path work.
Then the runtime - GC configuration, pooling, and the rest of the knobs. Last, because it is the smallest lever and the easiest one to get wrong.
The judgment being tested is that you do not start at the bottom. Tuning GC settings on a service whose real problem is a missing index is a very expensive way to achieve nothing.
Red flag answer: “I’d add caching.” - Sometimes correct, but as a first move it hides the problem, adds an invalidation bug surface, and makes the underlying query slower to find later.
Q21. When Is “Just Add a Cache” the Wrong Answer?
Mid
When the underlying operation is wrong rather than slow. Caching a query that returns the wrong data just serves the wrong data faster, and now it is stale as well.
When the data cannot tolerate staleness. Anything a user just wrote and expects to see, anything used for an authorisation decision, anything financial. Caching a permission check is how you end up serving a revoked user their old access for the length of the TTL.
When the hit rate will be low. A cache in front of data with a long tail of unique keys costs you a lookup, a serialisation, and memory, and returns misses. You need to know the access distribution before you can say caching helps.
When it hides a fixable problem. If a query takes 4 seconds because it has no index, caching it means it takes 4 seconds for one unlucky user every TTL, forever, and nobody will ever go back and add the index.
And when invalidation is genuinely harder than the original problem. If the data has several writers, caching converts a performance question into a correctness question, and correctness bugs cost more.
Red flag answer: “Caching is always good if you set a short TTL.” - A short TTL reduces staleness and destroys hit rate at the same time. If the TTL has to be short enough to be safe, the cache often is not buying you anything.
Data at Scale
Query mechanics live on the EF Core page. These are the questions about what happens when the data outgrows the assumptions the code was written under.
Q22. Your Read Replica Is Four Seconds Behind and Users See Stale Data Right After Saving. Fix It.
Senior
This is read-your-own-writes, and it is a consistency problem that got created the moment someone pointed reads at a replica without deciding what to do about the lag.
The pragmatic fix is to route reads to the primary for a short window after a write by that user - usually by stamping the session with a timestamp or a log position on write and sending reads to the primary until the replica has caught past it. It keeps the replica benefit for the 95% of traffic that is not reading its own recent write.
The cheaper variant, which is often enough, is to route by operation: anything in a write-then-read flow reads the primary; reporting, search, and list views read the replica. It is coarse but it is easy to reason about and hard to get subtly wrong.
The third option is to stop lying to the user. If the write is genuinely asynchronous, showing “saved, updating shortly” is more honest than pretending it is immediate and then contradicting yourself. That is a product decision as much as a technical one, and being willing to raise it is part of the answer.
Red flag answer: “Increase the replica’s resources so it keeps up.” - Replication lag under load is a normal operating condition, not a bug you can buy your way out of. Any design that assumes zero lag will break again.
Follow-up: “How would you measure the lag well enough to alert on it?”
Q23. A Nightly Batch Job Locks the Orders Table and the API Starts Returning 500s. What Do You Change?
Senior
The batch is doing one enormous transaction, holding locks that escalate to the table, and holding them for minutes. Everything else queues and then times out.
The change I would make first is batching: process in chunks of a few thousand rows, commit each chunk, and pause briefly between them. That converts one long lock into many short ones, which the API can interleave with. It also makes the job restartable, which matters the first time it fails halfway through.
Second is to narrow what is locked - operate on an indexed key range so the engine takes row or page locks rather than escalating to the table, and make sure the job’s WHERE clause is actually using an index rather than scanning.
Third is to ask whether it needs to touch the operational tables at all. A lot of nightly jobs are aggregations that would be happier reading a replica or a copy, and never contending with live traffic in the first place.
I would also set a lock timeout on the batch rather than the API. If something has to lose, it should be the job that can retry at 4am, not the customer request.
Red flag answer: “Run it at a quieter time.” - Fine mitigation, no fix. It works right up until the business has customers in another timezone, and it does nothing about the job’s runtime growing with the data.
Q24. You Need to Add a NOT NULL Column to a 200-Million-Row Table With Zero Downtime. Walk Me Through It.
Senior
Not in one migration, because a single ALTER that adds a non-nullable column with a default rewrites the table and holds a lock for as long as that takes.
The pattern is expand and contract, in separate deployments:
- Expand. Add the column as nullable, with no default and no constraint. On a modern engine this is a metadata-only change and effectively instant.
- Backfill. Populate it in batches, committing each chunk, throttled so it does not saturate the IO the live traffic needs. This can run for hours; that is fine, because nothing is blocked.
- Dual-write. Deploy code that writes the new column on every insert and update, while still tolerating nulls on read. Now the data stops going stale behind the backfill.
- Contract. Once the backfill is complete and verified, add the
NOT NULLconstraint, and only then deploy code that assumes the column is always present.
The rule underneath all of it is that schema changes and code changes must be independently deployable and backward compatible for one release, because during a rolling deploy old and new code run against the same database at the same time. Any step that requires them to change together is a step that requires downtime.
Red flag answer: “Add it with a default value in one migration.” - On some engines the default makes it worse, not better, because it forces the rewrite the metadata-only path was avoiding.
Follow-up: “The backfill has been running for six hours and is a third done. Do you let it finish?”
Q25. When Do You Leave the Relational Database, and What Do You Actually Gain?
Senior
Rarely, and the honest answer starts by saying so, because most “we need NoSQL” conversations turn out to be “we need an index”.
The cases where I think it genuinely earns its place: the access pattern is a known key lookup at a scale where horizontal partitioning matters more than joins; the data is genuinely schemaless and varies per record in ways a table cannot express without a sea of nullable columns; it is time-series or append-only at a volume where a purpose-built store is an order of magnitude cheaper; or it is full-text and relevance search, where a search engine is not really a database replacement at all but a specialised index alongside one.
What you gain is a data model shaped like your access pattern, and horizontal scaling that does not require you to solve distributed joins.
What you give up is worth naming explicitly, because it is where the regret comes from: transactions across entities, ad-hoc querying for anything you did not anticipate, and the enormous amount of operational knowledge that exists for relational engines and does not for whatever you picked instead. Also, you rarely replace the relational database - you add a second store, and now you own consistency between them.
Red flag answer: “Relational doesn’t scale.” - It scales further than almost any team will ever need, and saying this signals you have absorbed a conference talk rather than hit the limit.
Architecture and Judgment
These have no correct answer. They are testing whether your instinct is to make things better or to make things yours.
Q26. You’ve Inherited a Codebase With Three Competing Patterns. What Do You Do in Your First Month?
Senior
Nothing structural, deliberately. The first month is for understanding why it looks like this, because a codebase with three patterns usually has three eras, and each one was a reasonable decision at the time given constraints I cannot see yet.
What I would actually do: ship small features across all three areas, because that teaches you where the pain really is far faster than reading does. Talk to whoever has been there longest and find out which decisions were deliberate and which were accidents. And watch which parts of the codebase generate the incidents and the slow reviews, because that is the real priority list, not the part that offends me aesthetically.
Then I would pick one thing - the one causing measurable pain - and set a direction for new code rather than launching a migration. New code follows the chosen pattern; old code gets converted when it is being touched anyway. That way the improvement compounds without a big-bang rewrite that competes with feature work and gets abandoned at 60%.
The thing I would say explicitly is that three patterns is not automatically a problem worth solving. If they are separated by clear boundaries and each is internally consistent, unifying them can cost more than it returns.
Red flag answer: “I’d standardise everything onto the pattern I know best.” - Fastest way to burn credibility and a quarter simultaneously.
Q27. Your Team Wants to Adopt Something New. How Do You Decide?
Senior
I would want three things answered before the technical merits even come up.
What problem are we having? Not what the tool does - what is hurting now. If nobody can point at a concrete pain, the honest answer is that someone read a good blog post, and that is not a reason.
What does it cost after the demo? Every adoption has a long tail: the team learning it, the CI changes, the debugging story when it misbehaves at 2am, the upgrade treadmill, and the fact that hiring now has one more requirement. The demo is always the cheapest hour you will ever spend with it.
How do we get out? If this turns out to be wrong in a year, what does reversing it cost? A library behind an interface is cheap to reverse. A messaging platform or a database is not. I am much more willing to say yes to things with a cheap exit.
Then I would want it proven on something real but small - one service, one feature, with an agreed date to decide - rather than a proof of concept that was always going to succeed because it avoided every hard part.
Red flag answer: “If it’s the industry standard, we should use it.” - Industry standard for whom, at what scale, with what team size. Most tools that are standard at a thousand engineers are overhead at ten.
Q28. Tell Me About a Technical Decision You’d Make Differently Today.
Senior
This is not a trick, and it is not really about the decision. The interviewer is checking three things: that you have owned something long enough to see its consequences, that you can criticise your own work without either defensiveness or performative self-flagellation, and that you updated on evidence rather than on fashion.
The answer that works has a specific shape. Name the decision and the context that made it reasonable at the time - because a decision that was obviously wrong when you made it says something worse about you than one that turned out wrong. Name the specific thing that revealed the problem: an incident, a cost line, a feature that took three times longer than it should have. Then name what you would do instead and why that is different, not just newer.
The common failure is picking something with no stakes - “I’d have named that variable better” - which reads as either not having owned anything real or not being willing to be honest in an interview. The other failure is blaming the constraints. Deadlines and legacy code are real, but the question is what you would do differently, and “nothing, it was the deadline’s fault” is an answer nobody scores well.
Red flag answer: “I can’t think of one.” - The only genuinely wrong answer to this question.
Q29. How Do You Decide Which Technical Debt to Pay Down and Which to Leave?
Senior
By what it is costing, not by how much it bothers me. Most codebases have a great deal of ugly code that is completely stable and completely irrelevant, and rewriting it is a hobby, not engineering.
The debt worth paying is the debt with interest: the module that shows up in every incident, the area where every estimate is wrong by a factor of three, the thing that blocks a change the business actually wants, the pattern that is being copied into new code and therefore growing. That last one is the most urgent, because it is the only kind that compounds.
The debt worth leaving is anything in a stable, rarely-touched area with no incidents attached, or anything in a component with a known end date. Cleaning up code that is being deleted in six months is a pure loss.
The part I would emphasise is that this has to be argued in the business’s terms to get funded. “This is badly written” gets you nothing. “This module caused four of our last six incidents and every change to it takes three times longer than estimated” gets you a sprint. Same debt, and the second framing is the one that is actually true anyway.
Red flag answer: “We should allocate 20% of every sprint to tech debt.” - It sounds disciplined and it is usually the first thing cut under pressure. A named, justified piece of work in the plan survives; a percentage does not.
Q30. The Architecture You’ve Inherited Is Wrong and a Rewrite Is Off the Table. What’s Your Play?
Senior
Accept the constraint, because it is almost always the right one. Rewrites fail at a rate that should make anyone cautious, and “off the table” usually means someone has already watched one fail.
What I would do instead is stop the bleeding first: new code goes behind a boundary that does not depend on the bad design, so the problem stops growing even if it does not shrink. Then strangle it incrementally - route one capability at a time through the new path, keep both running until the old one has no traffic, and delete it. Every step ships, and any step can be the last one if priorities change, which is exactly what a rewrite cannot offer.
The prerequisite for all of it is a seam. If the current design has no boundaries, the first real work is introducing one - an interface, a facade, an anti-corruption layer at the edge - so there is somewhere to put the new implementation.
I would also be honest that sometimes the correct answer is that it stays wrong. If the system is stable, the team is delivering, and the wrongness is aesthetic rather than operational, the cost of fixing it may simply exceed the cost of living with it. Being able to say that out loud is a senior signal in itself.
Red flag answer: “I’d build a v2 alongside and switch over when it’s ready.” - That is a rewrite with a different name, and it has the same failure mode: it competes with feature work, drifts from the original as the original keeps changing, and gets abandoned at 70%.
Q31. How Would You Scale This to Ten Times the Traffic?
Senior
I would start by refusing to answer it in the abstract, politely, because “10x traffic” is not one problem. 10x reads and 10x writes have almost nothing in common, and a 10x sustained increase is a completely different exercise from a 10x spike.
So the first move is to ask what shape the traffic is, then find the actual constraint. Usually the answer is not the application tier - that is the easy part to scale, because stateless services scale horizontally by adding instances. The constraint is almost always the database, or a piece of shared state somebody added without noticing, or a downstream service that is not scaling with you.
Then, in order of cost: cache the reads that dominate and tolerate staleness. Scale reads horizontally with replicas. Move anything that does not need to be in the request path out of it - notifications, exports, anything the user does not wait on. Only then look at partitioning writes, because sharding is the point where the system gets meaningfully harder to operate forever.
And I would name the thing most people skip: at 10x, the failure modes change. Retries that were harmless become an amplifier, a cache miss storm can take out the database that was previously fine, and connection pools that were generous become the bottleneck. Capacity planning without failure-mode planning is half an answer.
Red flag answer: “Add more instances and put it behind a load balancer.” - That scales the tier that was never the problem, and at 10x it usually makes the database problem arrive faster.
Migration and the 2026 Reality
The last section is a currency check. These questions are cheap for the interviewer to ask and very revealing, because the answers change every couple of releases.
Q32. You Have a .NET Framework 4.8 Monolith and a Mandate to Modernise. What’s the Plan?
Senior
Inventory before anything else, because the plan depends entirely on what is in there. I want to know which dependencies have modern equivalents, which have replacements that are not drop-in, and which have nothing at all - because that last list determines whether this is a port or a rewrite of those pieces.
Then I would move in this order. Retarget the projects to modern SDK-style project files first, which can be done while still on Framework and removes a lot of noise. Move shared libraries next, ideally to netstandard2.0 so both worlds can consume them during the transition. Then move the leaf projects, then the entry point last, because it depends on everything else.
The delivery pattern I would argue for is the strangler fig: put a reverse proxy in front, move one route or capability at a time to the new application, and let the two run side by side. That keeps every step shippable and reversible, which is the only way a migration of this size survives contact with a roadmap.
One tooling note that dates people: the .NET Upgrade Assistant is deprecated. Microsoft now points at the GitHub Copilot app modernization agent instead, which is in Visual Studio 2026 and recent 2022 builds. Recommending Upgrade Assistant in 2026 is a small thing that tells the interviewer when you last did this.
Red flag answer: “We’d do a big-bang rewrite, it’ll be cleaner.” - Cleaner, and unshippable for eighteen months while the old system keeps changing underneath you.
Q33. What in .NET Framework Has No .NET 10 Equivalent?
Senior
The short list, and this is exactly what the inventory step in the previous question is looking for:
ASP.NET Web Forms. Nothing to port to. It has to be rewritten, usually to ASP.NET Core MVC, Razor Pages, or Blazor depending on how interactive it is.
Server-side WCF. The client side has support, but hosting WCF services does not exist in the box in modern .NET. CoreWCF is a community-driven .NET Foundation project that covers a lot of the surface area and is Microsoft-supported as a migration path, but it is a migration rather than a recompile.
Windows Workflow Foundation. No equivalent. Whatever it was orchestrating has to be re-expressed, often as a durable workflow engine or explicit state machines.
AppDomains. Not supported. Isolation now means separate processes, and plugin loading means AssemblyLoadContext, which is similar in spirit and different in practice.
BinaryFormatter. Worth being precise about, because it is a common thing to get slightly wrong: it was obsoleted in .NET 5, became a compile error in .NET 7, started throwing at runtime in .NET 8, and the in-box implementation was removed in .NET 9. The APIs still exist and always throw. There is an unsupported System.Runtime.Serialization.Formatters package that restores the old behaviour along with all of its vulnerabilities, and reaching for it should be a deliberate, temporary decision with a date on it.
Red flag answer: “BinaryFormatter was removed in .NET 10.” - Close enough that a lot of blog posts say it, and wrong. It went in .NET 9.
Q34. How Do You Keep a Large Codebase Current Across Annual .NET Releases?
Senior
By treating it as routine maintenance rather than a project, because the moment upgrading becomes a project it stops happening and you end up doing four versions at once.
The practical mechanics: centralise versions with Directory.Packages.props so a bump is one file rather than forty. Keep the TFM current even when nothing forces it, because the cost of one version is small and the cost of four is not linear. Read the breaking-changes list for each release - it is short, and it is the highest-value hour of the whole upgrade.
I would target LTS to LTS for anything large and slow-moving, staying on .NET 10 until .NET 12, and I would upgrade smaller services on every release so the team keeps the muscle. That way problems get found by the service where they are cheap to fix.
The other half is the safety net. Upgrades are exactly the change that passes tests and fails in production, because what shifts is often runtime behaviour rather than API surface - GC defaults being the current example, and DATAS specifically being the one people are hitting on .NET 9 and 10. So: canary the deploy, and watch latency percentiles and memory for a full traffic cycle rather than declaring victory when the build goes green.
Red flag answer: “We upgrade when there’s a security patch that forces it.” - Understandable and very common, and it converts a small recurring cost into a large unplanned one.
Q35. Your Team Uses AI Coding Tools. How Do You Keep Quality From Sliding?
Senior
This is a 2026 question and it is starting to appear in job descriptions, so it is worth having a real position rather than a diplomatic one.
The failure mode I would name is not that the generated code is bad - it is usually fine in isolation. It is that the volume of code goes up faster than the volume of understanding. Review capacity becomes the bottleneck, and the thing that quietly gives way is the reviewer’s willingness to actually read a large diff. That is where defects get in.
So the practices I would want: the author is accountable for the code regardless of what wrote it, which means being able to explain every line in review - if you cannot, it does not ship. Small diffs stay small, because a 2,000-line generated PR gets rubber-stamped and everyone knows it. Tests get written or at minimum verified by a human, because generated tests have a habit of asserting what the code does rather than what it should do. And project conventions live somewhere the tool actually reads, so it produces code that fits the codebase instead of a plausible average of the internet.
The thing I would flag as the real risk is architectural drift: these tools are excellent at producing more of what already exists and poor at noticing that the existing pattern is wrong. Design decisions still need a human who is looking at the whole system.
Red flag answer: “We ban them” or “we let it write everything and review the output.” - The first loses the productivity for no quality gain, since people will use them anyway. The second is where the incidents come from.
Key Takeaways
- Senior questions test process, not recall. There is rarely one right answer; the interviewer is watching whether you narrow the problem space or start guessing fixes.
- Name the measurement before the cause. “I’d check
dotnet.thread_pool.thread.countagainst CPU” beats “it’s probably thread pool starvation” every time, and asserting a cause without evidence is the most common senior rejection reason. - Slow requests with idle CPU means thread pool starvation, and the cause is almost always sync-over-async:
.Result,.Wait(), or.GetAwaiter().GetResult()on a request path. - Know the diagnostics escalation:
dotnet-countersto triage,dotnet-stackfor a constant issue,dotnet-tracefor an intermittent one,dotnet-gcdumpfor memory,dotnet-dumplast. - Currency is checked cheaply. DATAS has been the default since .NET 9,
BinaryFormatter’s in-box implementation was removed in .NET 9, and the .NET Upgrade Assistant is deprecated - getting any of these wrong dates you instantly.
How to Prepare for a Senior .NET Interview
If you have a loop coming up, the ranked version of what actually moves the needle:
- Be able to describe a production incident end to end. What broke, how you found it, what you changed, what you would do differently. This is the single most-asked senior question shape and the most commonly under-prepared.
- Know the diagnostics tools by name.
dotnet-counters,dotnet-stack,dotnet-trace,dotnet-gcdump,dotnet-dump. Even naming the right one for a scenario puts you ahead of most candidates. - Have a defensible position on two or three architecture debates. Microservices, CQRS, the repository pattern. Not the correct answer - a position, with the conditions attached.
- Refresh your currency. The .NET 9 and 10 changes in this article are exactly the kind of detail that dates a candidate: DATAS, the removed
BinaryFormatter, the deprecated Upgrade Assistant, the renamed counters. - Practice saying “I’d measure that.” Then say what you would measure and what result would change your mind. That sentence, said naturally, is most of what separates a senior answer from a confident guess.
What is the difference between a senior and a mid-level .NET interview?
A mid-level interview tests whether you know how the framework works. A senior interview tests judgment under incomplete information: diagnosing a production problem with partial evidence, defending an architecture decision with its trade-offs, and knowing when not to adopt something. Senior questions usually have no single correct answer, only a correct process.
How many rounds are in a senior .NET interview in 2026?
Typically four to five: a recruiter screen, a technical deep dive on .NET and the runtime, a system design round, a behavioural or leadership round, and often a final conversation with a hiring manager or skip-level. Some companies fold system design into the technical round for individual contributor roles.
Do senior .NET interviews still include coding exercises?
Often, but the emphasis shifts. Instead of an algorithm puzzle you are more likely to get a code review exercise, a debugging scenario, or a small design task where the interviewer is watching how you handle ambiguity and trade-offs rather than whether you reach a specific solution.
What .NET version should I prepare for in 2026?
Prepare on .NET 10, which is the current LTS release and is supported through November 2028. Know what changed since .NET 8, because that is the upgrade path most teams are on. The details that most commonly date a candidate are DATAS being the default GC adaptation mode since .NET 9, BinaryFormatter's in-box implementation being removed in .NET 9, and the .NET Upgrade Assistant being deprecated.
How much system design is expected of a senior .NET developer?
Enough to design a service and defend the trade-offs: data storage choice, caching strategy, communication between services, failure handling, and how it scales. You are not usually expected to design a global-scale distributed system unless the role specifically calls for it. Being able to say what you would not build is worth as much as what you would.
What is the most common reason senior candidates get rejected?
Answering with certainty where the honest answer is uncertainty. Asserting a cause without evidence, recommending a technology without naming its cost, or describing a past decision as flawless all read as a lack of production experience. Saying what you would measure and what would change your mind is consistently scored higher.
How do I answer a question about something I have never done?
Say so, then reason from what you do know. Name the closest thing you have experience with, describe how you would approach the unfamiliar part, and say what you would need to find out first. Interviewers are usually testing your reasoning under uncertainty, and a confident wrong answer scores far worse than an honest one with a clear approach.
Are behavioural questions important for senior .NET roles?
Yes, and they carry more weight than at mid level. Seniority implies influence on other people's work, so expect questions about mentoring, disagreement, and decisions you would make differently. Prepare three or four real situations you can describe in detail, including what went wrong and what you learned.
Should I mention AI coding tools in a senior .NET interview?
Yes, if asked, and have a real position. Job descriptions increasingly reference AI-assisted development, and the useful answer covers how you keep review quality and architectural direction intact as code volume increases, not simply that you use the tools.
How long should I prepare for a senior .NET interview?
Two to four weeks of focused preparation is realistic if you are currently working in .NET. Most of that time is best spent recalling and structuring your own production experience rather than reading new material, because the hardest senior questions are about things you have done rather than things you know.
Wrapping Up
The pattern across all 35 of these is the same. Senior interviews are not testing whether you can recall more facts than a mid-level developer. They are testing whether you can operate when the information is incomplete, name a mechanism instead of a symptom, and be honest about the limits of what you know.
If you take one thing from this page, make it the habit of separating what you have measured from what you believe. Almost every strong answer above comes back to it: the leak you confirmed with a heap diff rather than assumed, the starvation you saw in a counter rather than guessed at, the optimisation you proved with a benchmark rather than felt. That habit is the senior signal, and it is visible in about thirty seconds of conversation.
Work through the topic pages below for the depth on each area, and drill the ones you are least comfortable with in the free mock interview until the answers come out under pressure rather than on the page.
Happy Coding :)
.NET Interview Questions
300+ real .NET interview questions with answers, red flags, and follow-ups - C#, EF Core, ASP.NET Core, system design
What's your take?
Push back, share a war story, or ask the obvious question someone else is wondering. I read every comment.