---
title: "Message bus"
manual: "TYPO3 Explained"
version: "main"
permalink: "https://docs.typo3.org/permalink/t3coreapi:message-bus@main"
source: "ApiOverview/MessageBus/Index.rst"
rendered: "2026-09-27T06:55:37+00:00"
---

# Message bus {#message-bus}

TYPO3 provides a message bus solution based on [symfony/messenger](https://symfony.com/doc/current/components/messenger.html). It has the
ability to send messages and then handle them immediately (synchronous) or
send them through transports (asynchronous, for example, queues) to be handled
later.

For backwards compatibility, the default implementation uses the synchronous
transport. This means that the message bus will behave exactly as before, but it
will be possible to switch to a different (asynchronous) transport on a
per-project base.

To offer asynchronicity, TYPO3 also provides a transport implementation based on
the [Doctrine DBAL messenger transport](https://github.com/symfony/doctrine-messenger) from Symfony and a basic implementation
of a consumer command.

> [!NOTE]
> **See also**
>
> To familiarize yourself with the concept, please also read the following
> resources:
>
> -   [The Symfony Messenger Component](https://symfony.com/doc/current/components/messenger.html)
> -   [Sync & Queued Message Handling](https://symfony.com/doc/current/messenger.html)
>
> More details and an example implementation are described in this blog post:
>
> -   [Message Bus and Message Queue in TYPO3](https://usetypo3.com/messages-in-typo3.html)

**Table of Contents**

-   ["Everyday" usage - as a developer](https://docs.typo3.org/permalink/t3coreapi:everyday-usage-as-a-developer@main)
-   ["Everyday" usage - as a system administrator/integrator](https://docs.typo3.org/permalink/t3coreapi:everyday-usage-as-a-system-administrator-integrator@main)
-   [Advanced usage](https://docs.typo3.org/permalink/t3coreapi:advanced-usage@main)

## "Everyday" usage - as a developer {#message-bus-everyday-usage}

### Dispatch a message {#message-bus-dispatch}

1.  Add a PHP class for your message object (which is an arbitrary PHP class)

    **EXT:my_extension/Classes/Queue/Message/DemoMessage.php**

    ```php
    <?php

    declare(strict_types=1);

    namespace MyVendor\MyExtension\Queue\Message;

    final class DemoMessage
    {
      public function __construct(
        public readonly string $content,
      ) {}
    }

    ```
1.  Inject the `MessageBusInterface` into your class and call the
    `dispatch()` method

    **EXT:my_extension/Classes/MyClass.php**

    ```php
    <?php

    declare(strict_types=1);

    namespace MyVendor\MyExtension;

    use MyVendor\MyExtension\Queue\Message\DemoMessage;
    use Symfony\Component\Messenger\MessageBusInterface;

    final class MyClass
    {
      public function __construct(
        private readonly MessageBusInterface $bus,
      ) {}

      public function doSomething(): void
      {
        // ...
        $this->bus->dispatch(new DemoMessage('test'));
        // ...
      }
    }

    ```

### Register a handler {#message-bus-handler}

Implement the handler class

**EXT:my_extension/Classes/Queue/Handler/DemoHandler.php**

```php
<?php

declare(strict_types=1);

namespace MyVendor\MyExtension\Queue\Handler;

use MyVendor\MyExtension\Queue\Message\DemoMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class DemoHandler
{
  public function __invoke(DemoMessage $message): void
  {
    // do something with $message
  }
}

```

The message handler can be registered using the symfony PHP attribute
`\Symfony\Component\Messenger\Attribute\AsMessageHandler`.

A `Services.yaml` entry is needed to use `before`/`after`
for registration if you need to define an order:

**EXT:my_extension/Configuration/Services.yaml**

```yaml
MyVendor\MyExtension\Queue\Handler\DemoHandler:
  tags:
    - name: 'messenger.message_handler'

# Define another handler which should be called before DemoHandler:
MyVendor\MyExtension\Queue\Handler\DemoHandler2:
  tags:
    - name: 'messenger.message_handler'
      before: 'MyVendor\MyExtension\Queue\Handler\DemoHandler'

```

## "Everyday" usage - as a system administrator/integrator {#message-bus-routing}

By default, TYPO3 will behave like in versions before TYPO3 v12. This means that
the message bus will use the synchronous transport and all messages will be
handled immediately. To benefit from the message bus, it is recommended to
switch to an asynchronous transport. Using asynchronous transports increases the
resilience of the system by decoupling external dependencies even further.

Currently, the TYPO3 Core provides an asynchronous transport based on the
[Doctrine DBAL messenger transport](https://github.com/symfony/doctrine-messenger). This transport is configured to use the
default TYPO3 database connection. It is pre-configured and can be used by
changing the settings:

**config/settings.php | config.additional.php**

```php
$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger']['routing']['*'] = 'doctrine';
```

This will route all messages to the asynchronous transport (mind the `*`).

> [!WARNING]
> **Attention**
>
> If you are using the Doctrine transport, make sure to take care of running
> the [consume command](https://docs.typo3.org/permalink/t3coreapi:message-bus-consume-command@main).

> [!NOTE]
> **See also**
>
> [$GLOBALS\['TYPO3_CONF_VARS'\]\['SYS'\]\['messenger'\]\['routing'\]](https://docs.typo3.org/permalink/t3coreapi:typo3confvars-sys-messenger-routing@main)

### Async message handling - the consume command {#message-bus-consume-command}

To consume messages, run the command:

**Composer-based installation**

```bash
vendor/bin/typo3 messenger:consume <receiver-name>
```

**Classic mode installation (no Composer)**

```bash
typo3/sysext/core/bin/typo3 messenger:consume <receiver-name>
```

By default, you should run:

**Composer-based installation**

```bash
vendor/bin/typo3 messenger:consume doctrine
```

**Classic mode installation (No Composer)**

```bash
typo3/sysext/core/bin/typo3 messenger:consume doctrine
```

The command is a slimmed-down wrapper for the Symfony command
`messenger:consume`, it only provides the basic consumption functionality. As
this command is running as a worker, it is stopped after 1 hour to avoid memory
leaks. Therefore, the command should be run from a service manager like
[systemd](https://en.wikipedia.org/wiki/Systemd) to restart automatically after the command exits due to the time
limit.

The following code provides an example for a service. Create the following
file on your server:

**/etc/systemd/system/typo3-message-consumer.service**

```ini
[Unit]
Description=Run the TYPO3 message consumer
Requires=mariadb.service
After=mariadb.service

[Service]
Type=simple
User=www-data
Group=www-data
ExecStart=/usr/bin/php8.1 /var/www/myproject/vendor/bin/typo3 messenger:consume doctrine --exit-code-on-limit 133
# Generally restart on error
Restart=on-failure
# Restart on exit code 133 (which is returned by the command when limits are reached)
RestartForceExitStatus=133
# ..but do not interpret exit code 133 as an error (as it's just a restart request)
SuccessExitStatus=133

[Install]
WantedBy=multi-user.target
```

## Advanced usage {#message-bus-advanced-usage}

### Configure a custom transport (senders/receivers) {#message-bus-custom-transport}

Transports are configured in the services configuration. To allow the
configuration of a transport per message, the TYPO3 configuration
([`settings.php`](../../Configuration/Typo3ConfVars/Index.md#file-project-config-system-settings-php), [`additional.php`](../../Configuration/Typo3ConfVars/Index.md#file-project-config-system-additional-php) on system level, or
[`ext_localconf.php`](../../ExtensionArchitecture/FileStructure/ExtLocalconf.md#file-extension-ext-localconf-php) in an extension) is utilized. The transport/sender
name used in the settings is resolved to a service that has been tagged with
`message.sender` and the respective identifier.

**config/settings.php | config/additional.php | EXT:my_extension/ext_localconf.php**

```php
<?php

$GLOBALS['TYPO3_CONF_VARS']['SYS']['messenger'] = [
  'routing' => [
    // Use "messenger.transport.demo" as transport for DemoMessage
    \MyVendor\MyExtension\Queue\Message\DemoMessage::class => 'demo',

    // Use "messenger.transport.default" as transport for all other messages
    '*' => 'default',
  ],
];

```

**EXT:my_extension/Configuration/Services.yaml | config/system/services.yaml**

```yaml
messenger.transport.demo:
  factory: [ '@TYPO3\CMS\Core\Messenger\DoctrineTransportFactory', 'createTransport' ]
  class: 'Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport'
  arguments:
    $options:
      queue_name: 'demo'
  tags:
    - name: 'messenger.sender'
      identifier: 'demo'
    - name: 'messenger.receiver'
      identifier: 'demo'

messenger.transport.default:
  factory: [ '@Symfony\Component\Messenger\Transport\InMemory\InMemoryTransportFactory', 'createTransport' ]
  class: 'Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport'
  arguments:
    $dsn: 'in-memory://default'
    $options: [ ]
  tags:
    - name: 'messenger.sender'
      identifier: 'default'
    - name: 'messenger.receiver'
      identifier: 'default'

```

The TYPO3 Core has been tested with three transports:

-   `\Symfony\Component\Messenger\Transport\Sync\SyncTransport`
    (default)
-   `\Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport`
    (using the Doctrine DBAL messenger transport)
-   `\Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport`
    (for testing)

### Add rate limiter {#message-bus-add-rate-limiter}

Rate limiting can be applied to asynchronous messages processed through the
consume command. This allows controlling message processing rates to:

-   Stay within external service limits (API quotas, mail sending thresholds)
-   Manage server resource utilization

#### Example: usage of a rate limiter {#message-bus-example-rate-limiter}

Use the following configuration to limit the process of messages to
max. 100 each 60 seconds:

**EXT:my_extension/Configuration/Services.yaml | config/system/services.yaml**

```yaml
messenger.rate_limiter.limit_db_messages:
  class: Symfony\Component\RateLimiter\RateLimiterFactory
  arguments:
    $config:
      id: 'limit_db_messages'
      policy: 'sliding_window'
      limit: 100
      interval: '60 seconds'
    $storage: '@TYPO3\CMS\Core\RateLimiter\Storage\CachingFrameworkStorage'
  tags:
    - name: 'messenger.rate_limiter'
      identifier: 'doctrine'

```

> [!TIP]
> **Hint**
>
> As TYPO3 default transport for asynchronous messages is `doctrine` you also
> have to set the tags `identifier` to `doctrine`.

### `InMemoryTransport` for testing {#message-bus-in-memory-transport-testing}

The `InMemoryTransport` is a transport that should only be used while
testing.

**EXT:my_extension/Configuration/Services.yaml | config/system/services.yaml**

```yaml
messenger.transport.default:
  factory: [ '@Symfony\Component\Messenger\Transport\InMemory\InMemoryTransportFactory', 'createTransport' ]
  class: 'Symfony\Component\Messenger\Transport\InMemory\InMemoryTransport'
  public: true
  arguments:
    $dsn: 'in-memory://default'
    $options: [ ]
  tags:
    - name: 'messenger.sender'
      identifier: 'default'
    - name: 'messenger.receiver'
      identifier: 'default'

```

### Configure a custom middleware {#message-bus-configure-middleware}

The middleware is set up in the services configuration. By default, the
`\Symfony\Component\Messenger\Middleware\SendMessageMiddleware` and the
`\Symfony\Component\Messenger\Middleware\HandleMessageMiddleware` are
registered. See also the [Custom middleware](https://symfony.com/doc/current/components/messenger.html#bus) section in the Symfony
documentation.

To add your own middleware, tag it as `messenger.middleware` and set the
order using TYPO3's `before` and `after` ordering mechanism:

**EXT:my_extension/Configuration/Services.yaml | config/system/services.yaml**

```yaml
Symfony\Component\Messenger\Middleware\SendMessageMiddleware:
  arguments:
    $sendersLocator: '@Symfony\Component\Messenger\Transport\Sender\SendersLocatorInterface'
    $eventDispatcher: '@Psr\EventDispatcher\EventDispatcherInterface'
  tags:
    - { name: 'messenger.middleware' }

Symfony\Component\Messenger\Middleware\HandleMessageMiddleware:
  arguments:
    $handlersLocator: '@Symfony\Component\Messenger\Handler\HandlersLocatorInterface'
  tags:
    - name: 'messenger.middleware'
      after: 'Symfony\Component\Messenger\Middleware\SendMessageMiddleware'

```
