The email sending stops, and I don't know why

Your software
My Mautic version is: 7
My PHP version is: 8.3
My Database type and version is: MariaDB 10.11.14-MariaDB-0ubuntu0.24.04.1 Ubuntu 24.04
I installed Mautic via: Docker
I’m using Doctrine to queued process

Your problem
My problem is:

I’m experiencing an issue with Mautic 7 where campaign emails are not being sent to all eligible contacts, and I’d appreciate any guidance or insight.

I have a segment with ~15,000 contacts.
I created a campaign with a simple flow:

  • Segment → Send Email (delay 1 min)

I’m using staggered cron jobs as recommended:

0,15,30,45 * * * * php /var/www/html/bin/console mautic:segments:update
5,20,35,50 * * * * php /var/www/html/bin/console mautic:campaigns:update
10,25,40,55 * * * * php /var/www/html/bin/console mautic:campaigns:trigger 2>&1 | tee /tmp/stdout

Issue observed

  • Total contacts in campaign: 15,389

  • Contacts that received the email: 6,400

  • Contacts not sent: 8,989

:magnifying_glass_tilted_left: Database verification

  • All contacts reached the campaign event:
SELECT COUNT(*) FROM campaign_lead_event_log WHERE campaign_id = X;
-- Result: 15389
  • Only 6,400 records in email_stats:
SELECT COUNT(DISTINCT lead_id) FROM email_stats WHERE email_id = X;
-- Result: 6400

:red_question_mark: Key questions

  1. Is it possible that mautic:campaigns:trigger is not finishing before the next cron execution and causing incomplete processing?

  2. Could there be a locking issue where:

    • a previous trigger process is still running

    • next executions are skipped due to lock

    • and if the original process dies (memory/time), remaining contacts are never processed?

  3. Is there a known issue where:

    • campaign_lead_event_log is marked as executed

    • but email_stats is never created if the process fails mid-run?


:test_tube: Additional notes

  • No errors are clearly visible in logs yet (still investigating)

  • Using Docker setup with:

    • web

    • cron

    • worker (2 email consumers)


:folded_hands: What I need help with

  • Understanding why valid contacts are skipped

  • Whether this is expected behavior with large batches

  • Recommended way to ensure all contacts are processed reliably

  • Best practices for cron + queue setup (Doctrine vs Redis)

thanks

I found this error:
root@b31faa658a90:/var/www/html/var/logs# cat prod-2026-04-01.php [2026-04-01T21:12:21.289350+00:00] console.CRITICAL: Error thrown while running command "messenger:consume email". Message: "An exception occurred while executing a query: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction" {"exception":"[object] (Symfony\\Component\\Messenger\\Exception\\TransportException(code: 0): An exception occurred while executing a query: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction at /var/www/html/vendor/symfony/doctrine-messenger/Transport/Connection.php:251)\n[previous exception] [object] (Doctrine\\DBAL\\Exception\\DeadlockException(code: 1213): An exception occurred while executing a query: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction at /var/www/html/vendor/doctrine/dbal/src/Driver/API/MySQL/ExceptionConverter.php:39)\n[previous exception] [object] (Doctrine\\DBAL\\Driver\\PDO\\Exception(code: 1213): SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction at /var/www/html/vendor/doctrine/dbal/src/Driver/PDO/Exception.php:28)\n[previous exception] [object] (PDOException(code: 40001): SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction at /var/www/html/vendor/doctrine/dbal/src/Driver/PDO/Statement.php:130)","command":"messenger:consume email","message":"An exception occurred while executing a query: SQLSTATE[40001]: Serialization failure: 1213 Deadlock found when trying to get lock; try restarting transaction"} {"hostname":"b31faa658a90","pid":26}

is it related to Doctrine?

Probably 2 workers are trying to send the same message and they like cancel each other.
That’s why I only run only 1 worker at a time. But some might thing otherwise.

Cancel all your running workers and restart your sending.

Thanks **rcarabelli
**
do you think Doctrine could have problems? are you using doctrine ?

thanks

I use doctrine, yes, have a guide, will search for it and share it

thanks rcarabelli did you find the doctrine guide?

thanks

I originally wrote a guide for Mautic’s knowledge base, but it needs an update. Please use the following file and save it with this exact path and name:

/full_path_to_mautic/var/lock/consume_mautic.sh

Before using it, edit the configuration variables inside the script to match your environment. In particular, you will need to update:

  • the sending frequency settings
  • the path to your PHP binary
  • the path to your Mautic installation

You will also need to create the `lock` directory inside Mautic’s `var` directory if it does not already exist:

/full_path_to_mautic/var/lock/

After saving the file, make it executable from the terminal with:

chmod +x /full_path_to_mautic/var/lock/consume_mautic.sh

Then create a cron job to run it once per minute. For example:

* * * * * /full_path_to_mautic/var/lock/consume_mautic.sh > /dev/null 2>&1

You can change the log output part if you want to store logs instead of discarding them.

What does this file do?

This script replaces the usual `messenger:consume` cron job and gives you much more control over how email sending is processed in Mautic.

