Asynchronous Task Execution with #[Async] in Winter Boot
#[Async] turns a service method into a background task: the caller returns immediately while a pool of worker processes does the work. Request latency stays low even when the work is expensive, because it happens outside the request. Under the hood, the call’s arguments are serialized into a shared-memory (or Redis-backed) queue and picked up by the async workers.
Prerequisites
Section titled “Prerequisites”Async tasks need the swoole extension and the WinterWebSwooleApplication runner. They are not available under PHP-FPM (WinterWebApplication). See Installation.
Enable Async on Your Application Class
Section titled “Enable Async on Your Application Class”Add the #[EnableAsync] attribute to your #[WinterBootApplication] class. Without it, #[Async] attributes are ignored and the methods run synchronously. If the swoole extension is missing, startup fails with an AnnotationException.
use dev\winterframework\stereotype\WinterBootApplication;use dev\winterframework\stereotype\task\EnableAsync;use dev\winterframework\core\app\WinterWebSwooleApplication;
#[WinterBootApplication( configDirectory: [__DIR__ . '/config'], scanNamespaces: [['com\\example', __DIR__ . '/src']])]#[EnableAsync]class MyApplication{ public static function main(): void { (new WinterWebSwooleApplication())->run(self::class); }}Mark a Method as Async
Section titled “Mark a Method as Async”Annotate a public, non-final, non-abstract method on a #[Service] or #[Component] bean with #[Async]. When the method is called, the framework serializes its arguments and hands the call to a background worker — the caller gets control back immediately.
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(); }}Return Values
Section titled “Return Values”An #[Async] method must return void (or declare no return type). The caller does not wait for the worker, so there is no way to return a value.
Method Parameter Constraints
Section titled “Method Parameter Constraints”Arguments are serialized to reach the worker process. Every parameter must have a type, and should be a scalar (int, float, string, or bool). Other types (objects, arrays, mixed) log an error at startup and may not survive the trip; pass an ID and load the object inside the task instead.
#[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 { … }}Configuration
Section titled “Configuration”Configure the async worker pool in application.yml under winter.task.async:
winter: task: async: poolSize: 4 # Number of background worker processes (default 1) queueCapacity: 100 # Maximum queued calls (default 50) argsSize: 2048 # Maximum serialized size of the arguments, in bytes (default 2048) maxConcurrency: 0 # Max jobs running at once per worker, 0 = unlimited (default)poolSizeint(default1) Number of background worker processes to start.queueCapacityint(default50) Maximum number of async calls that can wait in the queue at once.argsSizeint(default2048) Upper limit in bytes for the serialized arguments of a single async call.maxConcurrencyint(default0= unlimited) Most jobs one async worker runs at the same time. Extra jobs wait in the queue. Set it below your database pool size when async jobs use the database.
When the in-memory queue is full, the call is dropped and an error is logged naming the class and method; the calling request is not failed. Enqueuing and processing a call log at DEBUG (they were INFO before 2.1.2), so a high call rate does not flood the logs.
Queue Storage
Section titled “Queue Storage”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:
winter: task: async: poolSize: 4 queueCapacity: 100 argsSize: 2048 queueStorage: handler: dev\winterframework\data\redis\async\AsyncRedisQueueStoreComplete Example
Section titled “Complete Example”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.
use dev\winterframework\stereotype\WinterBootApplication;use dev\winterframework\stereotype\task\EnableAsync;use dev\winterframework\core\app\WinterWebSwooleApplication;
#[WinterBootApplication( configDirectory: [__DIR__ . '/config'], scanNamespaces: [['com\\example', __DIR__ . '/src']])]#[EnableAsync]class MyApplication{ public static function main(): void { (new WinterWebSwooleApplication())->run(self::class); }}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"; }}use dev\winterframework\stereotype\RestController;use dev\winterframework\stereotype\Autowired;use dev\winterframework\stereotype\web\PathVariable;use dev\winterframework\stereotype\web\PutMapping;use dev\winterframework\web\http\ResponseEntity;
#[RestController]class UserController{ #[Autowired] private AuditService $auditService;
#[PutMapping(path: '/users/{userId}/{resource}')] public function updateProfile(#[PathVariable] string $userId, #[PathVariable] string $resource): ResponseEntity { // Fire and forget — returns immediately $this->auditService->logActivity($userId, 'UPDATE', $resource);
return ResponseEntity::ok(['status' => 'Profile updated']); }}