
Jan 28, 20266 min read
Category:.NET
Correlation IDs in .NET: trace one request across services and jobs
A production playbook for a single correlation ID contract in .NET so requests and jobs can be traced end-to-end across boundaries.
Download available. Jump to the shipped asset.
The incident is never "logging is bad". The incident is: a customer reports a duplicate charge, a job runs twice, or requests time out, and you cannot answer the basic question: what happened to this specific request.
On-call ends up guessing. Someone restarts IIS, someone retries the job manually, and the system returns to normal until the next repeat. The root cause is not always in code. Sometimes it is in the absence of a traceable narrative.
This post gives you a production playbook for correlation IDs in .NET: one contract, consistent propagation across HTTP and jobs, and log fields that turn a repeat incident into one query.
Rescuing an .NET service in production? Start at the .NET Production Rescue hub and the .NET category.
- Define a correlation ID contract (header name, format, propagation rules).
- Log it everywhere: request start, dependency calls, job starts, and job progress.
- Propagate through boundaries (HttpClient, queue messages) instead of hoping scopes survive.
The mechanism: incidents repeat when you cannot join events
In a stable system, you can answer:
- Which request started this workflow?
- Which downstream calls did it make?
- Which job run processed it?
- Where did it stall, retry, or duplicate?
Without a correlation ID, your logs are a pile of facts with no edges. You see 500s and timeouts, but you cannot group them into one narrative. In legacy systems, this is why "restart fixed it" becomes the dominant incident response.
Correlation IDs are not about pretty dashboards. They are about making the next incident smaller.
Diagnosis ladder (fast checks first)
- Pick one incident and ask: can we reconstruct the request path?
If you cannot list the inbound request, the dependency calls, and the job run that handled it, you do not have end-to-end correlation.
- Check whether you have multiple competing IDs.
Common failure:
- a reverse proxy adds
X-Request-Id - an API gateway adds
X-Correlation-Id - your app generates its own
RequestId
Now every team is "correlating" but nothing joins.
- Check boundary crossings.
Correlation often exists inside one process (scoped logging), but dies at the boundaries:
- HttpClient calls do not copy headers
- queue messages do not carry correlation properties
- background jobs generate new IDs with no parent
If correlation dies at the boundary, your incident narrative will die there too.
Fix plan: one contract, propagated everywhere
The goal is consistency, not perfection.
1) Define the contract
Pick a single header name and use it everywhere.
- Header:
X-Correlation-Id - Format: opaque string (GUID is fine; ULID is better for sortability)
- Rule: if inbound request has it, keep it. If not, generate it.
Then make the rule explicit: this header is copied to every dependency call and included on every job message.
2) Add inbound middleware (or an equivalent entry hook)
Every request needs to end up with:
- a correlation ID
- a logging scope that includes it
- a response header so callers can report it
In ASP.NET Core, middleware is clean. In classic ASP.NET, this is usually an HttpModule or early pipeline hook.
The important part is not the framework. The important part is that the ID is created once and becomes part of the request context.
3) Propagate through HttpClient
HttpClient does not automatically copy your correlation ID. If you do not add a handler, your downstream logs will be unjoinable.
The safe approach:
- set the header on outbound requests
- do not overwrite an existing header (respect caller provided IDs)
- log dependency calls with correlation ID and route
4) Propagate through queues and jobs
Jobs are where correlation breaks most often.
- attach correlation ID to message headers/properties
- include it in the job payload if headers are not supported
- set a logging scope at job start
- log progress events with the same ID
If jobs do not log progress with correlation, you will still end up with "job stuck" incidents that cannot be explained.
What to log so one request becomes one query
Log fields are the join keys for incident narratives. At minimum:
correlation_idoperation(normalized route or job name)component(api,worker,scheduler)dependency(host/vendor)duration_msoutcome(success,timeout,429,exception,cancelled)attempt(if retries exist)
Example request start log:
{
"event": "request.start",
"operation": "POST /orders",
"correlation_id": "01H...",
"component": "api",
"remote_ip": "..."
}Example dependency log:
{
"event": "dependency.call",
"dependency": "vendor-x",
"operation": "GET /v2/orders",
"correlation_id": "01H...",
"duration_ms": 842,
"outcome": "429"
}Example job start log:
{
"event": "job.start",
"job": "nightly-export",
"correlation_id": "01H...",
"component": "worker"
}Shipped asset
Correlation ID package (HTTP + jobs)
A clear correlation ID contract plus copy/paste middleware and HttpClient handler so correlation survives boundaries.
Use this when incidents repeat because you cannot join logs across services, queues, and jobs.
What you get (4 files)
correlation-id-contract.mdAspNetCoreCorrelationIdMiddleware.csCorrelationIdDelegatingHandler.csREADME.md
How to use
- On call: add correlation ID to the incident report and use it to pull the full request path.
- Tech lead: standardize the contract and propagate it through HttpClient and job boundaries.
- CTO: reduce repeated firefighting by making every incident diagnosable.
Resources
Internal:
- .NET Production Rescue
- Axiom waitlist
- Contact
- Why your background jobs hang forever (and no one notices)
External:
FAQ
Not exactly. A correlation ID is an application level join key that you control. A trace ID is part of distributed tracing and usually comes from W3C trace context (traceparent). In practice you can log both: keep an explicit correlation_id for humans and incident reports, and log trace_id when you have tracing.
Pick one as the contract and propagate it. If your proxy ID is stable and available everywhere, you can adopt it. The failure mode is keeping both and letting services pick different ones. Standardize the join key, then log the proxy request ID separately if you still want it.
No. Correlation IDs are useful even with plain logs. OpenTelemetry helps when you want spans, timing, and sampling across services. The correlation ID contract is still valuable because it works across jobs and message boundaries where traces are often incomplete.
Because jobs cross more boundaries and run outside a request scope. A request scope can carry context automatically, but a job needs explicit propagation through message metadata and explicit logging scope creation at job start. If you do not do that, job logs become isolated facts.
Pick one API and one worker. Add inbound correlation, outbound HttpClient propagation, and job start scopes. Then validate that one request produces a connected chain of logs across components. Once that works, roll it out to the rest of the estate.
Coming soon
Axiom is where we keep consistent operational defaults for production systems: logging schemas, incident runbooks, and safe reliability patterns you can reuse.
Axiom (Coming Soon)
Get notified when we ship real operational assets (runbooks, templates, schemas), not generic tutorials.
Key takeaways
- Correlation IDs are a join key for incident narratives.
- Pick one contract, propagate it through boundaries, and log it consistently.
- The value is measured in faster incident resolution and fewer repeat incidents, not in prettier dashboards.
Related posts

Handling 429s and Retry-After correctly in HttpClient
A production playbook for honoring Retry-After and stopping retry amplification when a dependency throttles your .NET service.

Why your background jobs hang forever (and no one notices)
Queues and scheduled jobs fail quietly: missing timeouts, missing heartbeats, and retries that hide failure. A practical runbook-style playbook for .NET systems.

The real cost of retry logic: when “resilience” makes outages worse
Retry storms don’t look like a bug — they look like good engineering until production melts. Here’s how to bound retries with stop rules and proof.