CodeOath
← All posts
Architecture & Patterns55 min total · 13 parts

Middleware Pipelines Compared: ASP.NET Core, Express.js, and Django

Contents — Part 11 of 13: Async Middleware and Performance
Part 11 of 13 · ~1 min

Async Middleware and Performance

A middleware that does I/O (a database lookup, an external API call) sits directly in the critical path of every single request that passes through it — the cost of a slow or poorly-written middleware isn't confined to one route, it's multiplied across the entire application.

# BAD — a blocking network call on every single request, even ones that don't need it
class GeoLookupMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        request.country = geoip_lookup_over_network(request.META["REMOTE_ADDR"])  # slow!
        return self.get_response(request)
# BETTER — a local, in-memory database file instead of a network round-trip,
# and only doing the lookup for routes that actually use request.country
class GeoLookupMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        self.reader = geoip2.database.Reader("GeoLite2-Country.mmdb")  # loaded once, at startup

    def __call__(self, request):
        if request.path.startswith("/api/localized/"):
            request.country = self.reader.country(request.META["REMOTE_ADDR"]).country.iso_code
        return self.get_response(request)

The general principle, true across all three frameworks: expensive work in a globally-registered middleware should either be genuinely necessary for every request, or gated behind a check for the specific requests that need it — and one-time setup (opening a file, a connection pool) belongs in the constructor/startup phase, not repeated on every request.

Async middleware in ASP.NET Core specifically

app.Use(async (context, next) =>
{
    await next();  // awaiting here means this middleware doesn't block a thread while waiting
});

await next() (rather than a blocking .Wait() or .Result) matters specifically because ASP.NET Core runs on a limited thread pool — blocking a thread synchronously while waiting for an inner layer to finish wastes that thread for the entire duration, whereas await frees it to serve other requests during any I/O wait inside the pipeline. Blocking on async code in ASP.NET Core is a well-known way to cause thread-pool starvation under load.