A shipment request failed somewhere after payment and before its documents were ready. The application, queue worker, and remote adapter all had logs. None of them shared an identifier.
The search began with timestamps, account details, and values which looked related. Payment was a plausible lead. So were several nearby remote calls. Some trails ended only after hours of reading because they belonged to different requests which happened at nearly the same time.
This was not a shortage of log lines. It was a broken chain of custody.
One investigation key must survive every boundary which can separate the work: HTTP ingress, application context, an outbound call, a queued message, a worker, and the response returned to the caller. If any boundary treats propagation as an optional logging detail, that boundary can split one execution into several unrelated stories.
Assignment and propagation are different contracts
The first article in this series established how to assign a distinct identifier to a request execution. That solves only the first transition:
incoming request -> active correlation identifierPropagation asks a different question:
which later records and executions still belong to that request?A good generator cannot answer it. A perfectly unique identifier which remains in middleware memory is as useless to a queue worker as an identifier which was never created.
The DBG-09 fixture models the expected path:
HTTP request
-> request context
-> request log
-> outbound request
-> queue envelope
-> worker context
-> worker log
-> HTTP responseEvery arrow is a carrier. The fixture fails if the queue envelope drops the identifier, even though the original request and both logs remain individually valid.
That failure is worth naming precisely. The logger did not lose context. The queue boundary never transported it.
Write a carrier map before adding another log
Correlation becomes reviewable when the application declares the carrier at each boundary:
| Boundary | Accept from | Validate | Carry with | Expose to |
|---|---|---|---|---|
| HTTP ingress | caller header, when trusted | format and size | request context | internal handlers |
| structured log | active context | required field present | event record | log index |
| outbound HTTP | active context | destination policy | request header | next service |
| queue dispatch | active context | serializable value | message metadata | worker |
| queue execution | message metadata | expected format | worker context | handlers and logs |
| HTTP response | active context | non-empty value | response header | caller and support |
This table is more useful than “add tracing” because it exposes ownership. If the worker log is empty, there are only a few places to inspect: dispatch capture, message serialization, worker hydration, or log enrichment.
It also prevents accidental claims. A custom correlation header can connect
records without producing a distributed trace. A distributed trace additionally
models parent-child execution and sampling. The W3C Trace Context
specification
standardizes traceparent and tracestate for
that purpose.
The retained implementation uses a validated correlation UUID rather than claiming full W3C trace participation. That is enough for its declared search contract. It should not be described as proof of spans, parentage, or end-to-end trace export.
Preserve valid context; replace untrusted context
Blindly trusting an incoming identifier lets a caller choose log cardinality, inject awkward values into search tools, or merge unrelated work under one key. Blindly replacing every identifier breaks a chain established by an upstream component.
The HTTP rule can remain small:
valid supported identifier received -> preserve it
missing or invalid identifier -> generate a new oneA retained application follows that rule for UUID correlation identifiers. It preserves a valid caller value, replaces an invalid value, stores the active identifier in request context, and returns it in the response.
The response matters. Support can ask a caller for one opaque value instead of asking for an exact timestamp, route, account, payload fragment, and timezone. It also lets a proxy verify that the identifier it sent is the one the application used.
Validation is still not authentication. A syntactically valid caller value may be safe to correlate while remaining unsafe for authorization, tenant lookup, deduplication, or billing. Correlation identifiers describe related execution; they confer no authority.
Context should remove manual logging arguments
Passing the identifier into every log statement invites omission:
Log::info('remote request started', [
'correlation_id' => $correlationId,
]);The next log call will eventually forget the field. Context makes the identifier part of the execution rather than part of each author’s memory:
Context::add('correlation_id', $correlationId);
Log::info('remote request started');
Log::info('remote response received');Current Laravel context documentation states that context added in middleware is appended to log entries. This removes repetitive call site plumbing, not the need to verify the logging pipeline. A formatter, processor, transport, or index mapping can still discard or rename the field.
The useful test is therefore not “the context API was called.” It is:
given one active identifier
when a representative structured event is emitted
then the stored event is searchable by that identifierThat test stops at the storage boundary the team actually operates. A unit test of the context facade cannot prove the field arrived in a remote log index.
A queue is a new execution, not a continuation of memory
An HTTP request and a queue worker may run in different processes, on different machines, hours apart. Process-local context cannot cross that gap by itself. The dispatcher has to dehydrate the relevant value into the message, and the worker has to hydrate it before application code and logging begin.
Laravel currently captures context when a job is dispatched and restores it while the job executes . That is a useful default. It is still a contract worth testing when jobs cross framework versions, custom serializers, other languages, or non-Laravel consumers.
The queue record should keep two identities separate:
correlation_id: the investigation chain
job_execution_id: this worker attemptRetries should preserve the first and change the second. Otherwise five worker attempts collapse into one apparent execution. The retry article owns whether those attempts should happen; this article owns whether an operator can tell them apart while still following the shared request.
The same distinction applies to business identity:
operation_id: the durable business request
correlation_id: the related investigation
execution_id: this particular attemptOne identifier cannot safely perform all three jobs. An operation may be retried by another request tomorrow. One request may create several queued operations. One queue operation may execute several times.
Outbound calls need an explicit trust boundary
Propagating an identifier to every destination is not automatically correct. Internal services may share the correlation contract. An external provider may ignore the header, reject unknown headers, reflect it, retain it under a different policy, or make it visible to another support organization.
The carrier map therefore needs a destination rule:
owned service with shared contract -> propagate supported context
external provider -> propagate only when agreed and useful
unknown destination -> do not leak internal context by defaultFor interoperable distributed tracing, use the standard header format and processing model rather than inventing a chain of proprietary headers. For a small internal correlation scheme, document the supported header, validation, replacement behavior, and whether intermediaries preserve it.
Either way, do not put customer data, account names, email addresses, or other meaning into the identifier. An opaque value is easier to propagate safely and does not become stale when business data changes.
Correlation narrows a search; it does not explain a failure
Once a shared identifier existed, the earlier partial-booking case could be read as one ordered set of records rather than several timestamp guesses:
request accepted
payment completed
remote booking completed
persistence failed
worker or cleanup decision recorded
response completedThat chain eliminates unrelated requests and shows which boundaries were crossed. It does not explain why persistence failed, prove that every missing record represents an error, or decide what cleanup should do. The timeline, failure handling, and causal explanation belong to other articles in this series.
This limitation matters because correlation can create false confidence. Ten records sharing one identifier are not necessarily complete. Sampling, buffering, exporter failure, conditional logging, and a crash before emission can all leave gaps. The absence of a record means only that the queried system did not return it.
Use correlation to reduce the candidate set. Use state, ordering, controlled reproduction, and before-and-after evidence to explain the mechanism.
Test the break, not just the happy path
The DBG-09 fixture uses one synthetic correlation value and deterministic boundary records. Its happy path proves that the request log, outbound header, queue metadata, worker log, and response header all retain the same value.
Two counterexamples make the contract sharper:
queue carrier removed
-> worker starts without the request correlation
-> verification rejects the broken chain
untrusted incoming value
-> ingress replaces it with an application-owned value
-> no record retains the rejected inputA production suite should also exercise the boundaries which exist there:
- a valid upstream identifier is preserved;
- an invalid or oversized value is replaced;
- log storage can retrieve a representative event by identifier;
- a queued job restores correlation before its first application log;
- each retry receives a distinct execution identifier;
- approved outbound clients propagate the supported format;
- unapproved destinations receive no internal correlation header; and
- the final response exposes the active value even on an error path.
The last condition is easy to miss when response middleware runs only after a successful handler. Error responses are often the ones for which the caller needs the identifier.
The fixture does not run Laravel, a queue, an HTTP server, or a log backend. The retained code proves only HTTP preservation, replacement, context storage, and response exposure. Brian’s account supplies the earlier operational pain and the use of central search; current framework documentation supplies the queue and log mechanics.
Missing correlation once turned investigations into weeks of waiting for another occurrence, followed by timestamp-based stitching and false trails. Adding more messages after the fact could help only if the failure happened again and the new messages happened to include the right fields.
The durable repair is not “log more.” List every boundary that can separate the work. Give each one an explicit carrier, validation rule, and test. A correlation identifier becomes useful only when losing it is a contract failure rather than an ordinary omission.