asynchronous programming
logging
request_id
Python
software development

How to use request_id while logging in asynchronous functions?

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

In asynchronous code, several requests can be in flight at once, so plain log messages quickly become hard to follow. A request_id solves that by attaching the same identifier to every log line produced while one request is being processed.

In Python async code, the correct tool for this is usually contextvars. A plain global variable breaks as soon as concurrent tasks overlap, while a context variable flows correctly through await boundaries.

Why contextvars Matters

With asyncio, multiple coroutines share the same thread. That means thread-local storage is not enough for per-request logging in many async applications. contextvars.ContextVar stores data in the logical execution context, so each request can keep its own identifier even when other requests are running at the same time.

Start by defining a request-id context variable:

python
import contextvars

request_id_var = contextvars.ContextVar("request_id", default="-")

Now each request can set its own value without trampling another request's logs.

Add the Request ID to Log Records

A clean pattern is to use a logging filter that reads the current context variable and injects it into each log record:

python
1import contextvars
2import logging
3import uuid
4
5request_id_var = contextvars.ContextVar("request_id", default="-")
6
7class RequestIdFilter(logging.Filter):
8    def filter(self, record):
9        record.request_id = request_id_var.get()
10        return True
11
12logger = logging.getLogger("app")
13logger.setLevel(logging.INFO)
14handler = logging.StreamHandler()
15handler.setFormatter(logging.Formatter(
16    "%(asctime)s %(levelname)s [request_id=%(request_id)s] %(message)s"
17))
18handler.addFilter(RequestIdFilter())
19logger.addHandler(handler)

Every emitted log record now has a request_id field derived from the current async context.

Set the Request ID per Request

Inside your async request handler, set the context variable at the beginning and reset it at the end:

python
1import asyncio
2import uuid
3
4async def process_request(name: str):
5    token = request_id_var.set(str(uuid.uuid4()))
6    try:
7        logger.info("started request %s", name)
8        await asyncio.sleep(0.1)
9        logger.info("finished request %s", name)
10    finally:
11        request_id_var.reset(token)
12
13async def main():
14    await asyncio.gather(
15        process_request("alpha"),
16        process_request("beta")
17    )
18
19asyncio.run(main())

Because the request ID is in a context variable, each concurrent request keeps the correct identifier through await points.

Framework Integration

In web frameworks, the same idea usually belongs in middleware. The middleware reads an incoming request header such as X-Request-ID, or generates one if the client did not send it, then stores it in the context variable before the rest of the request runs.

That gives you one consistent ID across:

  • access logs
  • application logs
  • downstream service calls
  • error reports

If your service calls another service, forward the same request ID so cross-service tracing stays coherent.

Avoid Global Variables and Manual String Concatenation

A weak implementation often looks like this:

  • store the request ID in a module-level global
  • pass request_id manually into every logging call
  • prepend it into message strings by hand

That tends to break under concurrency or become repetitive. A context variable plus a logging filter keeps the logging call sites clean while still ensuring the ID is attached automatically.

Combine with Structured Logging

If you use JSON logs or structured logging libraries, inject the request ID as a field rather than embedding it only in the message string. That makes filtering and searching much easier in log aggregation systems.

The principle stays the same: bind the request ID to the async context and have the logger read it automatically.

Common Pitfalls

  • Using a global variable for the request ID in async code, which causes requests to overwrite each other.
  • Forgetting to reset the context variable after the request finishes.
  • Manually adding the request ID to some log messages but not all of them.
  • Generating a new request ID deep inside helper functions instead of once at the request boundary.

Summary

  • Use a ContextVar to store request_id in Python async code.
  • Add the current request ID to every log record with a logging filter or structured logger hook.
  • Set the request ID at the request boundary, usually in middleware or the top-level handler.
  • Reset the context when the request is done.
  • Avoid globals and manual string formatting for per-request logging state.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.