With this script, you can:

  • limit how many emails are processed per hour
  • control the pacing of email processing through delays between runs
  • restrict sending to specific days of the week
  • restrict sending to specific hours of the day
  • prevent overlapping executions by making sure only one worker runs at a time

In other words, it gives you a simple way to throttle email sending and avoid multiple workers running at the same time, which can be useful for controlling server load and keeping delivery behavior more predictable.

The file content starts here:

#!/bin/bash
set -euo pipefail

########################################

USER CONFIG

########################################

PHP_BIN=“/usr/bin/php8.3”

MAUTIC_ROOT=“/full_path_to_mautic”
MAUTIC_BIN=“$MAUTIC_ROOT/bin/console”
LOCK_DIR=“$MAUTIC_ROOT/var/lock”

LOCKFILE=“$LOCK_DIR/consume_mautic.lock”
STATEFILE=“$LOCK_DIR/consume_mautic_hourly.state”

LOCKFILE_TIMEOUT=50

1=Lun … 7=Dom

WORKING_DAYS=(“1” “2” “3” “4” “5” “6”)

Start time, end time

START_HOUR=6
END_HOUR=22

Cadence

INTERVAL=5
PER_BURST=1
HARD_LIMIT=50

Max per hour

HOURLY_LIMIT=500

Consumer limits

TIME_LIMIT=1
MEMORY_LIMIT=“8192M”

########################################

INTERNALS

########################################

mkdir -p “$LOCK_DIR”

current_hour_key() {
date +%Y%m%d%H
}

is_working_day() {
local dow
dow=$(date +%u)

for d in “${WORKING_DAYS[@]}”; do
if [ “$d” = “$dow” ]; then
return 0
fi
done

return 1
}

is_allowed_hour() {
local hour
hour=$(date +%H)
hour=$((10#$hour))

if [ “$hour” -lt “$START_HOUR” ] || [ “$hour” -ge “$END_HOUR” ]; then
return 1
fi

return 0
}

load_state() {
local now_hour
now_hour=$(current_hour_key)

if [ ! -f “$STATEFILE” ]; then
printf “%s|0\n” “$now_hour” > “$STATEFILE”
fi

local state_hour state_count
IFS=‘|’ read -r state_hour state_count < “$STATEFILE” || true

if [ -z “${state_hour:-}” ] || [ -z “${state_count:-}” ]; then
state_hour=“$now_hour”
state_count=0
printf “%s|%s\n” “$state_hour” “$state_count” > “$STATEFILE”
fi

if [ “$state_hour” != “$now_hour” ]; then
state_hour=“$now_hour”
state_count=0
printf “%s|%s\n” “$state_hour” “$state_count” > “$STATEFILE”
fi

STATE_HOUR=“$state_hour”
STATE_COUNT=“$state_count”
}

save_state() {
printf “%s|%s\n” “$STATE_HOUR” “$STATE_COUNT” > “$STATEFILE”
}

########################################

GUARDS

########################################

if ! is_working_day; then
echo “This is not an approved day. Exiting…”
exit 0
fi

if ! is_allowed_hour; then
echo “Outside of authorized time frame (${START_HOUR}:00-${END_HOUR}:00). Exiting…”
exit 0
fi

if [ -e “$LOCKFILE” ]; then
LOCKFILE_AGE=$(( $(date +%s) - $(stat -c %Y “$LOCKFILE”) ))
if [ “$LOCKFILE_AGE” -ge “$LOCKFILE_TIMEOUT” ]; then
echo “Lock > ${LOCKFILE_TIMEOUT}s. Deleting old lock file…”
rm -f “$LOCKFILE”
else
echo “Process already running. Exiting…”
exit 1
fi
fi

touch “$LOCKFILE”
trap ‘rm -f “$LOCKFILE”’ EXIT INT TERM

########################################

MAIN

########################################

load_state

if [ “$STATE_COUNT” -ge “$HOURLY_LIMIT” ]; then
echo “Hourly limit has been reached (${STATE_COUNT}/${HOURLY_LIMIT}) in the hour ${STATE_HOUR}. Exiting…”
exit 0
fi

SENT_REAL=0
START_TS=$(date +%s)

while (( $(date +%s) - START_TS < HARD_LIMIT )); do
load_state

if [ “$STATE_COUNT” -ge “$HOURLY_LIMIT” ]; then
echo “Límite horario alcanzado (${STATE_COUNT}/${HOURLY_LIMIT}).”
break
fi

if “$PHP_BIN” “$MAUTIC_BIN” messenger:consume email 
–limit=“$PER_BURST” 
–time-limit=“$TIME_LIMIT” 
–memory-limit=“$MEMORY_LIMIT” 
–no-interaction; then

STATE_COUNT=$(( STATE_COUNT + 1 ))
SENT_REAL=$(( SENT_REAL + 1 ))
save_state

echo "Processed: 1 | Hour: $STATE_HOUR | Accumulated on hour: $STATE_COUNT/$HOURLY_LIMIT"

else
echo “Messeger command returned:consume returned error. Exiting…”
break
fi

NOW=$(date +%s)
if (( NOW - START_TS + INTERVAL >= HARD_LIMIT )); then
break
fi

sleep “$INTERVAL”
done

echo “Processed in this run: $SENT_REAL”
exit 0