The shop accepts orders, they appear complete in the admin list - but the order confirmation only reaches the customer hours later. The search index still shows old product names, the sitemap is two days old, the newsletter dispatch is not moving. These symptoms belong together, and in Shopware 6 they usually share one cause: the Shopware message queue is being processed by the admin worker in a browser tab instead of real CLI workers on the server. Shopware relies on the Symfony Messenger component for asynchronous processing; out of the box, messages are stored in the database and processed through the browser as long as somebody is logged into the Administration - according to the vendor a simple and fast method for development, but explicitly not recommended for production systems (Shopware Developer Documentation, Message Queue). This article is an operations guide: what runs through the queue, how to run workers as a system service, how to separate transports and queues, how to spot a backlog, and at which queue length an alert makes sense. For shops covered by our hosting and maintenance, exactly this setup is part of the standard scope.

The symptoms: late mails, stale index, dead sitemap

The complaints merchants arrive with in these cases sound like four different problems - and they are one. Everything that Shopware 6 does not handle directly inside the request goes into the message queue. If its processing stops, the tasks customers see fail first, then the ones Google sees, and finally the ones accounting misses.

  • Order confirmations arrive late or in batches as soon as somebody logs into the Administration. Queued mail dispatch is configured, the consumer is missing.
  • The search index lags behind. Price and name changes only become visible in the storefront after a manual reindex, because indexing messages remain unprocessed - an effect easily mistaken for poor search relevance in the shop.
  • The sitemap is outdated. Its generation depends on the scheduled task shopware.sitemap_generate with a default interval of 86,400 seconds (shopware/docs, scheduled-task.md) - which is never executed without a running worker.
  • Product export feeds stall.product_export_generate_task has the shortest default interval out of the box at 60 seconds (shopware/docs, scheduled-task.md) and therefore surfaces quickly.
  • Newsletters and bulk mails hang, because mail dispatch runs over the message bus as soon as framework.mailer.message_bus is set (Shopware Developer Documentation, Message Queue).
  • Cache invalidation is delayed. The task shopware.invalidate_cache is listed with a 20-second interval in the defaults (shopware/docs, scheduled-task.md) and co-decides when changed content reaches the storefront.

For order confirmations, the delay is not merely a service topic. In Germany, Section 312i (1) sentence 1 no. 3 of the Civil Code (BGB) obliges businesses in electronic commerce to confirm receipt of the order without undue delay by electronic means (Federal Ministry of Justice, Section 312i BGB). A confirmation that leaves the server the next morning because somebody logs into the admin then is hard to classify as undelayed. On top of that comes the commercial part: every missing confirmation typically produces a support request - at exactly the moment when the customer has just paid and is unsure whether the order arrived at all (project experience). That costs twice: handling time once, trust a second time.

Meanwhile the shop looks perfectly healthy

Uptime checks are green, the home page loads fast, checkout works. The queue is the only part of the system standing still - and it does not report itself. That is why the condition often goes unnoticed for weeks, until a cluster of support cases, an outdated feed or a missing invoice draws attention to it.

What runs through the Shopware message queue

Shopware uses Symfony Messenger and Enqueue to process messages asynchronously - tasks run in the background and are therefore independent of request timeouts or system crashes (Shopware Developer Documentation, Message Queue). Out of the box there are three transports: async for regular operation, low_priority for secondary tasks and a failure transport for messages that have used up their delivery attempts. They are configured through the environment variables MESSENGER_TRANSPORT_DSN, MESSENGER_TRANSPORT_LOW_PRIORITY_DSN and MESSENGER_TRANSPORT_FAILURE_DSN (Shopware Developer Documentation, Message Queue). Anyone planning e-commerce operations should know these three names - half the daily routine of the shop depends on them.

Transactional mails

Shopware sends mails synchronously by default. Setting framework.mailer.message_bus to messenger.default_bus moves order, cancellation and shipping mails into the queue - the recommended variant according to the vendor, because slow mail servers otherwise slow down page delivery (Shopware Developer Documentation, Performance Tweaks).

Indexing in the data layer

Entity indexing messages update product, category and search data. According to the vendor they are comparatively slow, while other message types are processed very quickly (Shopware Developer Documentation, Message Queue) - the main reason for separating queues.

