---
title: "Consume Queen messages in Laravel with Artisan"
description: "Hand the messages other services push to a Queen queue to a PHP class with php artisan queen:consume, outside Laravel's job system: the handler, the ack and nack, the lease and its renewal, the options, and what stops the command."
---

> Queen MQ documentation, for AI agents
> Complete self-contained summary of Queen MQ: https://queenmq.com/llms-brief.txt
> Fetch that first when the question is about the product rather than about this page.
> Index of all pages: https://queenmq.com/llms.txt

# Consume Queen messages in Laravel with Artisan

`php artisan queen:consume` reads a Queen queue and passes each message to a class of yours. Use it
for messages that other services push to Queen. Laravel jobs keep going through `queue:work`
([Laravel](/guides/laravel/)).

The command connects with the settings of `config/queen.php`, as the queue connection does
(`QUEEN_URL`, `QUEEN_BEARER_TOKEN`). The service provider registers it for the console only. This
page describes PHP client 2.3.0 and later; [before 2.3.0](#before-php-client-230) lists
what differs in earlier releases.

## Try it

1. **Write a handler.** Any class with a `handle()` method.

```php
<?php

namespace App\Queen;

use Illuminate\Support\Facades\Log;

final class RecordPayment
{
    public function handle(array $message): void
    {
        // $message['data'] is the payload the producer pushed.
        Log::info('payment received', $message['data']);
    }
}
```

2. **Start the command.** Quote the class name, or the shell removes its backslashes.

```bash
php artisan queen:consume payments \
  'App\Queen\RecordPayment' --group=ledger --auto-ack
```

```text
Starting Queen consumer on queue: payments
Consumer group: ledger
Consumer subscribed. Waiting for messages... (Ctrl+C to stop)
```

3. **Push a message**, from `php artisan tinker` or from another application.

```php
Queen::queue('payments')
    ->push(['data' => ['paymentId' => 'p-1']])
    ->execute();
```

Start the command before the first push. A new group starts with the messages pushed after its
first pop, unless you pass `--subscription-mode=all`
([where a new group starts](/concepts/consuming/#where-a-new-group-starts)).

## The handler

- **Built by the container.** The command resolves the class with `app()`, so constructor injection
  works. One instance handles every message of the run.
- **One message or a list.** `handle()` gets one message array. With `--batch` above 1 it gets a
  list of up to that many messages.
- **The message.** `$message['data']` is the payload. `transactionId`, `partitionId`, `leaseId`,
  `deliveryAttempt` and the other keys are in the
  [PHP client reference](https://github.com/queen-mq/queen/blob/master/clients/client-php/REFERENCE.md#pop).
- **Exceptions.** The command prints the exception's message as an error line, nacks the message
  (or the list) with `--auto-ack` only, then pops again. Laravel's exception handler does not see
  it.

## Ack and nack

| `handle()` | With `--auto-ack` | Without `--auto-ack` |
| --- | --- | --- |
| returns | the command acks the message | nothing is acked |
| throws | the command nacks it | nothing is nacked |

- **A nack spends a retry.** With `--auto-ack`, a message that always throws reaches the
  dead-letter queue once the queue's retries are spent
  ([retry budget](/concepts/consuming/#ack-nack-and-the-retry-budget)). The nack carries the
  exception's message as its error.
- **Without `--auto-ack`, the handler settles.** A throw is printed, then
  `Not nacked without --auto-ack`. The message comes back when its lease expires, which spends no
  retry. Nack it in the handler when a failure must count, as the SDK consumers expect.
- **Client-side.** The command acks after `handle()` returns, so delivery stays at-least-once.
- **One call per pop.** With `--batch`, one ack or nack covers the whole list.
- **Refusals are printed.** A refused call prints one warning, for example
  `Ack refused for 1 of 1 message: invalid or expired lease`.

Without `--auto-ack`, ack in the handler, with the group you passed:

```php
<?php

namespace App\Queen;

use Queen\Laravel\QueenFacade as Queen;

final class RecordPayment
{
    public function handle(array $message): void
    {
        // ... the work ...
        Queen::ack($message, true, ['group' => 'ledger']);
    }
}
```

Without `--group`, leave `group` out of the context. With `--auto-ack`, leave the ack to the
command.

## Lease

Every pop asks for a lease of `--lease` seconds, by default `retry_after` of `config/queen.php`
(90). Keep `handle()` well inside it, or turn on renewal.

- **Renewal.** With `lease_renewal` true in `config/queen.php` and `--auto-ack`, the command renews
  the lease while `handle()` runs, with the renewer of `queue:work`
  ([lease renewal and fencing](/guides/laravel/concepts/#lease-renewal-and-fencing)).
- **Timing.** The `lease_renewal_*` keys apply, measured against `--lease`. A timing that does not
  fit stops the command at start, with exit code 1
  ([lease renewal keys](/guides/laravel/configuration/#lease-renewal)).
- **Fencing.** When a renewal cannot finish in time, the renewer sends the command SIGTERM, then
  SIGKILL after the kill grace, before the lease ends.
- **Before each ack or nack.** The command checks the lease and stops its renewal. A lease that is
  no longer safe is neither acked nor nacked: the command prints `Not acked:` (or `Not nacked:`)
  and the reason, and the broker hands the message out again.
- **Without `--auto-ack`.** Renewal stays off, and the command prints one line to say so. A
  handler that acks by itself releases the lease, and renewing a released lease stops the process.
- **Long polls.** The command counts the lease from the start of each pop. Keep `--timeout` well
  below `--lease`: a message that arrives late in a long poll can leave too little lease to track,
  and the command then stops with exit code 1.

## Options

| Option | Default | Effect |
| --- | --- | --- |
| `--group=` | none | The consumer group. Unset: queue mode. |
| `--auto-ack` | off | Ack after `handle()` returns. |
| `--batch=` | `1` | Messages per pop. Above 1: a list. |
| `--partitions=` | the broker | Partitions per pop, under one lease. |
| `--no-autopilot` | off | Unset `--partitions` means 1. |
| `--lease=` | `retry_after` | The lease of each pop, in seconds. |
| `--timeout=` | `30000` | Long poll of one pop, in ms. |
| `--subscription-mode=` | the broker | `new` or `all`, for a new group. |
| `--subscription-from=` | none | `now` or an ISO time, for a new group. |
| `--conflation` | off | The newest message per partition only. |
| `--limit=` | none | Stop after N messages handed out. |
| `--idle-timeout=` | none | Stop after N ms without a message. |

- **`--partitions`.** The partitions of one pop share the `--batch` budget.
- **`--no-autopilot`.** The broker stops choosing the partitions. `QUEEN_SDK_POP_AUTOPILOT=off` does
  the same for the whole process.
- **`--lease`.** An integer from 1 to 2147483647. Another value stops the command with exit code 1.
- **`--conflation`.** It needs `--group` and broker 1.1.0 or later. It works with `--auto-ack`.
- **`--limit`.** It counts every message handed to `handle()`, failed ones too. A pop asks for at
  most the messages the limit leaves, so the command never handles more than N.
- **`--idle-timeout`.** Counted from the start, then from the end of the last `handle()`. A long
  poll never waits past it. The command prints `No message for N ms (--idle-timeout): stopping.`
  and exits with code 0.

## Before production

- **`--group` has no default.** The command does not read `QUEEN_CONSUMER_GROUP`, which is the
  group of the queue connection.
- **No restart on deploy.** The command does not watch `queue:restart`. Restart it with your
  process manager.

## Stop and exit codes

`SIGINT` (Ctrl+C) and `SIGTERM` stop the command after the pop or the message in progress. It then
prints `Consumer stopped. Processed N messages.` This needs `pcntl`, and the handlers the command
installs replace yours for those two signals.

A pop that times out, or fails with a connection error, counts as an empty pop. After a connection
error the command waits 1 s before the next pop. It also prints `Queen broker unreachable:` and the
error, again at most every 30 s while it lasts, and `Queen broker reachable again after N s.` once
a pop gets an answer.

| Exit code | When |
| --- | --- |
| `0` | stopped by a signal, `--limit` or `--idle-timeout` |
| `1` | the handler class does not exist or has no `handle()` |
| `1` | an invalid lease, or a renewal timing that does not fit it |
| `1` | an error left the loop; Laravel reports it |

Errors that leave the loop include:

- **`ConflationUnsupportedException`.** `--conflation` against a broker older than 1.1.0.
- **`ConflationPolicyMismatchException`.** The group conflates, and the command has no
  `--conflation`.
- **`HttpException`.** The broker refused the pop, for example with a 403.

## Before PHP client 2.3.0

- **The nack carried no error.** With `--auto-ack`, a throw was nacked without the exception's
  message.
- **No `--lease` and no renewal.** Every pop got the queue's own lease time.
- **`--idle-timeout` had no effect.** The command ran until a signal or `--limit`.
- **`--limit` counted only successes.** With `--batch` it could handle up to `--batch` minus 1
  messages more than N.
- **Nothing was printed** for a refused ack or for a broker the pops could not reach.

The command is a loop around the client's
[KafkaConsumer-style consumer](https://github.com/queen-mq/queen/blob/master/clients/client-php/REFERENCE.md#the-kafkaconsumer-style-consumer).
Write your own loop with `Queen::queue('payments')->getConsumer()` when you need another ack policy.

Source: https://queenmq.com/guides/laravel/consume/index.mdx
