Every request that hits your app writes log lines. If that request dispatches a job, the job writes its own logs — on a different worker, at a different time, with no obvious link back to the request that spawned it. When something breaks in production, stitching those two streams together by eye is miserable. Laravel Context fixes it: you attach a request ID (or tenant, or user) once, and it rides along on every log entry and every queued job without you touching another Log:: call.
Set Laravel Context in middleware#
Context lives in the Illuminate\Support\Facades\Context facade. Call Context::add() to store a key, and Laravel appends it as metadata to every log entry written for the rest of that request. The right place to set it is middleware, so it covers the entire request lifecycle before any of your code runs.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Context;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class AddRequestContext
{
public function handle(Request $request, Closure $next): Response
{
// A per-request trace ID every downstream log line will carry.
Context::add('request_id', (string) Str::uuid());
Context::add('url', $request->fullUrl());
return $next($request);
}
}
Register it in bootstrap/app.php — remember Laravel 12 and 13 have no Http/Kernel.php, middleware is wired up here:
->withMiddleware(function (Middleware $middleware) {
$middleware->prependToGroup('web', \App\Http\Middleware\AddRequestContext::class);
})
Now any log call downstream carries the context automatically. Write a plain log entry:
Log::info('User authenticated.', ['auth_id' => Auth::id()]);
And the request-scoped metadata is appended alongside the values you passed in:
User authenticated. {"auth_id":27} {"request_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697","url":"https://example.com/login"}
That request_id is what makes this worth doing. Tailing your logs locally, Laravel Pail surfaces that metadata inline so you can watch a single request flow through. And once you are centralizing logs in Grafana Loki, you filter on one request_id and get every line from that request — across web and queue — in one query.
Let Laravel Context ride along to queued jobs#
This is where Context earns its keep. When you dispatch a job, Laravel captures ("dehydrates") the current context onto the job payload, then rehydrates it into the worker's context before handle() runs. You dispatch normally:
// In a controller, mid-request — request_id is already in context.
ProcessPodcast::dispatch($podcast);
The job does nothing special. It just logs:
class ProcessPodcast implements ShouldQueue
{
use Queueable;
public function __construct(public Podcast $podcast) {}
public function handle(): void
{
Log::info('Processing podcast.', ['podcast_id' => $this->podcast->id]);
}
}
And the worker's log line carries the same request_id as the web request that dispatched it:
Processing podcast. {"podcast_id":95} {"request_id":"e04e1a11-e75c-4db3-b5b5-cfef4ef56697","url":"https://example.com/login"}
No wiring, no constructor argument threaded through by hand. The context travels through chained and batched jobs the same way, so a whole pipeline dispatched from one request shares one trace ID. If you are already correlating spans in a full observability stack with Pulse and OpenTelemetry, that same ID is the natural correlation key to hang traces off.
Hidden context and temporary scopes#
Sometimes you want data available to your code but kept out of the logs — an internal tenant key, a feature-flag bucket, a value you would rather not leak into a log aggregator. Use addHidden():
Context::addHidden('tenant_key', $tenant->internal_key);
Context::getHidden('tenant_key'); // 'acme-internal-42'
Context::get('tenant_key'); // null — never written to logs
Hidden context still travels to queued jobs; it is only excluded from log output, not from dehydration. That is exactly why the framework's own locale-on-queue pattern uses it, restoring app.locale inside a Context::hydrated() callback so queued notifications render in the right language.
For context that should only exist for part of a request, scope() merges extra data for the duration of a callback and restores the original state afterwards:
Context::add('trace_id', 'abc-999');
Context::scope(
function () {
Context::add('action', 'adding_friend');
// Logs in here carry trace_id AND action.
},
data: ['user_name' => 'taylor'],
hidden: ['user_id' => 987],
);
// Outside the closure, action / user_name / user_id are gone;
// trace_id remains.
Context::all(); // ['trace_id' => 'abc-999']
Gotchas and edge cases#
Context is captured at dispatch time. Anything you add after Job::dispatch() will not reach that job — the payload snapshot is already taken. Set your request-scoped values in middleware, early, before you queue anything.
Whatever you store has to survive serialization onto the job payload. Scalars, arrays, and Eloquent models are fine; closures, PDO handles, and open resources are not. Keep context small and serializable — it is copied onto every job you dispatch.
add() silently overwrites an existing key. If two middleware both set request_id, the second wins without warning. When you only want to set a value if it is not already present, use addIf() instead — this is the usual fix for context that a parent request may or may not have populated already.
Do not call the Context facade inside dehydrating() or hydrated() callbacks. Those callbacks receive an Illuminate\Log\Context\Repository instance — mutate that, because touching the facade there changes the wrong process's context.
Wrapping Up#
Add a middleware that drops a request_id into Context, and you get end-to-end tracing across web and queue for almost no code. It pays off fastest when the logs land somewhere you can query them — so pair it with centralized logging in Grafana Loki and, if you are running queues at any real volume, Horizon for queue monitoring so a slow job and its originating request are two clicks apart.
FAQ#
What is the Context facade in Laravel?
Context is a facade (Illuminate\Support\Facades\Context) that captures, stores, and shares key/value metadata across a request, its queued jobs, and Artisan commands. Anything you put in it is automatically appended to every log entry written afterwards, which makes it the standard way to attach request-scoped trace data without passing it into each log call by hand.
How do I add a request ID to every Laravel log?
Add a middleware that calls Context::add('request_id', (string) Str::uuid()) and register it early in your web (and api) middleware group. From that point on, every Log:: call in the request — and in any job it dispatches — includes the request_id as metadata. Because it is set once in middleware, you never touch your individual logging statements.
Does Laravel Context get passed to queued jobs?
Yes, automatically. When a job is dispatched, the current context is "dehydrated" and stored on the job payload, then "hydrated" back into the worker's context before the job's handle() method runs. The request and every job it spawns therefore share the same context values, which is what lets a single trace ID follow a flow across the HTTP/queue boundary.
What is hidden context in Laravel?
Hidden context is data you store with addHidden() (and read with getHidden()) that is available to your code but never written to log entries. It still travels to queued jobs like normal context — it is only excluded from log output. Use it for values you need at runtime but do not want leaking into your logs, such as internal tenant keys or a locale you restore on the queue.
What is the difference between Context::add and Context::addIf?
Context::add() sets a key and overwrites any existing value for that key. Context::addIf() only sets the key if it is not already present, leaving an existing value untouched. Reach for addIf() when a value might already have been populated upstream — for example a request_id that an outer middleware or a parent process may have set — and you do not want to clobber it.