Controlling Cron and Queue Consumers During Deployment
AI generated
CI/CD
.yml
GitLab · Magento · Cron · Queue Consumer · Deployment
Cron and Queue Consumers
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.

12 min read cron:run · queue:consumers:start · Supervisor · systemd Magento 2.4.x · GitLab 16.x

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.

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.

11. FAQ: Controlling Cron and Consumers in Deployment

1Why stop cron before deployment?
Prevents cron starts during setup:upgrade, which can cause database deadlocks. cron:remove removes the crontab entry; running jobs finish normally.
2cron:remove vs. maintenance:enable?
cron:remove only stops Magento cron. maintenance:enable blocks all web requests. cron:remove is the minimally invasive option with no maintenance window.
3Stopping consumers safely?
supervisorctl stop all (Supervisor) or systemctl stop magento-consumer@* (systemd). Then a wait loop until all processes have ended.
4What is a graceful shutdown?
Send SIGTERM, the consumer finishes the current message, then terminates itself. stopwaitsecs (Supervisor) / TimeoutStopSec (systemd) defines the max. wait time before SIGKILL.
5When to switch the symlink?
As late as possible, only after setup:upgrade and static content deploy. Minimizes the inconsistency window down to the atomic symlink switch itself.
6Restarting consumers after deployment?
supervisorctl start all. Consumers start in the new release context, the symlink now points to the new directory.
7Consumer will not stop, what now?
After the timeout (60s), continue the deployment with a warning. Or escalate to SIGKILL. Check the process manager configuration for stopwaitsecs.
8Is a process manager mandatory?
For production systems, yes. Without a process manager there is no controlled stop command. pkill is not a reliable substitute.
9Rollback with consumers?
Like a normal deployment: stop consumers, switch the symlink back, restart consumers. They then run against the old release code.
10How much time does stopping consumers cost?
With a 60s timeout and 30s consumer shutdown, about 90s extra. Trade off for safety against consumer deadlock cycles after deployment.