Scheduled tasks

Shopware ships 16 default tasks - from log_entry.cleanup through cart.cleanup to shopware.sitemap_generate (shopware/docs, scheduled-task.md). They are scheduled through the queue and need a running consumer to be executed at all.

Import, export and feeds

Import and export runs, product export feeds and app updates are asynchronous as well. A bulk import creates a large number of messages in a short time - the point at which queue separation decides how long order mails have to wait.

The default scheduled tasks cover a wide range of intervals: shopware.invalidate_cache runs every 20 seconds, product_export_generate_task every 60 seconds, most cleanup jobs daily at 86,400 seconds, product_keyword_dictionary.cleanup weekly at 604,800 seconds and product_download.media.cleanup roughly monthly at 2,628,000 seconds (shopware/docs, scheduled-task.md). They are scheduled by bin/console scheduled-task:run; since Shopware 6.5.5.0 the --no-wait option makes it possible to place the call cleanly into a cron entry every five minutes instead of keeping a permanently waiting process (Shopware Developer Documentation, Scheduled Task).

The scheduler only schedules - the work happens elsewhere

scheduled-task:run places messages into the queue, messenger:consume executes them. If only the scheduler runs, the queue fills up without anything happening. If only the consumer runs, tasks are never scheduled in the first place. Both processes belong in service management - since Shopware 6.7.2.0, scheduled-task:list and scheduled-task:run-single help to inspect the current state (Shopware Developer Documentation, Scheduled Task).

Why the admin worker in a browser tab is not an operations solution

The admin worker is a mechanism of the Administration: the interface keeps a request open, processes the configured transports, and once the poll interval has elapsed the request ends, after which the Administration starts a new one. The documented default for this interval is 30 seconds, and the transports to be consumed are listed explicitly (Shopware Developer Documentation, Message Queue):

config/packages/shopware.yaml
# config/packages/shopware.yaml
shopware:
    admin_worker:
        enable_admin_worker: true
        poll_interval: 30
        transports: ["async", "low_priority"]

Three operational problems follow from this. First, processing only happens while somebody is logged in - at night, on weekends and during company holidays the queue stands still. Second, every open Administration creates a permanently running PHP request; with several staff members logged in at the same time this leads to high CPU load and interferes with the smooth execution of PHP-FPM, according to the vendor (Shopware Developer Documentation, Message Queue). Third, processing capacity depends on user behaviour instead of capacity planning: closing the admin tab stops background processing without anyone noticing.

CriterionAdmin workerCLI worker as a service
Runs when?Only with the Administration logged inContinuously, independent of logins
CapacityDepends on open admin tabsControlled via the number of service instances
Load profilePHP-FPM processes, CPU load in the web stackSeparate processes, decoupled from the web stack
PrioritisationFixed transport list in the configurationOwn transports and order per service
Restart after deploymentOnly on the next page viewOrderly via stop-workers and process manager
Failure transportNot coveredDedicated consumer can be set up
Running both at once is the most common misconfiguration

If CLI workers are set up, the admin worker should be disabled in shopware.yaml - the vendor documentation states this explicitly (Shopware Developer Documentation, Message Queue). If both run, they compete for the same messages, and the load in the web stack remains even though the services have long taken over the work:

config/packages/shopware.yaml
# config/packages/shopware.yaml
shopware:
    admin_worker:
        enable_admin_worker: false

Workers as a system service: limits, instances, orderly restart

The recommended way is the CLI worker (Shopware Developer Documentation, Message Queue). The messenger:consume command processes one or more transports and terminates once defined limits are reached. That is intentional: a long-lived PHP process accumulates memory over its runtime, so it is replaced regularly instead of running forever. Time and memory limits are therefore not an emergency brake but the operating mode.

Terminal
$ bin/console messenger:consume async --time-limit=60 --memory-limit=128M
$ bin/console messenger:consume async low_priority
$ bin/console scheduled-task:run --no-wait

The order of transports in the call is a prioritisation decision: the worker processes the first transport and only moves to the next one when nothing is waiting there (Symfony Documentation, Messenger). So messenger:consume async low_priority means mails and indexing first, secondary tasks afterwards. To make the process start again once it hits its limits, it belongs under a process manager - systemd or supervisor; a cron call is the weaker alternative (Shopware Developer Documentation, Message Queue). Shopware ships a template for an instantiable systemd unit:

