An editor changes a title, fixes the summary, and adjusts a category. Sending three complete search updates during that short burst does unnecessary work. A useful design is to save every edit immediately, then refresh the derived search document once the editor pauses. Laravel 13 now documents a dedicated queue API for that pattern.
The Laravel queue documentation describes DebounceFor, a job attribute that lets a newer dispatch supersede an earlier one with the same debounce identity. The example below applies that API to an article index.
This example assumes your application already has an Article model and a RefreshArticleSearchDocument action. The action should build the search document from the current database record and update the corresponding document in your search provider.
namespace App\Jobs;
use App\Actions\RefreshArticleSearchDocument;
use App\Models\Article;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\DebounceFor;
#[DebounceFor(30, maxWait: 120)]
final class RefreshArticleSearch implements ShouldQueue
{
use Queueable;
public function __construct(public readonly int $articleId) {}
public function debounceId(): string
{
return 'article:'.$this->articleId;
}
public function handle(RefreshArticleSearchDocument $refresh): void
{
$refresh->handle(Article::query()->findOrFail($this->articleId));
}
}The attribute sets a 30-second debounce window and caps repeated postponement at 120 seconds. Use a central cache when dispatching from multiple servers, and do not combine this attribute with ShouldBeUnique. Those are requirements of the documented queue mechanism.
For this design, an article ID is enough information to enqueue. Loading the record when the job runs makes the index reflect the saved article at execution time. Passing an earlier title in the constructor would make it easier to accidentally write an obsolete document.
Decide explicitly how your action handles publication changes. If the record is now a draft, its search document should be removed. If articles can be permanently deleted, define a separate removal workflow or adapt the refresh action to accept an ID and handle a missing record. The illustrative job above deliberately fails for a missing article so that this decision cannot pass unnoticed.
Place the dispatch after the successful save and database commit. An indexer should never advertise a title from a transaction that later rolls back. Keep the actual database write responsive; the queue owns only the derived search work.
Here is the boundary inside an application action. Assume a custom request has already validated the title and the controller has authorized updating the article through its policy. The action receives a trusted string; neither validation nor authorization belongs to the delayed indexer.
namespace App\Actions;
use App\Jobs\RefreshArticleSearch;
use App\Models\Article;
use Illuminate\Support\Facades\DB;
final class UpdateArticleTitle
{
public function handle(Article $article, string $title): void
{
DB::transaction(function () use ($article, $title): void {
$article->title = $title;
$article->save();
RefreshArticleSearch::dispatch((int) $article->getKey())
->onQueue('search')
->afterCommit();
});
}
}Adapt the ID type if your application uses UUIDs. Also choose one dispatch owner: if a model observer already requests search refreshes, adding this action without changing that observer creates duplicate requests. Debouncing may conceal that duplication in a manual demonstration while leaving confusing application behavior.
Suppose the editor saves at second zero, second eight, and second twenty. With a 30-second window, the quiet period following the final save ends around second fifty. This is when the surviving work can become eligible; queue congestion and provider response time can make the visible search update later. Present an honest pending state if editors need to know whether a change is searchable.
Now consider an automated importer changing the same article continuously. The maximum wait prevents the debounce window from postponing work indefinitely. It does not promise a provider response within two minutes, nor does it cancel a network request that has already started. Choose your values from the product’s tolerance for stale search results and measure actual end-to-end delay.
Loading current state avoids a stale constructor payload, but it does not solve every race. One worker could read revision A, pause during a slow provider call, and finish after a different worker has written revision B. For a frequently edited record, review provider-supported document versions or serialize writes for that record. Treat this as a separate concurrency requirement when designing the refresh action.
Make the provider operation repeatable. Updating a document keyed by article ID should leave the same document after a retry, rather than append a second search result. Exercise a timeout after the provider accepted the request, because a failed response does not necessarily mean the remote write failed. Log the article ID and revision so an unexpected result can be traced without storing the entire article in logs.
php artisan queue:work --queue=searchUse an asynchronous connection configured for your environment and a cache shared by every dispatcher. The command above listens to the named search queue on the default connection. A worker listening only to a different queue will never refresh these documents, even though the application successfully dispatches them. Check worker supervision, failure reporting, and deployment restarts as part of the integration.
Search refreshes, preview generation, and recalculated summaries are good candidates when the latest saved state is what matters. A purchase ledger needs every transaction recorded. If discarding an intermediate event changes the business outcome, choose a different job design.
A debounce ID also deserves a domain review. In a multitenant application where identifiers are only unique within a tenant, include that tenant in the identity. Two customers editing article 17 must never suppress each other’s work.
For each test burst, record the last save time, the time indexing begins, and the time the updated document becomes searchable. Compare the number of provider writes with the number of edits, but also check how long readers see old content. An optimization that reduces writes while leaving editors waiting longer than the product allows needs a different window or more worker capacity. Keep the sample size visible in the report; one short burst is a functional check, not a throughput benchmark.
Dispatch repeated edits for one article, then confirm its final search document matches the last saved state.
Edit two articles in the same window and confirm both refresh independently.
Keep dispatching beyond the maximum wait and observe that work still becomes eligible to run.
Exercise draft, deletion, provider failure, and retry behavior using the same queue and cache types as production.
A queue fake can show that application code dispatched a job. It cannot establish that a running worker, shared cache, and search provider produce the intended final result. Record both the input burst and the final indexed document when checking this feature. That evidence tells you whether the optimization preserved correctness.