A fundamental rule is simple: don't make the caller wait for something they don't need to wait for.
A merchant asks the License API to issue a license for a new site. The Statamic driver makes a real network call, and that call can take a second or two, longer if upstream is having a slow morning. The caller still needs the license key back in the response body right now, because the next thing that happens is the site gets configured with it. That request stays synchronous whether you like it or not.
Now take the opposite case: a license gets revoked because a site was torn down. Nobody is waiting on that response. The site is already gone. The only thing that matters is that the revocation eventually happens, not that anyone sits through a round trip to Statamic to find out.
Same domain, same kind of operation, two different answers to "should this be synchronous." This chapter is about giving the right answer to that question, and about what actually happens after you dispatch a job: whether it can survive being run twice, whether it knows when to stop trying, and whether anyone finds out when it quietly falls over. A job that only works when nothing goes wrong isn't a background job. It's a bug that hasn't fired yet.
What belongs off the request
Look at the two license operations again. Creation returns data the caller needs immediately, key, domain, status, all of it has to be in the HTTP response. Deletion returns nothing the caller cares about. That's the actual test, not "is this slow" but "does anyone need the result before the response leaves."
// app/Jobs/DeleteLicenseJob.php
final class DeleteLicenseJob implements ShouldBeEncrypted, ShouldQueue
{
use Queueable;
public int $tries = 3;
/** @var array<int, int> */
public array $backoff = [60, 300];
public ?int $timeout = 30;
public function __construct(
public string $key,
public ?string $driver = null,
public ?string $token = null,
) {
/** @var string $queue */
$queue = config('services.license.delete_queue', 'default');
$this->onQueue($queue);
}
public function handle(): void
{
if (is_string($this->driver) && $this->driver !== '') {
Config::set('services.license.default', $this->driver);
if (is_string($this->token) && $this->token !== '') {
Config::set("services.license.drivers.{$this->driver}.token", $this->token);
}
}
LicenseFacade::deleteLicense(key: $this->key);
}
}
The controller that handles the delete endpoint dispatches this job and returns a MessageResponse immediately. No polling, no waiting for Statamic. The constructor here is worth noticing: driver and token are captured at dispatch time, from the caller's bearer, and reapplied inside handle() before the facade runs. Jobs execute on a worker process with no HTTP request behind them, so anything the underlying request context would normally supply (which driver, which credentials) has to travel with the job payload instead. ShouldBeEncrypted on this job class means that payload, driver name and token included, is encrypted at rest in the queue table. A job carrying a bearer token is a job that leaks a bearer token if you forget that flag.
One job creates a license and the HTTP response can't leave without its output. Another deletes a license and neither the response nor the caller particularly cares when it finishes. A third, later in this chapter, notifies staff that a job failed after six hours of retries, and by that point nobody has been waiting on anything for a long time. Three shapes of "later," three different reasons to queue.