Skip to content

Recurring Background Tasks with #[Scheduled] in Winter Boot

The #[Scheduled] attribute runs a bean method on a recurring timer, with no external cron job. Scheduled tasks run in a dedicated worker process, separate from the HTTP workers, so a slow task never blocks request handling. Intervals are fixed (in seconds); cron expressions are not supported.

Scheduling needs the swoole extension and the WinterWebSwooleApplication runner; it is not available under PHP-FPM. See Installation.

Enable Scheduling on Your Application Class

Section titled “Enable Scheduling on Your Application Class”

Add #[EnableScheduling] to your #[WinterBootApplication] class. Without it, #[Scheduled] attributes are ignored. If the swoole extension is missing, startup fails with an AnnotationException.

MyApplication.php
use dev\winterframework\stereotype\WinterBootApplication;
use dev\winterframework\stereotype\task\EnableScheduling;
use dev\winterframework\core\app\WinterWebSwooleApplication;
#[WinterBootApplication(
configDirectory: [__DIR__ . '/config'],
scanNamespaces: [['com\\example', __DIR__ . '/src']]
)]
#[EnableScheduling]
class MyApplication
{
public static function main(): void
{
(new WinterWebSwooleApplication())->run(self::class);
}
}

Place #[Scheduled] on a public, non-final, non-abstract void method with no required arguments, on a #[Service] or #[Component] bean. Set exactly one interval — fixedDelay, fixedRate, or one of their String variants; setting none or more than one throws an AnnotationException at startup.

  • fixedDelay int Seconds 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.

  • fixedDelayString string Property placeholder (e.g. ${my.delay}) that resolves to the fixedDelay value from your application configuration.

  • fixedRate int Seconds between the start of successive executions, regardless of how long each run takes. Use this when you need a consistent heartbeat.

  • fixedRateString string Property placeholder resolving to the fixedRate value from your application configuration.

  • initialDelay int Seconds to wait after application startup before the first execution fires.

  • initialDelayString string Property placeholder resolving to the initialDelay value from your application configuration.

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.

CacheWarmer.php
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 → …

Configure Timing with Property Placeholders

Section titled “Configure Timing with Property Placeholders”

Use the String variants of each parameter to read timing values from application.yml, so you can change schedules per environment without changing code.

application.yml
app:
scheduler:
reportDelay: 45
SomeScheduler.php
#[Component]
class SomeScheduler
{
#[Scheduled(fixedDelayString: '${app.scheduler.reportDelay}', initialDelay: 5)]
public function someScheduledMethodName(): void
{
echo 'Report generated: ' . uniqid() . PHP_EOL;
}
}

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

application.yml
winter:
task:
scheduling:
poolSize: 1 # Number of scheduling worker processes (default 1)
queueCapacity: 50 # Maximum number of scheduled tasks (default 50)
  • poolSize int (default 1) Number of worker processes that run scheduled tasks.
  • queueCapacity int (default 50) Maximum number of #[Scheduled] methods the scheduler can track.

The following files show a full scheduling setup: the application entry point and a scheduler component with two differently-timed tasks.

MyApplication.php
use dev\winterframework\stereotype\WinterBootApplication;
use dev\winterframework\stereotype\task\EnableScheduling;
use dev\winterframework\core\app\WinterWebSwooleApplication;
#[WinterBootApplication(
configDirectory: [__DIR__ . '/config'],
scanNamespaces: [['com\\example', __DIR__ . '/src']]
)]
#[EnableScheduling]
class MyApplication
{
public static function main(): void
{
(new WinterWebSwooleApplication())->run(self::class);
}
}
SomeScheduler.php
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'));
}
}