Pushword runs several operations as background tasks to avoid blocking HTTP requests:
pw:image:cache)pw:image:optimize)pw:pdf:optimize)pw:static)pw:page-scan)pw:flat:sync)Two modes are available, configured via background_task_handler:
Uses nohup to spawn background processes with PID-based locking. Works out of the box with no extra setup.
# config/packages/pushword.yaml
pushword:
background_task_handler: process
Dispatches tasks onto the Symfony Messenger bus. Better suited for high-load sites or environments where direct process spawning is restricted.
# config/packages/pushword.yaml
pushword:
background_task_handler: messenger
composer require symfony/messenger
config/packages/messenger.yaml:framework:
messenger:
transports:
async:
dsn: 'doctrine://default' # uses your existing database — no Redis needed
routing:
'Pushword\Core\BackgroundTask\RunCommandMessage': async
For higher throughput, you can use Redis (
redis://localhost:6379/messages) or RabbitMQ instead of the Doctrine transport.
php bin/console messenger:consume async
The HTMX-based admin polling UI works identically in both modes since existing commands handle their own PID registration and output writing.
Messenger routes by message class, and every background task shares RunCommandMessage — so a single transport serves them first-come. Queue a few thousand thumbnails and the next site generation waits behind all of them.
background_task_transports gives a command its own lane. Keys are the command as passed by the dispatching code (pw:static, pw:image:cache, pw:page-scan, pw:pdf:optimize, pw:image:optimize); anything left out follows the routing above.
# config/packages/pushword.yaml
pushword:
background_task_handler: messenger
background_task_transports:
'pw:static': static
# config/packages/messenger.yaml
framework:
messenger:
transports:
async: { dsn: 'doctrine://default' }
static: { dsn: 'doctrine://default?queue_name=static' }
routing:
'Pushword\Core\BackgroundTask\RunCommandMessage': async
Then consume the interactive lane with its own worker, so a mass sweep on async cannot delay it:
php bin/console messenger:consume static
queued vs runningA dispatched task that no consumer has picked up yet is reported as queued — by the admin screens and by the status field of the static-generation and page-scan APIs. It is distinct from running: nothing has started, there is no console output yet, and on a busy queue it can stay that way for a long time. Clients polling a statusUrl should treat queued and running alike and keep waiting.
This state only occurs in messenger mode. In process mode a dispatched task is running by the time the request returns.
Commands like pw:image:cache use per-file locks to prevent duplicate processing of the same image. When a second instance is triggered for the same file while the first is still running, it skips silently.
Each upload spawns a separate background process. Different files can run concurrently without blocking each other (per-file locks). Under heavy concurrent load, many simultaneous processes may be spawned.
With Messenger, tasks are queued and processed sequentially by the worker. Even if many uploads happen simultaneously, all cache generation tasks are dispatched to the message bus. The worker processes them one at a time, ensuring controlled resource usage.
# config/packages/pushword.yaml
pushword:
background_task_handler: messenger
Configure commands that run automatically on specific triggers: when a scheduled page becomes published, or on a cron schedule.
# config/packages/pushword.yaml
pushword:
scheduled_commands:
- { command: 'pw:static -i', on: 'publish' }
- { command: 'pw:static', on: 'cron: 0 4 * * *' }
Triggers:
publish — runs when a page with a future publishedAt date becomes publishedcron: <expression> — runs on a cron schedule (e.g., cron: 0 4 * * * for daily at 4am)Everything is automatic — no system cron needed. Install the Scheduler component:
composer require symfony/scheduler
Then consume the Pushword schedule alongside your async transport:
php bin/console messenger:consume async scheduler_pushword
For on: publish triggers, set up a system cron to run pw:cron periodically:
*/5 * * * * cd /path/to/project && php bin/console pw:cron
For on: cron: triggers in process mode, you need to set up the corresponding system crons manually.
These commands are run manually to maintain media consistency:
pw:media:normalize-filenames)Renames media files to URL-safe slugs. Use --dry-run to preview changes.
php bin/console pw:media:normalize-filenames --dry-run
php bin/console pw:media:normalize-filenames
pw:media:clean-duplicates)Detects media entries that share the same file content (SHA-1 hash) and merges them. For each duplicate group, the oldest entry (lowest ID) is kept as canonical. Duplicate filenames are added to the canonical entry’s fileNameHistory so existing references keep resolving transparently.
Page mainImage references are transferred to the canonical entry. Physical files and image cache for removed duplicates are cleaned up automatically.
# Preview which media would be merged
php bin/console pw:media:clean-duplicates --dry-run
# Merge duplicates
php bin/console pw:media:clean-duplicates