Skip to content

Asynchronous Task Execution with #[Async] in Winter Boot

Winter Boot’s asynchronous execution model lets you fire off any service or component method as a background task — the calling code returns immediately while a dedicated pool of Swoole worker processes handles the actual work. This non-blocking design keeps web request latency low even when individual operations are expensive, because heavy lifting happens outside the request/response cycle entirely. Under the hood, arguments are serialised into an in-memory (or Redis-backed) queue and consumed by background workers, so the concurrency model is process-based rather than thread-based.

Async support is powered by the Swoole PHP extension. Install it via PECL and enable it in your php.ini before proceeding.

  1. Install the Swoole extension

    Terminal window
    pecl install swoole
  2. Enable Swoole in php.ini

    extension=swoole.so

Add the #[EnableAsync] attribute to your #[WinterBootApplication] class. The framework validates at boot time that both attributes are present and that Swoole is loaded — a TypeError or AnnotationException is thrown otherwise.

MyApplication.php
use dev\winterframework\stereotype\WinterBootApplication;
use dev\winterframework\stereotype\task\EnableAsync;
use dev\winterframework\web\WinterWebSwooleApplication;
#[WinterBootApplication(scanPackages: ['com\example'])]
#[EnableAsync]
class MyApplication
{
public static function main(): void
{
(new WinterWebSwooleApplication())->run(self::class);
}
}

Annotate any public, non-final, non-abstract method on a #[Service] or #[Component] bean with #[Async]. When the method is called at runtime the framework serialises its arguments and dispatches the call to a background worker — the caller receives control back instantly.

NotificationService.php
use dev\winterframework\stereotype\Service;
use dev\winterframework\task\async\stereotype\Async;
#[Service]
class NotificationService
{
#[Async]
public function sendWelcomeEmail(string $recipientEmail, string $userName): void
{
// This runs in a background worker process.
// Heavy I/O, third-party API calls, etc. go here.
echo "Sending welcome email to {$recipientEmail} from PID " . getmypid();
}
}

An #[Async] method must declare a void return type. Because the caller does not wait for the worker to finish, there is no mechanism to return a value back to the calling code.

The framework serialises method arguments to pass them to a worker process. Each parameter must be typed as a scalar (int, float, string, or bool). Avoid mixed types; the framework logs an error at boot if it encounters them.

ReportService.php
#[Service]
class ReportService
{
// ✅ Scalar parameters — fully supported
#[Async]
public function generateReport(int $reportId, string $format, bool $compress): void
{
// background work …
}
// ⚠️ Complex/object parameters are not recommended
// #[Async]
// public function processOrder(Order $order): void { … }
}

Configure the async worker pool in application.yml under winter.task.async:

application.yml
winter:
task:
async:
poolSize: 4 # Number of background worker processes
queueCapacity: 100 # Maximum concurrent async requests in the queue
argsSize: 2048 # Maximum serialised size (bytes) of method arguments
  • poolSize int Total number of dedicated background worker processes to start.
  • queueCapacity int Maximum number of async calls that can be queued and awaiting execution at any one time.
  • argsSize int Upper limit in bytes for the combined serialised arguments of a single async call.

By default, Winter Boot uses shared memory as its async queue. Pending calls are lost if the application restarts. For persistence across restarts, switch to the Redis-backed queue store provided by the winter-data-redis module:

application.yml
winter:
task:
async:
poolSize: 4
queueCapacity: 100
argsSize: 2048
queueStorage:
handler: dev\winterframework\data\redis\async\AsyncRedisQueueStore

The following three files show a full fire-and-forget audit logging flow: the application class, the async service, and the controller that triggers it.

MyApplication.php
use dev\winterframework\stereotype\WinterBootApplication;
use dev\winterframework\stereotype\task\EnableAsync;
use dev\winterframework\web\WinterWebSwooleApplication;
#[WinterBootApplication(scanPackages: ['com\example'])]
#[EnableAsync]
class MyApplication
{
public static function main(): void
{
(new WinterWebSwooleApplication())->run(self::class);
}
}
AuditService.php
use dev\winterframework\stereotype\Service;
use dev\winterframework\task\async\stereotype\Async;
#[Service]
class AuditService
{
#[Async]
public function logActivity(string $userId, string $action, string $resource): void
{
// Runs in a background worker — caller is not blocked.
$timestamp = date('Y-m-d H:i:s');
echo "[{$timestamp}] User {$userId} performed {$action} on {$resource} (PID: " . getmypid() . ")\n";
}
}
UserController.php
use dev\winterframework\stereotype\RestController;
use dev\winterframework\stereotype\Autowired;
use dev\winterframework\web\http\ResponseEntity;
#[RestController]
class UserController
{
#[Autowired]
private AuditService $auditService;
public function updateProfile(string $userId, string $resource): ResponseEntity
{
// Fire and forget — returns immediately
$this->auditService->logActivity($userId, 'UPDATE', $resource);
return ResponseEntity::ok(['status' => 'Profile updated']);
}
}