Every ASP.NET Core system design question eventually comes back to the request pipeline, because it is where hosting, security, performance and correctness all collide. Interviewers use pipeline questions to separate candidates who memorized app.Use...() calls from candidates who can explain why a line has to sit where it does, trace a request through Kestrel, DI and routing without notes, and reason about what happens when something in that chain fails halfway through. For engineers with a decade or more of experience, the bar isn't reciting the middleware list; it's diagnosing a production incident, such as a redirect loop behind a load balancer or a captured HttpContext that outlives its request, from first principles. The ten questions below cover the hosting model, Kestrel, middleware ordering, endpoint routing internals, filters versus middleware, HttpContext lifetime and IIS hosting, the topics that come up most often in senior and lead loops.
Q1 Walk through what happens between the moment a client connects to Kestrel and the moment your endpoint handler runs. Where does it typically go wrong in production?#
Short answer: Kestrel accepts the connection, negotiates TLS and the HTTP version, and hands the parsed request to the hosting layer, which builds an HttpContext and opens a dependency injection scope for it. The request then flows through registered middleware in order; routing matches an endpoint partway through, later middleware reads that endpoint's metadata, and the endpoint itself finally runs and writes the response, which unwinds back out through the same middleware in reverse.
Breaking it into stages makes the failure points obvious:
- Transport. Kestrel accepts a TCP or QUIC connection. For HTTPS, the TLS handshake and ALPN negotiation pick HTTP/1.1, HTTP/2 or HTTP/3 before a single byte of your code runs. A misconfigured certificate or an unreachable client fails here, long before your handlers see anything.
- Context creation. The server builds an
HttpContextand the host creates a DI scope for it (HttpContext.RequestServices). A scoped service registered incorrectly, or resolved from the wrong scope, fails at this boundary or later when it is first requested. - Middleware, in registration order. Each component can inspect or rewrite the request, short-circuit, or call the next one. This is where forwarded-header, exception-handling and static-file bugs live.
- Routing.
UseRoutingmatches the URL and method against every mapped endpoint and stores the winner and its route values on the context, without running it yet. - Policy middleware. Authentication, authorization, CORS, rate limiting and output caching, registered after routing, read the selected endpoint's metadata and enforce it.
- Execution. The endpoint binds parameters, runs its filters, invokes your code and writes a result.
- Unwind. The response flows back through the middleware chain. Once any byte of the body is flushed,
HttpResponse.HasStartedbecomestrueand headers are frozen.
app.Use(async (context, next) =>
{
// Nothing has matched yet: this prints "(none)" on every request.
app.Logger.LogInformation("Before routing: {Endpoint}", context.GetEndpoint()?.DisplayName);
await next(context);
});
app.UseRouting();
app.Use(async (context, next) =>
{
// Runs after matching, so the endpoint is now visible.
app.Logger.LogInformation("After routing: {Endpoint}", context.GetEndpoint()?.DisplayName);
await next(context);
});What interviewers look for: a layered mental model (transport, host, middleware, routing, endpoint) rather than a flat list of method calls, and the instinct to say where in that chain a given bug class lives.
Common mistakes: treating Kestrel as "the framework" instead of the server underneath it; assuming routing executes the endpoint immediately instead of only selecting it; forgetting that headers freeze once the response starts.
Follow-up questions:
- What's stored on
HttpContextafterUseRoutingruns, and what reads it? - How would this trace change with a reverse proxy in front of Kestrel?
Q2 What does WebApplicationBuilder actually configure, and how does Kestrel relate to a reverse proxy in production?#
Short answer: WebApplication.CreateBuilder(args) wires up layered configuration, console/debug logging, Kestrel (with IIS integration) as the server, and a dependency injection container with scope and build validation enabled in Development. Kestrel is fully supported as an internet-facing server on its own, but most production deployments still put a reverse proxy or load balancer in front of it for shared ports, centralized TLS and defense in depth.
CreateBuilder is one of three factory methods, and picking the right one is itself a hosting decision:
| Factory | Includes | Typical use |
|---|---|---|
CreateBuilder | HTTPS, HTTP/3, IIS integration, full defaults | Most web apps and APIs |
CreateSlimBuilder | Config, logging, no HTTPS/QUIC/IIS integration | Native AOT services behind a TLS-terminating proxy |
CreateEmptyBuilder | Nothing; you add the server and everything else | Specialized or minimal hosts |
Kestrel itself is built on System.IO.Pipelines, speaks HTTP/1.1 and HTTP/2 by default, and can add HTTP/3 over QUIC when you opt in and the MsQuic library is present. Whether Kestrel sits at the edge or behind a proxy, it never depends on IIS or System.Web, which is the core architectural break from classic ASP.NET: the server runs inside your own process.
What interviewers look for: knowing that Kestrel is production-ready on its own, understanding why teams still add a proxy anyway (TLS offload, multiple apps per port, WAF, load balancing), and awareness that the slim and empty builders trade defaults for a smaller, faster-starting app.
Common mistakes: claiming Kestrel "isn't safe for the internet," a holdover from pre-.NET-Core guidance; not knowing that CreateSlimBuilder drops HTTPS and HTTP/3 by default.
Follow-up questions:
- Why would you choose
CreateSlimBuilderoverCreateBuilder? - What breaks if you deploy a
CreateSlimBuilderapp directly to the internet without a proxy?
Q3 Why does middleware order matter, and what's a realistic bug caused by getting it wrong?#
Short answer: Middleware forms nested layers around whatever runs after it, so code before next() executes in registration order and code after it executes in reverse; putting a component before or after the wrong neighbor silently changes what it sees or protects, and the app still compiles and runs.
A production-shaped order looks like this, and each line has a reason:
app.UseForwardedHeaders(); // Must run before anything reads scheme or client IP
app.UseExceptionHandler(); // Must wrap everything that can throw
app.UseHttpsRedirection();
app.UseRouting(); // Selects the endpoint; nothing below can see it without this
app.UseCors(); // Before auth, so preflight requests aren't rejected
app.UseAuthentication();
app.UseAuthorization();
app.UseRateLimiter(); // After routing, so per-endpoint policies apply
app.UseOutputCache(); // After auth, or it can serve one user's cache to another
app.MapControllers();A realistic incident: a team added output caching to speed up a catalog endpoint and registered UseOutputCache() before UseAuthentication(). Nothing failed in testing, because the test account always requested the same personalized banner. In production, a promotional banner meant for one logged-in customer was cached and served to the next anonymous visitor, because the caching middleware ran before authorization had a chance to mark the request as needing per-user handling. The fix wasn't a code change to the endpoint at all, just moving one line.
What interviewers look for: the ability to justify each position, not just recite an order; a real story about a silent, hard-to-reproduce bug rather than a syntax error.
Common mistakes: putting CORS after authorization, which breaks preflight OPTIONS requests; registering routing-dependent middleware, such as rate limiting policies, before UseRouting.
Follow-up questions:
- Why must exception-handling middleware be registered before everything it protects?
- What differs about the recommended order when the app sits behind a load balancer?
Q4 How does endpoint routing separate matching from execution, and why does that separation matter for policy middleware?#
Short answer: UseRouting evaluates every mapped endpoint's template against the request, picks the best match by specificity, and stores it and its route values on HttpContext without running it; the endpoint only executes once the pipeline reaches its position, which lets middleware in between inspect the selection and attach policy before any application code runs.
That gap is exactly how authorization, CORS and rate limiting apply per-endpoint rules: they call context.GetEndpoint()?.Metadata.GetMetadata<T>() to read attributes like [Authorize] or RequireRateLimiting(...) that were attached when the endpoint was mapped.
app.UseRouting();
app.Use(async (context, next) =>
{
// Only meaningful because routing already ran and attached metadata.
if (context.GetEndpoint()?.Metadata.GetMetadata<AuditAttribute>() is not null)
{
var audit = context.RequestServices.GetRequiredService<IAuditLog>();
await audit.RecordAsync(context.User, context.Request.Path, context.RequestAborted);
}
await next(context);
});
app.MapDelete("/api/customers/{id:guid}", DeleteCustomer).WithMetadata(new AuditAttribute());A useful subtlety: a route constraint like {id:int} only disambiguates between candidate routes; a value that fails it produces a 404, not a validation error, because the constraint is a routing concern, not a business one. Since .NET 8, ShortCircuit() can run a matched endpoint immediately after routing, skipping the rest of the pipeline for cheap, high-volume paths, but a short-circuited endpoint cannot carry authorization or CORS metadata, because the middleware that would enforce it never runs; combining them throws at startup.
What interviewers look for: clear separation of "selecting" from "running," and the connection between that separation and how policy middleware actually reads its rules.
Common mistakes: assuming a 404 from a typed route parameter means validation failed; not knowing why short-circuited endpoints can't be [Authorize]d.
Follow-up questions:
- How does
MapGroupchange where metadata is attached? - Why can't
ShortCircuit()andRequireAuthorization()be used together?
Q5 What's the difference between middleware and filters, and when do you reach for each?#
Short answer: Middleware operates on the raw HttpContext for every request regardless of which endpoint, or whether one, is selected; filters run inside a specific endpoint's execution, after parameters are bound, so they can inspect or replace arguments and results.
| Mechanism | Scope | Sees bound parameters | Typical use |
|---|---|---|---|
| Middleware (before routing) | Every request | No | HTTPS redirection, static files, forwarded headers |
| Middleware (after routing) | Every request, with endpoint metadata | No | Authentication, authorization, CORS, rate limiting |
| Endpoint filters | Minimal API endpoints (and controllers via AddEndpointFilter) | Yes | Argument checks, cross-cutting logging with argument context |
| MVC filters | Controllers and Razor Pages | Yes, plus ModelState | Action-scoped policies, result shaping |
The practical rule: reach for middleware when a concern applies to HTTP traffic in general and doesn't need to know what the handler's parameters are, and reach for a filter when the logic depends on those parameters or on transforming the result of one specific kind of endpoint. A team that implements request validation as global middleware usually ends up parsing the body a second time by hand, because middleware runs before model binding; the same validation as an endpoint filter or an [ApiController] behavior gets the already-bound, already-typed object for free.
What interviewers look for: the binding-time distinction, not just "filters are more specific"; awareness that endpoint filters are the Minimal API analogue of MVC action filters and share the same motivating problem.
Common mistakes: writing a middleware component that re-parses the request body because it needed typed arguments; assuming MVC filters and endpoint filters are interchangeable across Minimal APIs and controllers without an adapter.
Follow-up questions:
- How would you apply one cross-cutting filter to both an MVC controller and a Minimal API group?
- Why can middleware not short-circuit based on a handler's bound arguments?
Q6 Describe HttpContext's lifetime and its DI scope. What breaks if you capture it somewhere that outlives the request?#
Short answer: HttpContext and the DI scope backing HttpContext.RequestServices are created when the request starts and disposed when it ends, so anything that stores the context itself, or a scoped service resolved from it, beyond that window is reading through a disposed object or racing another request that reused the same instance pool.
// Wrong: a singleton field capturing per-request state.
public sealed class BadAuditQueue(IHttpContextAccessor accessor)
{
private HttpContext? _lastContext; // Captured once, then stale or disposed forever
public void Enqueue() => _lastContext = accessor.HttpContext; // Don't do this
}
// Right: copy out only the values you need, synchronously, before any async gap.
public sealed class AuditQueue(IHttpContextAccessor accessor, Channel<AuditEntry> channel)
{
public ValueTask EnqueueAsync()
{
var context = accessor.HttpContext
?? throw new InvalidOperationException("No active HTTP request.");
var entry = new AuditEntry(context.User.Identity?.Name, context.Request.Path, DateTimeOffset.UtcNow);
return channel.Writer.WriteAsync(entry);
}
}IHttpContextAccessor uses AsyncLocal<T> to flow the context across await points within the same request, which is why it must be registered explicitly and why teams add it only when they genuinely need ambient access, typically inside a library that has no other way to reach the request. Passing an explicit value into a service's method call is almost always simpler and safer than passing HttpContext itself, because it removes any temptation to read from it after the request has completed, for example on a background thread that outlives the response.
What interviewers look for: the request-scoped lifetime stated precisely, plus a concrete failure mode (ObjectDisposedException, stale data, or a captive dependency) rather than a vague "it's not thread-safe."
Common mistakes: injecting HttpContext directly into a singleton constructor, which fails validation or captures a torn-down instance; queuing background work with the live HttpContext instead of a snapshot of the data it needs.
Follow-up questions:
- Why does
IHttpContextAccessorneedAsyncLocalinstead of a simple field? - What would
ValidateScopestell you about a singleton that captures a scoped service derived fromHttpContext?
Q7 In-process versus out-of-process hosting on IIS: what's actually different, and when would you choose each?#
Short answer: In-process hosting runs your app inside the IIS worker process (w3wp.exe) on the IIS HTTP Server, skipping Kestrel entirely and avoiding a loopback hop, while out-of-process hosting runs Kestrel in its own process that the ASP.NET Core Module starts, proxies requests to, and restarts on failure.
| Aspect | In-process (default) | Out-of-process |
|---|---|---|
| Server | IIS HTTP Server (IISHttpServer), inside w3wp.exe | Kestrel, in a separate process |
| Throughput | Higher; no loopback hop | Lower; ANCM proxies every request |
| Isolation | One app pool per app, matching bitness required | Kestrel process is independent of IIS |
| Configure with | Default | <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel> |
The ASP.NET Core Module (ANCM), installed by the .NET Hosting Bundle, is what makes either model work: it bootstraps the CLR (in-process) or manages and proxies to the Kestrel process (out-of-process). Windows also offers HTTP.sys as a third option, a kernel-mode server outside IIS that supports port sharing and kernel-mode Windows authentication, useful for intranet services that want some IIS-like features without running IIS itself. In practice, most new deployments default to in-process for the throughput win and reach for out-of-process only when they specifically need process isolation independent of the app pool, such as restarting the app without recycling IIS.
What interviewers look for: the process-boundary distinction stated correctly (whether Kestrel is even in the picture), and a reason to pick out-of-process beyond "it's the old default."
Common mistakes: assuming in-process still runs Kestrel behind IIS; not knowing that bitness and app-pool identity constraints come from in-process sharing the worker process.
Follow-up questions:
- What does ANCM do differently in each model?
- Why might a team deliberately choose out-of-process today?
Q8 An app works locally but redirects forever, or logs every request as coming from the same IP, once it's behind a load balancer. Diagnose it.#
Short answer: The load balancer terminates TLS and forwards plain HTTP with the original client details in X-Forwarded-* headers, so UseHttpsRedirection sees an insecure request and keeps redirecting, and anything reading HttpContext.Connection.RemoteIpAddress sees the balancer's address instead of the client's; the fix is the forwarded headers middleware, configured with an explicit trust list, registered before anything that depends on scheme or client address.
using Microsoft.AspNetCore.HttpOverrides;
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// .NET 10+: KnownIPNetworks (System.Net.IPNetwork) replaces the obsolete KnownNetworks.
options.KnownIPNetworks.Add(System.Net.IPNetwork.Parse("10.20.0.0/16"));
});
var app = builder.Build();
app.UseForwardedHeaders(); // Before HSTS, HTTPS redirection and authentication
app.UseHsts();
app.UseHttpsRedirection();Two traps make this harder to spot than it should be. First, since the 8.0.17 and 9.0.6 servicing updates, the middleware silently ignores forwarded headers from a sender that isn't in KnownProxies or KnownIPNetworks, which looks exactly like the middleware "not working" if you forgot to populate the trust list. Second, setting ASPNETCORE_FORWARDEDHEADERS_ENABLED=true does turn the middleware on, but it also clears the trust lists entirely, so it's only appropriate when the app is reachable exclusively through the proxy.
What interviewers look for: naming the forwarded-headers middleware specifically, not just "check the load balancer config," and knowing that trusting every sender by default would be a spoofing risk.
Common mistakes: enabling forwarded headers without a trust list; placing the middleware after HTTPS redirection instead of before it.
Follow-up questions:
- Why does the middleware require an explicit trust list instead of trusting all headers by default?
- What's different about
X-Forwarded-HostversusX-Forwarded-Forin this configuration?
Q9 How would you add request correlation IDs or timing across every endpoint without touching each handler? Where does it belong in the pipeline?#
Short answer: As a single piece of custom middleware, registered near the top of the pipeline so it wraps everything else, that reads or generates an ID before calling next() and, because response headers can't be set after the body starts flushing, writes the header back through Response.OnStarting rather than after the awaited call returns.
public sealed class CorrelationIdMiddleware(RequestDelegate next, ILogger<CorrelationIdMiddleware> logger)
{
private const string HeaderName = "X-Correlation-Id";
public async Task InvokeAsync(HttpContext context)
{
var correlationId = context.Request.Headers[HeaderName].ToString() is
{ Length: > 0 and <= 64 } incoming ? incoming : Guid.NewGuid().ToString("N");
context.Response.OnStarting(() =>
{
context.Response.Headers[HeaderName] = correlationId;
return Task.CompletedTask;
});
using (logger.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId }))
{
await next(context);
}
}
}Placing this as convention-based middleware, created once for the app's lifetime rather than per request, keeps the cost low; only reach for the IMiddleware factory pattern if the component genuinely needs a scoped constructor dependency such as a DbContext. Bound the incoming header's length before trusting it, since echoing arbitrary client input into logs and response headers is a log-injection vector. If the system already emits OpenTelemetry traces, the W3C traceparent header often makes a bespoke correlation ID unnecessary, because every log and span can already be joined on the trace ID.
What interviewers look for: knowing that late header writes must go through OnStarting, not a line placed after await next(context); validating untrusted input even in "just logging" code.
Common mistakes: setting the header after next(context) returns, which throws or silently fails once the response has started; trusting a client-supplied correlation ID without any bound on its length or characters.
Follow-up questions:
- Why does setting a header after
next()sometimes work in tests and fail in production? - When would you replace a custom correlation ID with the OpenTelemetry trace ID?
Q10 What happens if nothing in the pipeline handles a request, and what happens when an unhandled exception occurs after the response has already started?#
Short answer: If no middleware or endpoint writes anything, the pipeline's terminal delegate sets a plain 404; if an exception is thrown after the response has started (HttpResponse.HasStarted is true), the exception-handling middleware can no longer rewrite the status code or body, so it rethrows and the connection is aborted instead of producing a clean error page.
The exception-handling middleware distinguishes a few situations that a senior engineer should be able to separate cleanly:
- Exception before the response starts.
UseExceptionHandlerruns registeredIExceptionHandlerservices in order; the first one that returnstrueowns the response, typically writing an RFC 9457 ProblemDetails body throughIProblemDetailsService. - Exception after the response starts. Nothing can fix the response at that point. The middleware rethrows, and the client sees a truncated or reset connection rather than a friendly error.
- Client disconnects mid-request. An
OperationCanceledExceptionorIOExceptioncaused by the client going away is logged as a request-aborted event and reported with status 499, not treated as a server-side failure worth alerting on. - No endpoint matched at all.
UseRoutingfinds no candidate, so the terminal delegate answers 404; this is different from an endpoint matching but throwing, which is a 500 unless a handler intervenes.
What interviewers look for: the precise consequence of HasStarted, since it explains a whole class of "the error page didn't show up" tickets, and the distinction between a genuine 5xx and a client-initiated disconnect.
Common mistakes: assuming exception middleware can always produce a clean error response no matter when the exception occurs; treating every logged exception, including client aborts, as equally actionable.
Follow-up questions:
- Why would you check
HttpResponse.HasStartedbefore writing to the response from inside acatchblock? - How would you keep client-disconnect noise out of your error-rate alerts?
Quick-Fire Round#
| Question | Answer |
|---|---|
| What server does ASP.NET Core use by default? | Kestrel, a cross-platform, in-process HTTP server |
Does UseRouting execute the endpoint? | No, it only selects it and stores it on HttpContext |
What does a failed {id:int} route constraint return? | 404, because constraints disambiguate routes, not validate input |
| Where do endpoint filters run relative to model binding? | After binding, so they see the typed arguments |
| What freezes once the response starts? | Headers and the status code (HttpResponse.HasStarted) |
| Which hosting model skips the loopback hop on IIS? | In-process |
What must run before UseHttpsRedirection behind a proxy? | The forwarded headers middleware |
How do you write a header that depends on the outcome of next()? | Response.OnStarting, not code placed after the await |
| What status code signals a client-initiated disconnect? | 499 |
What replaces KnownNetworks for forwarded headers in .NET 10? | KnownIPNetworks (System.Net.IPNetwork) |
How to Prepare#
- Be able to draw the pipeline from memory: transport, host, middleware, routing, endpoint, unwind, and say what each layer owns.
- Practice justifying middleware order line by line instead of memorizing a fixed list; interviewers probe by swapping two lines and asking what breaks.
- Know the difference between "selecting" and "running" an endpoint, and which mechanisms read metadata in between.
- Rehearse one real (or realistic) incident involving middleware order, forwarded headers or a captured
HttpContext. Concrete stories outperform textbook answers. - Review
HttpResponse.HasStartedandResponse.OnStarting; late-header bugs come up constantly in practice and in interviews.