SYSTEM ARCHITECTURE

Scaling Shopware 6.7: Stop Using Your Database as a Queue

By Huzaifa Mustafa • 8 min read • February 17, 2026

The Problem

If you run a high-traffic Shopware 6 store, the default Doctrine DBAL transport for the Symfony Messenger is likely your first performance bottleneck. Every email, stock sync, and indexing task writes to the messenger_messages table. Under load, the polling queries that workers use to consume messages create lock contention. When traffic spikes, your database spends resources managing the queue instead of serving orders.

This guide provides a production-ready setup to switch to Redis Streams in Shopware 6.7, plus two real-world scenarios where this architectural change makes a measurable difference.

The Setup (Shopware 6.7)

Prerequisites

The symfony/redis-messenger package requires the PHP Redis extension. Install it first if you have not already:

pecl install redis

Or use your OS package manager, for example:

apt install php8.3-redis

Restart PHP-FPM after installation.

Step 1: Install the transport

composer require symfony/redis-messenger

Step 2: Configure your environment (.env.local)

Shopware defines three messenger transports: async, low_priority, and failed. Each has its own environment variable. Set the first two to Redis and keep failed on Doctrine so you can inspect dead-lettered messages via SQL.

Use dbindex=1 to isolate queue data from your cache (which typically uses database 0). The delete_after_ack and delete_after_reject flags prevent processed messages from accumulating in the stream indefinitely.

# .env.local
MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages?dbindex=1&delete_after_ack=true&delete_after_reject=true
MESSENGER_TRANSPORT_LOW_PRIORITY_DSN=redis://localhost:6379/messages_low_priority?dbindex=1&delete_after_ack=true&delete_after_reject=true
MESSENGER_TRANSPORT_FAILURE_DSN=doctrine://default?queue_name=failed&auto_setup=false

DSN format note: The path segment after the port is the stream name, not a database index. redis://localhost:6379/1/messages would create a stream named 1 with consumer group messages. Always use the dbindex query parameter instead.

That's it. No YAML override needed.

Shopware's built-in framework.yaml already defines the async, low_priority, and failed transports with the correct serializer (messenger.transport.symfony_serializer), retry strategy (3 retries, exponential backoff), and message routing. Setting the environment variables is all that is needed to swap the transport backend.

Shopware's default transport routing (no changes required):

# vendor/shopware/core/.../framework.yaml (read-only, do not edit)
routing:
    'Shopware\...\AsyncMessageInterface': async
    'Shopware\...\LowPriorityMessageInterface': low_priority
    'Symfony\...\SendEmailMessage': async

Step 3: Run the workers

Consume both transports. Use --time-limit and --memory-limit so workers restart cleanly and do not leak memory over time.

bin/console messenger:consume async low_priority --time-limit=60 --memory-limit=512M

Production Notes

Multi-worker deployments

When running multiple worker processes, each worker must have a unique consumer name within the same stream and group. Without this, messages can be delivered to multiple workers simultaneously. Set the consumer name via the DSN or use a per-process environment variable:

# Append consumer name to the DSN per worker instance
MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages?dbindex=1&delete_after_ack=true&consumer=worker-1

Why keep failed on Doctrine?

Failed messages are inspected and retried manually. Storing them in MySQL lets you query them with SQL, filter by error type, and retry selectively using bin/console messenger:failed:show and messenger:failed:retry. Redis works for this too, but you lose the queryability.

Real-World Use Cases

Scenario 1: Black Friday Checkout Surge

The problem: Thousands of users check out at the same time. The default MySQL transport triggers row-level lock contention on the messenger_messages table as workers poll for new messages. This leads to query timeouts and failed order processing.

The fix: Route the async messages triggered during checkout (order confirmation emails, stock decrements, indexing) to Redis. Redis Streams handle ingestion near-instantly. Background workers then consume these messages at a controlled pace, keeping the checkout flow responsive and free from database deadlocks.

Scenario 2: Bulk ERP Price Imports

The problem: Your ERP pushes 200,000 price updates through the API. Processing them synchronously either times out the HTTP request or degrades storefront performance for active shoppers.

The fix: Create a custom ImportPriceMessage and dispatch it from your controller to the Redis-backed queue. The controller returns a 202 Accepted response immediately. A dedicated worker consumes the queue and processes prices in batches, completely decoupled from the storefront.

A Note on Alternatives

RabbitMQ offers advantages when you need durable message persistence, complex routing, or dead-letter queue handling. Redis Streams work best when speed and simplicity are the priority and you already have Redis in your stack for caching.

Redis Streams

  • Near-instant message ingestion
  • Simple setup if Redis already in stack
  • Lower operational overhead

RabbitMQ

  • Durable message persistence
  • Complex routing patterns
  • Dead-letter queue handling

Key Takeaway

Redis is not just for caching. It is the backbone of a decoupled, scalable Shopware architecture. By moving your message queue off the database, you free up MySQL resources for what matters most: serving your customers.

Need Help Scaling Your Shopware Infrastructure?

Whether you're preparing for Black Friday traffic or optimizing your message queue architecture, get expert guidance from a Certified Shopware Developer.

Talk to an Engineer