/etc/systemd/system/shopware_consumer@.service
[Unit]
Description=Shopware Message Queue Consumer, instance %i
PartOf=shopware_consumer.target

[Service]
Type=simple
User=www-data
Restart=always
WorkingDirectory=/var/www/html
ExecStart=php /var/www/html/bin/console messenger:consume --time-limit=60 --memory-limit=512M async low_priority

[Install]
WantedBy=shopware_consumer.target

With systemctl enable shopware_consumer@{1..3}.service this becomes three parallel instances started through a shared target (Shopware Developer Documentation, Message Queue). The template works with --time-limit=60 and --memory-limit=512M: the process ends after 60 seconds or at 512 MB of memory usage and is restarted immediately by Restart=always. Those who prefer supervisor control the instance count via numprocs, allow several startup attempts via startretries and grant the process a grace period to finish the current message via stopwaitsecs - documented with an example value of 20 seconds in the Symfony documentation (Symfony Documentation, Messenger).

Using cron as a worker substitute has a catch

A periodic cron call knows no upper limit for concurrently running workers. If a message takes longer than the configured time limit, the next cron run still starts another process - which can create an unintended, growing number of workers, as the vendor documentation points out explicitly (Shopware Developer Documentation, Message Queue). Process managers such as systemd or supervisor prevent this because they count their instances.

After every deployment the workers have to restart, otherwise they keep running the code loaded at process start. Symfony provides bin/console messenger:stop-workers for this: the command sets a flag in the cache, each worker finishes its current message and then exits, and the process manager restarts it with the new code (Symfony Documentation, Messenger). The call belongs after the code rollout and cache warmup. Two details decide whether it works: if the application runs on several hosts, the cache must be a shared adapter so the flag reaches all processes (Symfony Documentation, Messenger). And anyone who has set opcache.validate_timestamps=0 - one of the recommendations in the Shopware performance notes - has to clear OPcache actively during deployment, otherwise web processes and workers keep running on stale bytecode (Shopware Developer Documentation, Performance Tweaks).

A deployment order that has proven itself

Roll out code, run database migrations, warm the cache, reset OPcache, call messenger:stop-workers, let the process manager restart the workers, and only then switch traffic. How to run this without a maintenance window is described in our article on zero-downtime deployments with blue-green; if you would rather hand over day-to-day operations, the overview on managed hosting for online shops draws the line.

Choosing a transport: database by default, Redis or AMQP under load

By default Shopware uses the Doctrine transport, which stores messages in the database. The vendor describes this as a simple solution that works well for development but is not recommended for production systems, and points to specialised brokers that relieve the database (Shopware Developer Documentation, Message Queue). In practice the database still carries smaller shops with a manageable message volume reliably - the switch pays off as soon as several workers run in parallel, bulk imports are routine, or the database is already close to its write limit.

TransportSuitable forOperating effortTypical limit
Database (Doctrine)Default case, moderate message volumeNo additional serviceWrite load and row locks with many workers
RedisHigh frequency, short messagesOwn service, persistence to be clarifiedMemory footprint, behaviour on restart
AMQP brokerMany consumers, multiple queues, routingOwn service with its own administrationAdditional component in the operating model

Regardless of the chosen transport there is one detail that becomes noticeable under load: Symfony Messenger checks on every connection whether the queue exists and creates it on demand. Shopware recommends disabling this with ?auto_setup=false on the connection URL and creating the queues once during deployment with bin/console messenger:setup-transports (Shopware Developer Documentation, Performance Tweaks). If Redis is in play anyway, it is worth looking at its other use cases - summarised in our article on Redis caching for Shopware.

Using the database as a queue has a visible price

With the Doctrine transport, messages live in the messenger_messages table. Every worker polls it at short intervals and locks rows for the duration of processing. As the number of workers grows, so does the write load on exactly the database that serves the shop in parallel. If you are working on the database anyway, it makes sense to review the database version lifecycle at the same time.

Separating queues so bulk imports do not block mail dispatch

