Deferred Tasks
defer() runs a piece of work after the response has been delivered. The visitor gets their page as soon as your controller returns; sending the email, calling the slow API or rebuilding the cache happens afterwards, without making anyone wait.
public function register(Request $request): Response
{
$user = $this->users->create($request);
// Runs after the response is sent. The visitor does not wait for it.
defer(function () use ($user): void {
$this->welcomeEmail->sendTo($user);
});
return $this->view('auth/registered.twig');
}
Using It
There are two equivalent ways in. Use whichever fits the code you are writing.
use Rhapsody\Core\Contracts\DeferrerInterface;
// 1. The global helper, available everywhere once the app has booted
defer(fn () => $this->rebuildSitemap());
// 2. Inject the service (easier to test and to swap)
public function __construct(private DeferrerInterface $deferrer) {}
$this->deferrer->defer(fn () => $this->rebuildSitemap());
Methods
| Method | What it does |
|---|---|
defer(callable $task) | Queues a task. It is called with no arguments, so capture what it needs with use (...). |
pending() | How many tasks are waiting to run. |
discard() | Drops every waiting task without running it. |
flush() | Finishes the response and runs the waiting tasks. The framework calls this for you; you should not need to. |
How It Runs
The framework detects the environment and picks the best behaviour automatically.
| Environment | What happens |
|---|---|
| PHP-FPM or LiteSpeed | The response is flushed and the connection is closed, then tasks run. The visitor never waits. |
Apache mod_php, the built-in server | The response is fully flushed, then tasks run. The page is complete in the browser, but the connection stays open until the tasks finish. |
| RoadRunner worker | Tasks run right after the response is handed to RoadRunner. That worker is busy until they finish. |
php rhapsody commands and other CLI scripts | There is no response to wait for, so the task runs immediately when you call defer(). |
Good to Know
- Tasks cannot change the response. By the time they run, the headers and body are gone. Anything a task prints is discarded.
- The session is already closed. The session lock is released before tasks run so the visitor's next request is not blocked. Writing to the session inside a task will not persist.
- Failures are logged, never shown. A task that throws is written to
storage/logs/defer.logand the remaining tasks still run. - A failed request runs nothing. If the request ends in an exception (including a 404 or 500 page), its queued tasks are discarded.
- Tasks run in the order you queued them, and a task may call
defer()to queue more. A single request runs at most 1000 tasks. - PHP time limits still apply. Under PHP-FPM,
max_execution_timeandrequest_terminate_timeoutapply to deferred work too. Closing the connection does not remove them. - Nothing is saved. If the server stops while a task is waiting, the task is lost. For work that must survive that (payments, important emails), use a persistent queue instead.
- Resolve it by its interface. Ask the container for
DeferrerInterface::class, not the concrete class. - Boot-time code: the
defer()helper is available once the application has finished booting. A module'sboot()runs earlier, so resolveDeferrerInterfacefrom the container there instead.
Tip: keep deferred tasks small and self-contained. Capture plain values (an id, an email address) rather than whole request objects, so the task does not depend on request state that no longer matters.
Upgrading an Existing App
New apps from the skeleton have this wired up. If you are updating an existing project, add one line after the response is sent in each entry point:
// index.php
$response->send();
$kernel->afterResponse();
// worker.php, inside the while() loop, after the try/catch that responds
$kernel->afterResponse();
Apps that do not call defer() pay essentially nothing for this: afterResponse() returns immediately when nothing was queued.