Short-Circuiting and Terminal Middleware
End requests early with ShortCircuit, MapShortCircuit and terminal middleware, and know which middleware and policies those requests then skip.
app.MapShortCircuit(404, "robots.txt", "favicon.ico");
app.MapGet("/health/live", () => "ok").ShortCircuit();Short-circuiting means producing a response without running the rest of the pipeline. It saves work for requests that need none of it, such as crawler hits on /robots.txt, browsers asking for /favicon.ico or load balancer probes. ASP.NET Core offers three ways to do it, and each skips a different amount.
The three tools
| Tool | What runs | What is skipped |
|---|---|---|
Middleware that does not call next | Everything registered before it | Everything after it, including the endpoint |
.ShortCircuit() on an endpoint | Middleware before UseRouting, then the handler, executed inside the routing middleware | All middleware between routing and endpoints: auth, CORS, rate limiting, your own |
app.MapShortCircuit(404, "robots.txt", "favicon.ico") | Same as above | Same, and no handler at all: it writes the status code for any path under those prefixes |
ShortCircuit(int? statusCode) optionally sets the status before the handler runs. Remember where WebApplication puts routing: if you never call UseRouting, it runs first, so a short-circuited endpoint skips all of your middleware, including UseExceptionHandler and request logging. Call UseRouting explicitly after the middleware that must still run.
The safety check
Because a short-circuited endpoint never reaches the authorization or CORS middleware, the routing middleware refuses to execute one that carries authorization, CORS or antiforgery metadata. A request to it throws InvalidOperationException: Endpoint ... contains authorization metadata, but this endpoint is marked with short circuit and the client gets 500. That is deliberate: the alternative would be serving a protected endpoint to anonymous callers.
Returning early from your own middleware
Write the status (and a body if useful) and return without calling next. Typical cases: a maintenance switch returning 503 with Retry-After, rejecting requests without a required tenant header, or answering a webhook signature failure with 401. Set the response before writing the body, and do not call next after you have started the response.
When not to
- Anything that needs authentication, authorization, CORS or rate limiting.
- Readiness probes that should run the same health check pipeline as real traffic; short-circuit liveness, not readiness, if anything.
- Requests you want counted in your own request logs or metrics middleware: they disappear from those numbers.
- Do not reach for it to save microseconds on ordinary API calls; the gain only matters for high-volume junk traffic.
The usual team decision: MapShortCircuit(404, "robots.txt", "favicon.ico", "apple-touch-icon.png") on pure APIs, and ShortCircuit() on an anonymous liveness endpoint, nothing else.
Example
When to use it
- Answer /robots.txt and /favicon.ico with 404 on a pure JSON API so crawler noise skips authentication and request logging.
- Return 503 with Retry-After: 120 for /orders while a maintenance flag is on, without calling the rest of the pipeline.
- Serve an anonymous /health/live probe with ShortCircuit() so a slow downstream middleware cannot fail liveness.
More examples
Pitfall: short-circuiting a protected endpoint
The routing middleware checks short-circuited endpoints for authorization, CORS and antiforgery metadata and throws rather than silently skipping those checks. The error appears per request, not at start-up, so a test that calls each protected route is what catches it before deployment.
using Microsoft.AspNetCore.Authentication.Cookies;
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
builder.Services.AddAuthorization();
var app = builder.Build();
// Wrong: authorization middleware never sees this endpoint, so routing refuses to run it
// and every call returns 500 with "contains authorization metadata, but this endpoint
// is marked with short circuit".
// app.MapGet("/admin/metrics", () => "...").RequireAuthorization().ShortCircuit();
// Right: protected endpoints go through the full pipeline.
app.MapGet("/admin/metrics", () => TypedResults.Ok(new { openOrders = 42 }))
.RequireAuthorization();
// Only anonymous, side-effect-free endpoints are candidates.
app.MapGet("/health/live", () => TypedResults.Ok("alive")).ShortCircuit();
app.Run();Keeping the exception handler in front of short-circuited endpoints
Without the explicit <code>UseRouting</code>, WebApplication would add routing at the start of the pipeline and a short-circuited endpoint would bypass even <code>UseExceptionHandler</code>, turning a thrown exception into a bare 500. Placing routing yourself decides exactly which middleware short-circuited requests still pass through.
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddProblemDetails();
var app = builder.Build();
// These run before routing, so they also apply to short-circuited endpoints.
app.UseExceptionHandler();
app.UseHttpsRedirection();
// Explicit routing: endpoints marked ShortCircuit execute here.
app.UseRouting();
// Skipped by short-circuited endpoints.
app.Use(async (context, next) =>
{
context.Response.Headers["X-Api-Version"] = "2026-10";
await next(context);
});
app.MapGet("/robots.txt", () => Results.Text("User-agent: *\nDisallow: /", "text/plain"))
.ShortCircuit();
app.MapGet("/products", () => TypedResults.Ok(new[] { "KET-0042" }));
app.Run();Requests and expected responses
An <code>.http</code> file documents what each path should return and doubles as a manual check after deployment. <code>MapShortCircuit</code> matches the listed prefixes and anything under them, so <code>/robots.txt</code> and <code>/favicon.ico</code> get a 404 without allocating a handler or touching authentication.
### Crawler noise: answered by MapShortCircuit, never logged, never authenticated
GET http://localhost:8080/robots.txt
# 404, empty body
GET http://localhost:8080/favicon.ico
# 404, empty body
### Liveness probe: ShortCircuit endpoint, cheap and anonymous
GET http://localhost:8080/health/live
# 200 alive
### Normal API call: full pipeline
GET http://localhost:8080/orders/1001
Authorization: Bearer <token-from-identity-provider>
# 200 {"id":1001,"status":"Paid"}
Discussion