safely controlled during deployment
Cron jobs and queue consumers still running during a deployment are an underestimated source of errors. A consumer that runs against the old release code while the symlink switches to the new release can cause data consistency problems. This article explains how to reliably control cron and consumers in the GitLab deployment process.
Table of Contents
- 1. The problem with cron and consumers during deployment
- 2. Magento cron: how it works and what disrupts it
- 3. Queue consumers: Supervisor, systemd, and manual control
- 4. Stopping cron and consumers before deployment
- 5. Waiting for running consumers, graceful shutdown
- 6. Symlink switch and Magento steps in the right order
- 7. Restarting cron and consumers after deployment
- 8. Comparison: uncontrolled vs. controlled
- 9. Typical failure patterns with cron and consumers during deployment
- 10. Summary
- 11. FAQ
1. The problem with cron and consumers during deployment
Magento deployments have a phase in which the system is briefly in an inconsistent state: the symlink already points to the new release, but processes that were started in the old context keep running. Cron jobs, which run every minute, can start at exactly this moment and access code from the new release while working with configurations or database states that are still tuned to the old release. Queue consumers, which run continuously, are even more problematic: they load classes at startup but use them for minutes or hours afterward.
The good news is that the problem is solvable if it is treated as an explicit step in the deployment process. The mistake many teams make is taking cron and consumers for granted and performing the symlink switch without first making sure that none of these processes are currently active. The result is sporadic errors after deployments, hard to reproduce because they depend on timing. The solution is a clear protocol: stop before the release, wait until all processes have ended, deploy, then restart.
2. Magento cron: how it works and what disrupts it
Magento uses two cron processes: cron:run (processes scheduled jobs) and cron:run --group=index (reindexing). Both are typically invoked through the system crontab. The problem during deployment is that the crontab keeps running and automatically starts the next cron run regardless of what the deployment process is currently doing. If setup:upgrade is running and a cron job is performing database access at the same time, the two can collide, up to and including deadlocks in Magento's own scheduling management.
The simplest way to control Magento cron during deployment is bin/magento cron:remove before the deployment (removes the crontab entry), then the actual deployment steps, then bin/magento cron:install afterward (re-registers the crontab entry). This approach stops cron cleanly without abruptly cancelling running jobs: every job that has already started runs to completion, but no new ones are started. For the duration of the deployment, that is an acceptable trade off between data safety and a minimal maintenance window.
3. Queue consumers: Supervisor, systemd, and manual control
Queue consumers in Magento are usually kept running continuously through a process manager. The two most common approaches are Supervisor and systemd. Supervisor manages processes through a configuration file and offers commands such as supervisorctl stop all and supervisorctl start all. systemd manages services through unit files and offers systemctl stop magento-consumer@* and systemctl start. In both cases, controlling these processes from the deployment pipeline is much easier than with manually started consumer processes that have no process manager at all.
Anyone starting consumers without a process manager, for example directly via the crontab or manual SSH sessions, has fallen into the most common anti pattern in Magento deployments: processes running in the background that nobody can count reliably and nobody can stop reliably. The process manager matters not only for stability monitoring but also for deployment control: a defined stop command, a defined start command, a definable wait state. Without a process manager, the deployment script has to stop consumers itself via pkill or similar, which is error prone because consumers do not always share the same process name.
# GitLab CI/CD deploy job with controlled cron and consumer shutdown
deploy:production:
stage: deploy
environment: production
only:
- tags
before_script:
- chmod 600 "$SSH_PRIVATE_KEY"
- eval "$(ssh-agent -s)"
- ssh-add "$SSH_PRIVATE_KEY"
- echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
script:
# Phase 1: Stop background processes before deployment
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" bash <<'REMOTE'
set -euo pipefail
# Remove Magento cron entries, running jobs finish, no new ones start
php "$DEPLOY_PATH/current/bin/magento" cron:remove || true
# Stop all supervised consumers gracefully
supervisorctl stop all 2>/dev/null || systemctl stop "magento-consumer@*" 2>/dev/null || true
echo "Background processes stopped"
REMOTE
# Phase 2: Wait for active consumers to finish (max 60 seconds)
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" bash <<'REMOTE'
set -euo pipefail
TIMEOUT=60
ELAPSED=0
while pgrep -f "queue:consumers:start" > /dev/null 2>&1; do
if [ "$ELAPSED" -ge "$TIMEOUT" ]; then
echo "WARNING: Consumers still running after ${TIMEOUT}s, proceeding anyway"
break
fi
echo "Waiting for consumers to finish (${ELAPSED}s / ${TIMEOUT}s)..."
sleep 5
ELAPSED=$((ELAPSED + 5))
done
echo "All consumers stopped"
REMOTE
4. Stopping cron and consumers before deployment
Stopping cron and consumers is the phase where most teams invest too little time. The correct order is: first remove the cron entry (so no new cron run starts), then instruct the process manager to stop consumers. The difference between an immediate stop (supervisorctl stop sends SIGTERM) and a graceful shutdown matters: a consumer that is currently processing a queue message should ideally finish that processing before it terminates. Most queue systems in Magento are designed so that a message is redelivered if a process ends unexpectedly, but an abrupt SIGKILL can lead to inconsistent database states if the consumer is in the middle of a multi step write operation.
Supervisor offers stopwaitsecs for this in its configuration: after sending SIGTERM, the process manager waits at most this many seconds before sending SIGKILL. A sensible value for Magento consumers is 30 to 60 seconds. Anyone using systemd configures TimeoutStopSec=60 in the unit file. This wait time must be coordinated with the deployment script: the script should only continue once it is certain that all consumers have actually stopped, not merely that the stop command was issued.
5. Waiting for running consumers, graceful shutdown
There is a gap between the stop command and the actual end of all consumer processes. A wait loop in the deployment script closes that gap: as long as consumer processes are still running, the script waits, checks periodically, and after a timeout continues anyway with a warning. This timeout is important: a consumer stuck in a deadlock or waiting on an external system with its own timeout should not block the deployment indefinitely. After the timeout, either it escalates to SIGKILL or the deployment continues despite processes still running, with explicit logging so the operations team knows what happened.
The pattern for the wait loop is simple: pgrep -f "queue:consumers:start" checks whether consumer processes are still running. If so, wait 5 seconds and check again. Continue after the configured timeout. This loop belongs in the SSH script on the deployment server, not in the GitLab job, because otherwise the job would abort on a network timeout or a lost SSH connection. The loop running on the server itself is more stable and independent of the connection to the GitLab runner.
6. Symlink switch and Magento steps in the right order
After stopping cron and consumers comes the actual deployment sequence: transfer the artifact to the server, link shared directories, run setup:upgrade, deploy static content, switch the symlink to the new release, flush the cache. The critical question is: exactly when does the symlink switch happen? Ideally as late as possible, only after all Magento steps have completed on the new release directory. That means setup:upgrade and static content deployment run in the new release directory before the symlink from current points to the new directory.
This sequencing shrinks the inconsistency window down to the atomic symlink switch itself, typically milliseconds. All long running operations (setup:upgrade, static content deploy) run beforehand in the new directory while the currently served version still runs through the old symlink. This approach only works if database migrations are backward compatible (the expand contract pattern), because during this phase the old code version has to run against the new database structure. For simple releases without schema changes that is not a problem; complex migrations need a separate concept.
7. Restarting cron and consumers after deployment
After the symlink switch and the cache flush, cron and consumers need to be restarted, in the new release context. This happens in reverse order to stopping: first restart consumers (supervisorctl start all or systemctl start magento-consumer@*), then re-register cron (bin/magento cron:install). Restarting consumers through the process manager automatically loads the new code, because the current symlink now points to the new release.
One important detail on restart: if Magento's queue:consumers:start command has consumer options such as --max-messages or --batch-size, these must be stored in the process manager configuration, not in the deployment script. The deployment script is responsible for telling the process manager to restart, not for knowing consumer options. This separation keeps the deployment script simple and keeps consumer configuration centrally managed in the Supervisor or systemd configuration on the server.
8. Comparison: uncontrolled vs. controlled
The difference between a deployment without and with cron/consumer control is often invisible in normal releases, and only becomes visible with edge case timing that is rarely reproducible.
| Phase | Uncontrolled | Controlled | Risk |
|---|---|---|---|
| Before deployment | Cron keeps running | cron:remove | No cron start during setup:upgrade |
| Consumers | Consumers keep running | supervisorctl stop all | No consumer running against old code after symlink switch |
| Waiting | No wait | Wait loop + timeout | Deployment starts only once all consumers are done |
| After deployment | Manual restart | supervisorctl start + cron:install | Consumers guaranteed to start in the new release context |
| Rollback | Consumers on the wrong code | Stop, rollback, start | Consumers running against old release code after rollback |
The controlled variant means a short additional time window before and after the deployment, in the range of 30 to 90 seconds depending on consumer runtimes. This window is the trade off for the assurance that no processes are working in an inconsistent state between old and new release code. For most Magento projects that is an acceptable trade off, one that systematically eliminates the category of sporadic, hard to reproduce post deployment errors.
9. Typical failure patterns with cron and consumers during deployment
The most common failure pattern is a consumer deadlock after deployment: a consumer began processing a queue message on the old code, the symlink switched to the new release, and the consumer now accesses classes that existed in the old vendor state but live at different paths or with a different interface in the new one. The result is a fatal error or an unhandled exception; the message is put back on the queue, but the consumer process dies. Supervisor restarts it, it picks up the same message again, and the cycle starts over.
The second failure pattern is a cron conflict during setup:upgrade: setup:upgrade runs database migrations while a cron job simultaneously performs write access to the same tables. This can cause deadlocks in MySQL or an aborted setup:upgrade that leaves the database in an intermediate state. Diagnosis: check the MySQL slow query log and the Magento deployment log for overlapping timing. Prevention: run cron:remove before setup:upgrade, not just before the symlink switch.
# Complete deployment with cron and consumer lifecycle control
deploy:production:
stage: deploy
environment: production
script:
- |
ssh "$DEPLOY_USER@$DEPLOY_HOST" bash <<'REMOTE'
set -euo pipefail
CURRENT="$DEPLOY_PATH/current"
RELEASE="$DEPLOY_PATH/releases/$(date +%Y%m%d-%H%M%S)"
echo "=== Phase 1: Stop background processes ==="
php "$CURRENT/bin/magento" cron:remove || true
supervisorctl stop all 2>/dev/null || true
# Wait up to 60 seconds for consumers to finish
for i in $(seq 1 12); do
pgrep -f "queue:consumers:start" > /dev/null 2>&1 || break
echo "Waiting for consumers... ($((i*5))s)"
sleep 5
done
echo "=== Phase 2: Deploy release ==="
mkdir -p "$RELEASE"
rsync -az --delete /tmp/release/ "$RELEASE/"
ln -sfn "$DEPLOY_PATH/shared/app/etc/env.php" "$RELEASE/app/etc/env.php"
ln -sfn "$DEPLOY_PATH/shared/pub/media" "$RELEASE/pub/media"
php "$RELEASE/bin/magento" setup:upgrade --no-interaction
php "$RELEASE/bin/magento" setup:static-content:deploy de_DE -f
echo "=== Phase 3: Switch symlink ==="
ln -sfn "$RELEASE" "$CURRENT"
php "$CURRENT/bin/magento" cache:flush
echo "=== Phase 4: Restart background processes ==="
supervisorctl start all 2>/dev/null || true
php "$CURRENT/bin/magento" cron:install --force
echo "Deployment complete"
REMOTE
10. Summary
Controlling cron and queue consumers during deployment is not an optional step, it is a prerequisite for stable Magento releases. cron:remove before the deployment prevents cron starts during setup:upgrade. Process manager stop halts consumers before the symlink switches. A wait loop makes sure all processes have actually ended before continuing. After the deploy: process manager start and cron:install start cron and consumers in the new release context.
Investing in this protocol pays off with every release: no sporadic post deployment errors from timing conflicts, no consumer deadlock cycle, no cron/database conflicts during migrations. Combined with a clean release structure and an atomic symlink switch, it produces a deployment process that lives up to the promise of zero downtime, at least for the layer that concerns cron and consumers.
Cron and Consumers in Deployment: The Essentials at a Glance
Before deployment
cron:remove + supervisorctl stop all. Wait loop until all consumer processes have ended (max. 60s timeout).
Deployment sequence
Artifact, shared links, setup:upgrade, static content: all before the symlink switch. Switch the symlink as late as possible.
After deployment
supervisorctl start all + cron:install. Consumers start in the new release context, not the old one.
Process manager
Supervisor or systemd is mandatory, for controlled stop/start and graceful shutdown with a configurable timeout.