Skip to content

Time web requests

Problem: You want wall and CPU time per HTTP request so you can log latency or export metrics without ad-hoc timers in every view.

Idea: Run the request (or the ASGI/WSGI pipeline) inside a Timer and handle the Measurement in on_end (logging, metrics, etc.). TimeRun stays dependency-free; framework imports live only in your app.

import logging
from starlette.middleware.base import BaseHTTPMiddleware
from timerun import Timer

logger = logging.getLogger(__name__)


class RequestTimerMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        async with Timer(
            on_end=lambda m: logger.info(
                "request",
                extra={
                    "path": str(request.url.path),
                    "wall_s": m.wall_time.timedelta.total_seconds(),
                    "cpu_s": m.cpu_time.timedelta.total_seconds(),
                },
            ),
        ):
            return await call_next(request)


# app.add_middleware(RequestTimerMiddleware)
import logging
from flask import Flask, g
from timerun import Timer

logger = logging.getLogger(__name__)
app = Flask(__name__)


@app.before_request
def _timer_enter():
    g._timer = Timer()
    g._measurement = g._timer.__enter__()


@app.teardown_request
def _timer_exit(exc):
    if not hasattr(g, "_timer"):
        return
    g._timer.__exit__(
        type(exc) if exc else None,
        exc,
        exc.__traceback__ if exc else None
    )
    m = g._measurement
    logger.info(
        "request",
        extra={
            "wall_s": m.wall_time.timedelta.total_seconds(),
            "cpu_s": m.cpu_time.timedelta.total_seconds(),
        },
    )
import logging
from timerun import Timer

logger = logging.getLogger(__name__)


class RequestTimerMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        def log_request(m):
            logger.info(
                "request",
                extra={
                    "path": request.path,
                    "wall_s": m.wall_time.timedelta.total_seconds(),
                    "cpu_s": m.cpu_time.timedelta.total_seconds(),
                },
            )

        with Timer(on_end=log_request):
            return self.get_response(request)

Add the class to MIDDLEWARE. Using with Timer() ensures on_end runs even when the view raises.

For richer context (request id, user), combine with Use metadata effectively and on_start.

See also: Share results for logs, Prometheus, and OpenTelemetry.