ASP.NET Core routing has always worked in two stages: the router picks which endpoint should handle a request, and then a separate piece of middleware actually executes that endpoint. .NET 8 adds a small but genuinely useful feature on top of this called short-circuit routing. It lets specific endpoints skip everything between those two stages, which reduces the overhead for requests you know don’t need authorization, CORS, or any other pipeline processing.
This is not a headline feature you will find in every release blog post, but it is one of those additions that solves a real, everyday annoyance: the constant stream of requests for favicon.ico, robots.txt, and similar well-known paths that every browser or crawler fires off automatically.
How endpoint routing works today
Routing in ASP.NET Core is split across two pieces of middleware, and understanding why helps explain what short-circuit routing actually changes. The first is the routing middleware, which looks at the incoming request and decides which registered endpoint should handle it. The second is the endpoint middleware, placed near the end of the pipeline, which executes whichever endpoint the router selected.
Splitting selection from execution is deliberate. It lets other middleware sit between the two stages and inspect metadata about the endpoint that is about to run, before it actually runs. Authorization middleware and CORS middleware both depend on this gap. They need to know which policy applies to the selected endpoint before that endpoint executes, so they are always placed between the routing middleware and the endpoint middleware. Picture the request flowing in, passing through the routing middleware, then through however many policy checks sit in between, and only then reaching the endpoint middleware that finally calls your handler.
This design works whether you are using minimal APIs or MVC controllers, and it is what makes the pipeline so extensible. You can attach custom metadata to an endpoint and write middleware that reacts to it, all while sitting comfortably between routing and execution. The trade-off is that every request, even a trivial one, walks through this full sequence.
What short-circuit routing changes
Some endpoints genuinely do not need any of that machinery. They do not need authorization, they do not need CORS, and they do not benefit from any middleware sitting between routing and execution. Short-circuit routing is built for exactly this case. When you mark an endpoint as short-circuited, the routing middleware executes it immediately instead of waiting for the endpoint middleware further down the pipeline, skipping every stage that would otherwise sit in between.
You enable it by calling ShortCircuit() on the endpoint when you register it. Here is the minimal example.
app.MapGet("/", () => "Hello World!")
.ShortCircuit(); // Add this
Once this line is in place, the “Hello World!” endpoint runs inside the routing middleware rather than the endpoint middleware. Nothing else about how you write the endpoint changes. You can also pass a status code, which the framework sets on the response automatically.
app.MapGet("/", () => "Hello World!")
.ShortCircuit(201); // Sets the status code to 201
That second overload is handy for endpoints that always return the same status regardless of what the handler itself does, which is exactly the case for most of the well-known paths this feature targets.
Where this feature actually pays off
The endpoint routing design exists for a reason and is genuinely useful, so short-circuiting is not something to sprinkle across your whole application. Once an endpoint is short-circuited, you lose the ability to apply CORS or authorization to it, along with anything else that relies on middleware sitting between routing and execution. The realistic use case is requests you already know will end in a 404, or requests that will never need authorization or CORS in the first place.
Well-known paths that browsers and crawlers request automatically are the clearest example. A few common ones are worth calling out specifically.
- /robots.txt, which tells web crawlers like Google what to index.
- /favicon.ico, the tab icon that every browser requests automatically.
- /.well-known/* (any path prefixed with .well-known/), used by specifications such as OpenID Connect, security.txt, and webfinger.
If your application does not explicitly handle these paths, every single request for them still walks all the way through your middleware pipeline before it finally returns a 404. Given how often browsers fire these requests automatically, that overhead adds up across a busy site, even though each individual request is trivial. Short-circuiting lets you skip that wasted work entirely.
Here is a small app that wires up all three cases together, along with a deliberately broken middleware to prove the short-circuit actually works.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
// return a 404 for favicon.ico
app.MapGet("/favicon.ico", () => Task.CompletedTask).ShortCircuit(404);
// return a valid robots.txt
app.MapGet("/robots.txt",
() => """
User-agent: *
Allow: /
""").ShortCircuit(200);
// any request starting with /.well-known/ returns a 404
app.MapShortCircuit(404, ".well-known");
app.UseRouting(); // not required (added by default) but explicit here
// any request NOT short-circuited hits this and always throws
app.Use((HttpContext _, RequestDelegate _)
=> throw new Exception("You shall not pass!"));
// this endpoint is never reached
app.MapGet("/", () => "Can't ever get to this");
app.Run();
Running this app confirms the behaviour: a request to /favicon.ico returns a 404, a request to /robots.txt returns 200 with the robots.txt body, and anything under .well-known returns a 404. Every other request hits the throwing middleware and the pipeline blows up, which is exactly what proves the short-circuited routes never reach that middleware at all. Obviously you would not deliberately throw in a real application; it is only here to demonstrate that the bypass genuinely happens.

How it is implemented under the hood
The implementation is smaller than you might expect, and reading through it is a good way to understand what the feature is and is not doing. There are essentially three moving parts: the ShortCircuit() extension method that attaches metadata to an endpoint, the MapShortCircuit() extension method that maps a whole route prefix in one call, and a check inside the routing middleware itself that looks for that metadata.
ShortCircuit() itself just adds a ShortCircuitMetadata instance to the endpoint’s metadata collection. A handful of common status codes are cached as static fields to avoid unnecessary allocations.
using Microsoft.AspNetCore.Routing.ShortCircuit;
namespace Microsoft.AspNetCore.Builder;
public static class RouteShortCircuitEndpointConventionBuilderExtensions
{
// These fields cache some common status code values
private static readonly ShortCircuitMetadata _200ShortCircuitMetadata = new(200);
private static readonly ShortCircuitMetadata _401ShortCircuitMetadata = new(401);
private static readonly ShortCircuitMetadata _404ShortCircuitMetadata = new(404);
private static readonly ShortCircuitMetadata _nullShortCircuitMetadata = new(null);
public static IEndpointConventionBuilder ShortCircuit(
this IEndpointConventionBuilder builder, int? statusCode = null)
{
var metadata = statusCode switch
{
200 => _200ShortCircuitMetadata,
401 => _401ShortCircuitMetadata,
404 => _404ShortCircuitMetadata,
null => _nullShortCircuitMetadata,
_ => new ShortCircuitMetadata(statusCode)
};
// Add the ShortCircuitMetadata instance to the endpoint
builder.Add(b => b.Metadata.Add(metadata));
return builder;
}
}
That is the whole mechanism for a single endpoint: attach a metadata marker, nothing more. MapShortCircuit() builds on top of this to handle route prefixes such as .well-known in one call, by generating a catch-all route for each prefix you pass in.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Routing;
public static class RouteShortCircuitEndpointRouteBuilderExtensions
{
// This is the no-op RequestDelegate executed for the endpoints
private static readonly RequestDelegate _shortCircuitDelegate = (context) => Task.CompletedTask;
// You can pass multiple route-prefixes to be handled and
// each one is converted to an endpoint
public static IEndpointConventionBuilder MapShortCircuit(
this IEndpointRouteBuilder builder, int statusCode, params string[] routePrefixes)
{
var group = builder.MapGroup("");
foreach (var routePrefix in routePrefixes)
{
string route = routePrefix.EndsWith("/", StringComparison.OrdinalIgnoreCase)
? $"{routePrefix}{{**catchall}}"
: $"{routePrefix}/{{**catchall}}";
// normalise the route to end with /{**catchall}
group.Map(route, _shortCircuitDelegate)
.ShortCircuit(statusCode) // Mark the request as short-circuited
.Add(endpoint =>
{
endpoint.DisplayName = $"ShortCircuit {endpoint.DisplayName}";
// make sure the route is last (it's a catch-all route)
((RouteEndpointBuilder)endpoint).Order = int.MaxValue;
});
}
return new EndpointConventionBuilder(group);
}
}
Note the Order being set to int.MaxValue. Because the mapped route is a catch-all, it has to be evaluated last, otherwise it could swallow requests meant for more specific endpoints registered later. The actual bypass happens inside the routing middleware, which checks for the ShortCircuitMetadata right after it has picked an endpoint.
var shortCircuitMetadata = endpoint.Metadata.GetMetadata<ShortCircuitMetadata>();
if (shortCircuitMetadata is not null)
{
return ExecuteShortCircuit(shortCircuitMetadata, endpoint, httpContext);
}
ExecuteShortCircuit() does a few sanity checks before it runs the endpoint. It refuses to short-circuit an endpoint that has authorization, CORS, or antiforgery metadata attached, since bypassing those silently would be a security problem rather than a performance win. After that check, it sets the status code if one was configured and invokes the endpoint’s request delegate directly.
private Task ExecuteShortCircuit(ShortCircuitMetadata shortCircuitMetadata,
Endpoint endpoint, HttpContext httpContext)
{
// This check mirrors the implementation in EndpointMiddleware
if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
{
// Trying to short circuit an endpoint with authorization,
// CORS, or antiforgery metadata throws an exception
if (endpoint.Metadata.GetMetadata<IAuthorizeData>() is not null)
ThrowCannotShortCircuitAnAuthRouteException(endpoint);
if (endpoint.Metadata.GetMetadata<ICorsMetadata>() is not null)
ThrowCannotShortCircuitACorsRouteException(endpoint);
if (endpoint.Metadata.GetMetadata<IAntiforgeryMetadata>() is { RequiresValidation: true } &&
httpContext.Request.Method is {} method &&
HttpExtensions.IsValidHttpMethodForForm(method))
ThrowCannotShortCircuitAnAntiforgeryRouteException(endpoint);
}
// Set the status code that was recorded when the endpoint was mapped
if (shortCircuitMetadata.StatusCode.HasValue)
httpContext.Response.StatusCode = shortCircuitMetadata.StatusCode.Value;
// Execute the endpoint directly
if (endpoint.RequestDelegate is not null)
return endpoint.RequestDelegate(httpContext);
// ...
}
That guard against authorization and CORS metadata is the important safety net here. It means you cannot accidentally short-circuit past an [Authorize] attribute and expose an endpoint you meant to protect; the framework throws at startup instead of silently letting the request through unauthenticated.
Where I would and would not reach for this
This is not a feature you apply broadly across a real application, and Andrew Lock is upfront about that in the original post. The clear win is on genuinely well-known, low-value paths such as favicon.ico, robots.txt, and .well-known, where you already know the response and you already know there is no security check to run. For a public-facing site fielding a constant trickle of automated bot and crawler traffic on these paths, this avoids the router walking every request through the full pipeline for no benefit.
I would be cautious about reaching for it on real business endpoints, even ones that feel simple today. An endpoint with no authorization requirement now can easily need one after the next sprint, and if it is short-circuited, adding [Authorize] later will throw at startup rather than fail silently, which is a good safety net but still an extra thing to remember when you go back and touch that endpoint. Health check and liveness probe endpoints are a reasonable middle ground, since they are usually anonymous by design and hit frequently by load balancers, but I would only short-circuit them if you have actually measured the pipeline overhead as a problem, not as a default habit.
It is also worth remembering this is purely a performance micro-optimization, not a routing feature that changes what your application can do. On a typical internal line-of-business app with modest traffic, you are unlikely to notice any difference at all. It earns its keep on high-throughput public services where shaving a few pipeline stages off frequently hit, non-sensitive endpoints adds up across millions of requests a day.
Summary
Short-circuit routing in .NET 8 lets specific endpoints execute directly inside the routing middleware instead of waiting for the endpoint middleware further down the pipeline. It is a narrow feature aimed at well-known, low-value paths like favicon.ico, robots.txt, and .well-known, or at endpoints you already know will never need authorization or CORS. The framework backs this up with a startup-time check that refuses to short-circuit any endpoint carrying authorization, CORS, or antiforgery metadata, which keeps the feature from becoming a foot-gun.
Leave a Reply