Laravel OpenTelemetry distributed tracing often breaks in async queues. Learn to propagate context across your message bus to maintain full request visibility.
Last month, I spent three days chasing a ghost in our production logs. A critical payment processing job was failing silently, but because the trace context died the moment the request hit our Redis queue, I couldn't link the incoming webhook to the background job that actually touched the database. We were flying blind.
If you're already using Distributed tracing for asynchronous microservices: A practical guide, you know that visibility is the difference between a five-minute fix and an all-night debugging session. In a synchronous Laravel environment, OpenTelemetry handles context propagation automatically. But once you move to dispatch() and background workers, that context evaporates.
When a Laravel request hits your controller, the traceparent header is typically captured by your instrumentation layer. However, when you dispatch a job, that context doesn't magically jump into the payload of your queue driver. It gets lost.
We first tried manually passing a correlation_id through the job constructor. It worked for basic logging, but it didn't give us a flame graph in Grafana. We were essentially reinventing the wheel without the benefit of the ecosystem's standards. To get true Distributed Tracing, you need to inject the W3C Trace Context into the job payload itself.
To make this work, we need to hook into the job serialization process. Laravel's queue system is robust, but it doesn't natively know about OTel. We can use a combination of a custom Job Middleware and the Queue::looping or Job events.
First, identify the current span context before dispatching:
PHPuse OpenTelemetry\API\Trace\Propagation\TraceContextPropagator; use OpenTelemetry\Context\Context; #6A9955">// Inside your dispatch logic $carrier = []; TraceContextPropagator::getInstance()->inject($carrier); #6A9955">// Now $carrier contains 'traceparent' $job = new ProcessPayment($payload); $job->withTraceContext($carrier); dispatch($job);
You'll need a base job class that handles this injection. If you're using Laravel Serialization: Architecting Deterministic Payloads for High-Performance Queues, make sure your custom payload serializer isn't stripping these metadata keys.
Once the worker picks up the job, we need to extract that context and start a new span that references the parent. This is where most implementations fail. If you don't start the span before the job logic executes, you lose the correlation.
I recommend using a Job middleware for this. It keeps your business logic clean and separates the observability concerns:
PHPnamespace App\Jobs\Middleware; use OpenTelemetry\API\Trace\Propagation\TraceContextPropagator; use OpenTelemetry\Context\Context; class TracePropagationMiddleware { public function handle($job, $next) { $context = TraceContextPropagator::getInstance()->extract($job->traceContext); $span = $this->tracer->spanBuilder('queue.process') ->setParent($context) ->startSpan(); $scope = $span->activate(); try { return $next($job); } finally { $span->end(); $scope->detach(); } } }
When you successfully propagate context, your traces in Kubernetes Observability: Implementing Distributed Tracing with Tempo will show a continuous line from the HTTP request, through the Redis queue, and into the worker process.
Without this, you have "orphaned spans"—trace segments that exist in your backend but aren't linked to a parent. It makes calculating P99 latencies for specific user requests across services impossible.
Be careful about what you inject into your queue payload. If you're serializing massive objects alongside your trace context, you'll see your queue latency climb. Keep the trace headers small and strictly compliant with W3C standards. In our case, we saw an overhead of roughly 1.2ms per job dispatch, which is well worth the cost for the visibility gained.
I'm still tinkering with how to handle retries. Currently, if a job fails and retries, it generates a new trace ID, which breaks the continuity. I'm exploring using the job_id as a secondary correlation identifier, but it’s a work in progress. If you’re implementing this, start by ensuring your primary trace propagation is rock solid before worrying about the edge cases of exponential backoff.
Laravel serialization often bloats your message bus. Learn to implement deterministic custom converters for high-performance, type-safe payload transmission.