The most important operational step after disabling the admin worker is separating the queues. The reason is quickly told: a catalogue import with 50,000 items creates a very large number of indexing messages within minutes. If order mails sit in the same queue, they queue up behind that pile - the customer waits for the confirmation while the worker indexes product data. Shopware points out explicitly that routing messages to a different transport can make sense in order to limit the number of workers for a specific message type, avoid database locks or prioritise mail dispatch (Shopware Developer Documentation, Message Queue).

Routing to a dedicated indexing transport
# config/packages/framework.yaml
framework:
    messenger:
        transports:
            async: "%env(MESSENGER_TRANSPORT_DSN)%"
            entity_indexing: "%env(MESSENGER_TRANSPORT_INDEXING_DSN)%"
        routing:
            '*': async

# config/packages/shopware.yaml
shopware:
    messenger:
        routing_overwrite:
            'Shopware\Core\Framework\DataAbstractionLayer\Indexing\EntityIndexingMessage': entity_indexing

shopware.messenger.routing_overwrite overwrites an existing assignment instead of adding another one, unlike the plain Symfony routing configuration. That is exactly what is needed for indexing messages, which are routed to async by default through the AsyncMessageInterface. The option was introduced with Shopware 6.6.4.0 and 6.5.12.0 (Shopware Developer Documentation, Message Queue). Implementing custom message types and handlers belongs into individual development and should be designed together with the operating concept, not after it.

  1. Service 1 - mail and regular operation:messenger:consume async, two to three instances, short time limits. The target metric is the waiting time until the order confirmation.
  2. Service 2 - indexing:messenger:consume entity_indexing, limited instance count, higher memory limit. The target metric is throughput without lock contention in the database.
  3. Service 3 - secondary work:messenger:consume low_priority, one instance. Cleanup jobs and everything that tolerates a delay of hours.
  4. Service 4 - failure transport: a dedicated consumer with a longer interval. Without it, failed messages remain unprocessed - the vendor documentation states this explicitly as a warning (Shopware Developer Documentation, Message Queue).

The real question is not how many workers a shop needs, but which message is allowed to wait behind which other one.

XICTRON development team

Scaling: sizing the worker count and spotting a backlog

Shopware deliberately gives no blanket answer to the question of the right worker count: it depends on the number of queued messages and their type - product indexing is rather slow, other messages are processed very quickly; the vendor recommends monitoring the queue and adjusting the number accordingly (Shopware Developer Documentation, Message Queue). That is not evasive but the only defensible answer - and it assumes the backlog is measured at all.

The most meaningful metric here is not the number of waiting messages but their age. A queue with 5,000 entries that is cleared within four minutes is unremarkable. A queue with 40 entries whose oldest has been waiting for two hours is an incident. With the Doctrine transport both values can be read from the messenger_messages table: the row count per queue_name and the oldest available_at timestamp without a delivered_at value. With Redis or an AMQP broker the built-in tooling of the respective service provides the same two figures.

ObservationCause is in the queueCause is in the shop
Queue lengthGrows continuously, even at nightRises during peaks and falls afterwards
Oldest entryAge grows linearly with the clockAge stays in the range of minutes
Worker processesNo process active or all permanently busyProcesses run and are idle at times
Error patternMails and index late, pages fastPages slow, error rate rising
First measureCheck services, add instances, split queuesCheck resources, database and caching

There is a natural limit to adding instances: beyond a certain point workers compete for the same database locks, and throughput no longer grows proportionally to the process count. That is the moment to either change the transport or move a message type to its own transport - not the moment for the fourth and fifth worker on the same queue. How the remaining levers in the system relate to each other is covered in our article on Shopware 6 performance optimisation.

Two numbers are enough to start with

Waiting messages per queue, and age of the oldest waiting message. Anyone who puts just these two values on a dashboard spots a stalled worker within minutes instead of learning about it from customer complaints. Everything else - throughput per minute, error rate, runtime per message type - is refinement that adds little without this basis.

Error handling: failure transport, retries and alerts

If processing a message fails, it is moved to the failure transport. The default configuration retries delivery three times; if it fails again, the message is deleted (Shopware Developer Documentation, Message Queue). In Symfony the retry strategy is parameterised: max_retries is 3, the first delay is 1,000 milliseconds and the multiplier is 2 - so the intervals grow exponentially, and max_delay caps them where needed (Symfony Documentation, Messenger). For a brief mail server outage this is exactly right; for an outage lasting several minutes it is not enough.

