In Laravel Scout, what changes when SCOUT_QUEUE is true, and why would you also turn on the after_commit option?
answer
- sync HTTP call vs queued job
- MakeSearchable and RemoveFromSearch jobs
- SerializesModels re-fetches the row
- after_commit defaults to false
- rollback or not-yet-committed rows
basics
~10 sWith SCOUT_QUEUE true, Scout dispatches MakeSearchable and RemoveFromSearch jobs instead of calling the engine during the request. after_commit delays syncing until open transactions commit, so rolled-back or uncommitted rows never reach the index.
solid answer
~40 sBy default `queue` is `env('SCOUT_QUEUE', false)`, so every save makes a synchronous call to the engine inside the request, adding latency and turning an engine outage into a failed save. With it on, the observer dispatches `MakeSearchable` or `RemoveFromSearch` on `scout.queue.connection` (or the app's default connection) and `scout.queue.queue`. The jobs use `SerializesModels`, so they store only keys and the worker re-fetches the rows, indexing the database state at run time. The trap is transactions: the `saved` event fires inside `DB::transaction`, so a job can run before the commit and find nothing, or a synchronous sync can push data that later rolls back. `'after_commit' => true` makes Scout's observer wait for the commit and skip rolled-back work.
code
php · 11 lines<?php
// config/scout.php (excerpt)
return [
'driver' => env('SCOUT_DRIVER', 'collection'),
'queue' => [
'connection' => 'redis',
'queue' => 'scout',
],
'after_commit' => true,
];go deeper
Recall that SCOUT_QUEUE moves index updates off the web request into queued jobs, and that a worker must run for them to happen.
Explain which jobs Scout dispatches, how the queue connection and name are chosen, and why SerializesModels means the worker indexes the row as it is when the job runs.
Diagnose the transaction race: jobs running before commit, rolled-back data in the index, and why after_commit fixes both. Mention asynchronous engines and failed-job monitoring.
Decide how much index lag the product tolerates, whether search sync gets its own queue and worker capacity, and how drift is detected and reconciled after outages.
## Synchronous by default Scout's `config/scout.php` sets `'queue' => env('SCOUT_QUEUE', false)`. With the default, the observer calls the engine directly on every `saved` or `deleted` event, inside the web request. For the `database` and `collection` engines that costs nothing, because they keep no index. For Algolia, Meilisearch or Typesense it means: - an HTTP round trip added to every save of a recipe; - an engine timeout or outage surfacing as an exception from `save()`; - bulk writes, such as an import of 5,000 recipes, running at the speed of the search API. The docs recommend a queue for any third-party engine. ## What SCOUT_QUEUE=true changes With the option on, `queueMakeSearchable()` and `queueRemoveFromSearch()` dispatch jobs instead: | Operation | Job class | Payload | |---|---|---| | upsert | `Laravel\Scout\Jobs\MakeSearchable` | the model keys, via `SerializesModels` | | remove | `Laravel\Scout\Jobs\RemoveFromSearch` | the keys only, rebuilt without a query | The connection is `scout.queue.connection` if you set it, otherwise the app's default queue connection; the queue name is `scout.queue.queue`. To use them, write `queue` as an array such as `['connection' => 'redis', 'queue' => 'scout']` and run a worker on that queue. Two consequences follow from `SerializesModels`: 1. **The worker indexes the current row**, not a snapshot from save time. Three quick edits may produce three jobs that each send the latest state. 2. **Missing rows are dropped silently.** When the worker reloads a collection, keys that no longer match a row are filtered out, and the job returns early if nothing is left. In write-heavy apps you can swap in `MakeSearchableUniquely` and `RemoveFromSearchUniquely` with `Scout::makeSearchableUsing()` and `Scout::removeFromSearchUsing()`, which use unique-job locks to avoid queueing duplicates. ## The transaction problem Eloquent fires `saved` as soon as the `INSERT` or `UPDATE` runs, even inside `DB::transaction()`. Scout reacts at that moment, before the commit: - **Queued:** the job may be picked up before the transaction commits. The worker's connection cannot see the uncommitted row, so a new recipe is dropped from the job and never indexed, or an update indexes the old values. - **Synchronous:** the document is pushed immediately. If the transaction then rolls back, the index holds a recipe that does not exist. ## What after_commit does Setting `'after_commit' => true` in `config/scout.php` sets the public `$afterCommit` property on Scout's `ModelObserver`. Laravel defers observers marked this way until every open transaction has committed, and discards the call if the transaction rolls back. With it on: 1. a recipe saved inside a transaction is only synced after commit; 2. a rolled-back recipe never reaches the index or the queue; 3. outside a transaction, behaviour is unchanged. The default is `false`, so apps that wrap writes in transactions should switch it on. ## What the queue does not fix Queueing moves the work, it does not make the index instantly consistent. Algolia and Meilisearch also apply writes asynchronously on their side, so there is always a short window where search lags the database. Failed jobs need monitoring like any other job, and a long outage of the engine can leave a backlog that you reconcile with `scout:import`. ## A checklist for queued Scout in production 1. Set `SCOUT_QUEUE=true` and, if search traffic should not compete with other jobs, give it its own queue in the `queue` array. 2. Set `'after_commit' => true` if any write path to a searchable model uses transactions. 3. Run and supervise a worker for that connection and queue. 4. Watch the failed-jobs table for `MakeSearchable` failures, which usually mean the engine rejected a document or was unreachable. 5. Keep `scout:import` in the runbook for reconciling after an engine outage or a lost backlog. Interviewers often ask why the jobs carry keys rather than documents. Carrying keys keeps payloads small and means a job that runs late still indexes fresh data, at the cost of one database read per batch in the worker.
- With Laravel Scout queued, a recipe is edited three times in ten seconds. What reaches the index?Each save dispatches a `MakeSearchable` job that holds only the recipe's key. Each worker run reloads the row, so every job sends the latest state; the index ends up correct, just updated more than once. `MakeSearchableUniquely` can cut the duplicate jobs while one is still queued.
- Does turning on after_commit in Laravel Scout change anything for writes made outside a transaction?No. An observer marked to run after commit runs immediately when no transaction is open. The option only matters for saves and deletes inside `DB::transaction()` or a manual `beginTransaction()`, where it delays the sync until commit and drops it on rollback.
saying these in an interview costs you the question
- SCOUT_QUEUE is on by default in the published config.
- The queued job carries the attribute values as they were at save time.
- Queueing Scout makes the search index consistent with the database immediately.
- Scout's model observer always waits for the transaction to commit.
- A job for a row that was rolled back throws ModelNotFoundException.