Recurring Background Tasks with #[Scheduled] in Winter Boot
Winter Boot’s scheduling system lets you declare recurring tasks directly on service or component bean methods using the #[Scheduled] attribute. Rather than managing cron jobs externally, you embed timing logic inside your application code, and the framework handles process lifecycle and execution. Scheduled tasks run inside a dedicated worker process separate from the HTTP workers — this means a slow or long-running task never blocks web request handling. All you need is the Swoole extension and a single annotation on your application class to activate the feature.
Prerequisites
Section titled “Prerequisites”Scheduling is powered by the Swoole PHP extension. Install and enable it before using #[Scheduled].
-
Install the Swoole extension
Terminal window pecl install swoole -
Enable Swoole in php.ini
extension=swoole.so
Enable Scheduling on Your Application Class
Section titled “Enable Scheduling on Your Application Class”Add #[EnableScheduling] to your #[WinterBootApplication] class. The framework validates at startup that #[WinterBootApplication] is also present and that Swoole is loaded — a TypeError or AnnotationException is raised if either condition is not met.
use dev\winterframework\stereotype\WinterBootApplication;use dev\winterframework\stereotype\task\EnableScheduling;use dev\winterframework\web\WinterWebSwooleApplication;
#[WinterBootApplication(scanPackages: ['com\example'])]#[EnableScheduling]class MyApplication{ public static function main(): void { (new WinterWebSwooleApplication())->run(self::class); }}The #[Scheduled] Attribute
Section titled “The #[Scheduled] Attribute”Place #[Scheduled] on any public, non-final, non-abstract, zero-argument void method on a #[Service] or #[Component] bean. You must supply exactly one of the interval parameters (fixedDelay or fixedRate); combining conflicting options throws an AnnotationException at boot.
Parameters
Section titled “Parameters”-
fixedDelayintSeconds to wait after the previous execution completes before starting the next. Use this when you want to avoid overlapping runs and the task duration may vary. -
fixedDelayStringstringProperty placeholder (e.g.${my.delay}) that resolves to thefixedDelayvalue from your application configuration. -
fixedRateintSeconds between the start of successive executions, regardless of how long each run takes. Use this when you need a consistent heartbeat. -
fixedRateStringstringProperty placeholder resolving to thefixedRatevalue from your application configuration. -
initialDelayintSeconds to wait after application startup before the first execution fires. -
initialDelayStringstringProperty placeholder resolving to theinitialDelayvalue from your application configuration.
Fixed Delay vs. Fixed Rate
Section titled “Fixed Delay vs. Fixed Rate”The two core scheduling modes behave differently when a task takes longer than its interval. Choose the one that matches your task’s requirements.
fixedDelay introduces a gap between the end of one execution and the start of the next. Use this when you want to avoid overlapping runs and the task duration may vary.
use dev\winterframework\stereotype\Component;use dev\winterframework\task\scheduling\stereotype\Scheduled;
#[Component]class CacheWarmer{ #[Scheduled(fixedDelay: 60, initialDelay: 10)] public function refreshCache(): void { // Runs 10 seconds after startup, then again 60 seconds after // each completion. echo 'Cache refreshed at ' . date('H:i:s') . PHP_EOL; }}Timeline: [startup] → 10s delay → run → 60s delay → run → 60s delay → run → …
fixedRate fires the method every N seconds counted from the start of the previous invocation. Use this when you need a consistent heartbeat regardless of individual run time.
use dev\winterframework\stereotype\Component;use dev\winterframework\task\scheduling\stereotype\Scheduled;
#[Component]class HealthReporter{ #[Scheduled(fixedRate: 30)] public function reportHealthMetrics(): void { // Fires every 30 seconds from the previous start time. echo 'Health check at ' . date('H:i:s') . ' — PID: ' . getmypid() . PHP_EOL; }}Timeline: [startup] → run starts → 30s → run starts → 30s → run starts → …
Externalise Timing with Property Placeholders
Section titled “Externalise Timing with Property Placeholders”Timing values can be externalised to application.yml using the String variants of each parameter. This lets you adjust schedules per environment without redeploying code.
app: scheduler: reportDelay: 45#[Component]class SomeScheduler{ #[Scheduled(fixedDelayString: '${app.scheduler.reportDelay}', initialDelay: 5)] public function someScheduledMethodName(): void { echo 'I generate a unique ID every ' . getenv('app.scheduler.reportDelay') . ' seconds: ' . uniqid(); }}Configuration
Section titled “Configuration”Configure the scheduling worker pool in application.yml under winter.task.scheduling:
winter: task: scheduling: poolSize: 1 # Number of dedicated scheduling worker processes queueCapacity: 50 # Maximum concurrent scheduled requests that can be queuedpoolSizeintTotal number of worker processes dedicated to executing scheduled tasks.queueCapacityintMaximum number of concurrent scheduled task invocations that can be queued at once.
Complete Example
Section titled “Complete Example”The following files show a full scheduling setup: the application entry point and a scheduler component with two differently-timed tasks.
use dev\winterframework\stereotype\WinterBootApplication;use dev\winterframework\stereotype\task\EnableScheduling;use dev\winterframework\web\WinterWebSwooleApplication;
#[WinterBootApplication(scanPackages: ['com\example'])]#[EnableScheduling]class MyApplication{ public static function main(): void { (new WinterWebSwooleApplication())->run(self::class); }}use dev\winterframework\stereotype\Component;use dev\winterframework\stereotype\Autowired;use dev\winterframework\task\scheduling\stereotype\Scheduled;
#[Component]class SomeScheduler{ #[Autowired] private ReportRepository $reportRepository;
/** * Generates a unique ID 10 seconds after startup, * then every 20 seconds after each completion. */ #[Scheduled(fixedDelay: 20, initialDelay: 10)] public function someScheduledMethodName(): void { echo 'I generate a unique ID every 20 seconds: ' . uniqid(); }
/** * Persists a daily summary on a 24-hour fixed rate. */ #[Scheduled(fixedRate: 86400, initialDelay: 60)] public function persistDailySummary(): void { $this->reportRepository->saveDailySummary(date('Y-m-d')); }}