Without a consumer on the failure transport, order mails disappear silently

The vendor documentation phrases it as an explicit warning: the CLI worker has to be set up for the failed queue as well, otherwise failed messages are not processed (Shopware Developer Documentation, Message Queue). In practice that means an order confirmation whose mail server was unreachable during a three-minute outage ends up in a table nobody looks at - while the customer keeps waiting.

Terminal
$ bin/console messenger:failed:show --stats
$ bin/console messenger:failed:show 20 -vv
$ bin/console messenger:failed:retry --force
$ bin/console messenger:failed:remove 20

Symfony ships dedicated commands for working with the failure transport: messenger:failed:show lists the entries and reveals the full error with -vv, messenger:failed:retry redelivers single or all messages, and messenger:failed:remove deletes them (Symfony Documentation, Messenger). Also useful is --failure-limit on the consumer: the worker stops after a defined number of failures instead of looping over the same broken message (Symfony Documentation, Messenger). Combined with Restart=always this produces a clean self-healing mechanism for temporary faults.

  • No worker process active - alert after 60 seconds. This single alert explains the majority of the symptoms from the first section.
  • Age of the oldest waiting message above 5 minutes in the mail queue, above 30 minutes in the indexing queue. Set thresholds per queue, otherwise the alert is either deaf or annoying.
  • Queue length above a defined value - the value follows from measured throughput, not from gut feeling.
  • Growth in the failure transport - every new entry is an event, not an operating state.
  • Scheduled task without execution within its expected interval - a task with a 60-second interval that has not run for an hour indicates a stalled scheduler.

Monitoring metrics and typical pitfalls

The measurement side does not require a large toolchain. Since 6.7.8.0 Shopware provides a dedicated endpoint for queue statistics: GET /api/_info/message-stats.json replaces the older GET /api/_info/queue.json, which has been marked as deprecated together with the increment-based statistics and is scheduled for removal in 6.8.0.0. The vendor explains the change with the old values often being inaccurate due to hardcoded multipliers and missing decrements in edge cases (Shopware Release Notes 6.7.8.0). Anyone alerting against queue.json today should plan the switch - including the equally deprecated options shopware.admin_worker.enable_queue_stats_worker and shopware.increment.message_queue (Shopware Release Notes 6.7.8.0).

  • Waiting messages per queue - separated by async, low_priority, custom transports and the failure transport
  • Age of the oldest waiting message per queue - the actual leading indicator in operations
  • Throughput in messages per minute, again per transport
  • Number of running worker processes and their uptime since the last restart
  • Growth of the failure transport per hour as a separate counter
  • Time from order placement to confirmation dispatch as a business metric - the only figure on this list that everybody outside IT understands as well

OPcache after deployment

With opcache.validate_timestamps=0, PHP no longer checks file changes - a recommendation from the Shopware performance notes that requires an active cache reset during deployment (Shopware Developer Documentation, Performance Tweaks). Without that step, web processes and workers keep running on stale bytecode.

Workers running old code

A worker loads the code at startup. Without messenger:stop-workers after deployment it processes messages with the previous version - producing error patterns that cannot be reproduced locally, because the new state is running there long since (Symfony Documentation, Messenger).

Duplicated cron jobs

A cron entry from the initial installation plus a systemd service from the relaunch: both consume the same queue, nobody knows the process count any more, and under load the runs overtake each other (Shopware Developer Documentation, Message Queue). An inventory of all background processes belongs in the operations documentation.

Scheduled tasks without a consumer

The scheduler queues tasks, but no messenger:consume processes them. The scheduled_task table fills up, the storefront shows stale data. Since Shopware 6.7.2.0 the state can be inspected directly with scheduled-task:list (Shopware Developer Documentation, Scheduled Task).

How these metrics fit into a coherent picture of availability, response times and error rates is described in our article on shop monitoring for uptime and performance. Two further operational topics reach directly into the queue: HTTP cache invalidation, which depends on a scheduled task and which we classify in the article on the cache rework and HTTP cache in Shopware, and securing the interfaces through which many of these messages are created in the first place - covered by the article on securing the Store API in headless shops.

