Some Laravel applications split database traffic between a write server and one or more read replicas. With the sticky option turned on, a write sends the reads that follow it to the write server, so a request can read the data it just saved. In a web request, that lasts until the response is sent. In a queue worker, it can last until the worker restarts.
Laravel 13.36 adds an opt-in flag that resets the sticky state before each queue job.
How Sticky Connections Work
A read/write connection in config/database.php looks like this:
'mysql' => [ 'driver' => 'mysql', 'read' => [ 'host' => ['10.0.0.11', '10.0.0.12'], ], 'write' => [ 'host' => ['10.0.0.10'], ], 'sticky' => true, 'database' => env('DB_DATABASE', 'laravel'), // ...],
The connection keeps a flag that records whether anything has been written. An insert, or an update or delete that changes at least one row, sets the flag. Once it is set and sticky is true, every select on that connection uses the write server instead of a replica. The flag stays set until something calls forgetRecordModificationState() on the connection. Octane already does this between requests.
The Problem in Queue Workers
A queue worker started with queue:work is one PHP process that runs many jobs, and it keeps the same database connection object between them. Between jobs, the worker already resets some state, such as shared log context and scoped container instances. Before this release, it did not reset the sticky flag.
Suppose a worker processes a job that marks an order as shipped, followed by 10,000 jobs that only read data. Every one of those 10,000 jobs reads from the write server instead of a replica. The worker only drops the flag when it exits, after it reaches --max-jobs, --max-time, or its memory limit, or when you run queue:restart during a deploy.
Worker::$flushStickyConnections
Jack Bayliss added a static property on the worker to turn the reset on:
// app/Providers/AppServiceProvider.phpuse Illuminate\Queue\Worker; public function boot(): void{ Worker::$flushStickyConnections = true;}
With the flag on, the worker calls forgetRecordModificationState() on every open database connection before it takes the next job from the queue. Each job starts with reads going to the replicas. Inside a job, the sticky option works as before, so a job that writes a row and then reads it back reads from the write server.
The flag is off by default in Laravel 13. A job could depend on reading data that an earlier job in the same worker wrote, before that data reaches the replicas. Jobs that depend on each other in that way should be a chain or a batch, and a chained job is not guaranteed to run in the same worker anyway. Still, turning the flag on can change which server a job reads from, so check jobs that read data right after another job writes it.
The Database Queue Driver
If you use the database queue driver, check which connection it uses. To reserve a job, the driver runs an update on the jobs table. If the jobs table is on the same connection as your application data, that update sets the sticky flag again, after the reset and before the job runs. Every job then reads from the write server, with or without the new flag.
The database queue connection reads its database connection name from DB_QUEUE_CONNECTION, and uses the default connection when it is empty:
// config/queue.php'database' => [ 'driver' => 'database', 'connection' => env('DB_QUEUE_CONNECTION'), 'table' => env('DB_QUEUE_TABLE', 'jobs'), // ...],
Point it at a second connection in config/database.php, such as a queue connection that uses the write server only:
DB_QUEUE_CONNECTION=queue
Laravel tracks the sticky flag per connection, so writes to the jobs table no longer affect where your jobs read from. The Redis and SQS drivers do not write to your database, so this only applies to the database driver.
Resetting the Flag Yourself
You can call forgetRecordModificationState() from other long-running code too. A custom Artisan command that loops forever has the same problem as a queue worker:
use Illuminate\Support\Facades\DB; while (true) { DB::forgetRecordModificationState(); $this->processNextBatch(); sleep(5);}
The DB::hasModifiedRecords() method returns the current value of the flag, which is useful in a test that checks a job sent its reads to the replicas.
Further Reading
- Rule Builders for Every Validation Rule in Laravel 13.36
- Laravel Jobs and Queue 101: How to run your workers on your production server
- Laravel queue:work Now Prints Why the Worker Stopped
Jack Bayliss contributed Worker::$flushStickyConnections in #61915.