The queue belongs in operations, not in a browser tab

Asynchronous processing is not a side topic in Shopware 6 but the path along which order confirmations, search index, sitemap, feeds and cleanup jobs travel. The out-of-the-box state with admin worker and database transport is meant for development - the vendor says so itself (Shopware Developer Documentation, Message Queue). The transition into production consists of a few clearly nameable steps: admin worker off, workers as a service with time and memory limits, separate queues for mail, indexing and secondary work, a dedicated consumer for the failure transport, an orderly restart after every deployment and two metrics in monitoring. None of these steps is expensive - the state before them is.

XICTRON sets up this configuration for Shopware shops and takes over day-to-day operations: we disable the admin worker, register the workers as system services with suitable limits, separate the queues by business urgency, connect the failure transport to alerting and integrate the orderly restart into the deployment. Whether the database transport is sufficient or a dedicated broker makes sense is decided on measured message volume rather than by template. Talk to us via the contact page, have the current state assessed as part of a consulting engagement, or take a look at what our hosting and maintenance covers. For changes inside the shop itself - custom message types, handlers, routing - our Shopware development is available.

Sources and studies

This article is based on the Shopware Developer Documentation - Hosting/Infrastructure: Message Queue and Scheduled Task as well as Hosting/Performance: Performance Tweaks (developer.shopware.com) -, the Shopware Documentation on Message Queue and Scheduled Tasks (docs.shopware.com), the shopware/docs GitHub repository with the files guides/hosting/infrastructure/message-queue.md and guides/hosting/infrastructure/scheduled-task.md, the Shopware Release Notes 6.7.8.0 (developer.shopware.com/release-notes) and the Symfony Documentation on Messenger: Sync and Queued Message Handling (symfony.com/doc/current/messenger.html). The legal classification of the order confirmation is based on Section 312i BGB as published by the German Federal Ministry of Justice (gesetze-im-internet.de). Figures marked as (project experience) come from our own operations projects. Version and configuration details may change with later vendor releases.

Frequently asked questions about the Shopware message queue

As a rule, yes. The vendor documentation explicitly recommends disabling the admin worker in shopware.yaml once the CLI worker is configured (Shopware Developer Documentation, Message Queue). If both run in parallel they compete for the same messages, and the CPU load caused by open Administrations remains - even though the services have already taken over the work.

A blanket number would typically be misleading. Shopware itself refrains from a general recommendation and points out that the suitable count depends on the volume and type of messages - product indexing is slow, other messages are very fast (Shopware Developer Documentation, Message Queue). The bundled systemd template shows three instances as an example. It usually makes sense to start with two or three instances, measure queue length and age, and adjust from there.

It generally works, but it has a documented catch: cron knows no upper limit for concurrently running workers and does not wait for a previous process to finish. If messages take longer than the configured time limit, an unintended and growing number of workers can result (Shopware Developer Documentation, Message Queue). For the scheduler scheduled-task:run --no-wait, on the other hand, a cron entry is common and sensible.

Because page delivery and background processing are two separate systems. If mail dispatch is configured over the message bus, the confirmation depends on a running consumer (Shopware Developer Documentation, Message Queue). Without a CLI worker it is only sent once somebody logs into the Administration. In Germany this is legally relevant, because Section 312i (1) sentence 1 no. 3 BGB requires confirmation of receipt of the order without undue delay by electronic means (Federal Ministry of Justice, Section 312i BGB).

They are moved to the failure transport and typically redelivered three times; if processing fails again, the message is deleted (Shopware Developer Documentation, Message Queue). That is why a dedicated consumer belongs to this transport as well, and its growth should be monitored. With messenger:failed:show, messenger:failed:retry and messenger:failed:remove the entries can be inspected, redelivered or removed (Symfony Documentation, Messenger).

Experience suggests doing so only when measurements call for it. Shopware uses the Doctrine transport by default, describes it as a good choice for development and recommends a specialised service for production that relieves the database (Shopware Developer Documentation, Message Queue). In practice the switch typically pays off as soon as several workers run in parallel, bulk imports happen regularly, or the database is already working at its write limit.