Skip to main content
You’ve pushed a new feature hours ago, but the push activity still says it’s in “pending” state. The activity log shows Waiting for other activities to complete. What exactly is the push waiting on? Nine out of ten times, it’s a cron.

Diagnosing the problem

When you have activities that aren’t proceeding, you first need to identify what exactly is keeping everything stuck. In the Console you can see a list of all activities that are Running and Pending. The CLI can also give you an overview:
upsun activity:list --incomplete
+---------------+---------------------------+---------------------------------------------------+----------+-------------+--------+----------------+
| ID            | Created                   | Description                                       | Progress | State       | Result | Environment(s) |
+---------------+---------------------------+---------------------------------------------------+----------+-------------+--------+----------------+
| o42d2e53kaovq | 2024-03-05T03:08:53+00:00 | Platform.sh Bot created a backup of Master        | 0%       | pending     |        | master         |
| luowvmcyr3dt4 | 2024-03-04T13:05:12+00:00 | Platform.sh Bot ran cron send-publisher-email     | 0%       | pending     |        | master         |
| huak2o4ytoxjo | 2024-03-04T12:50:12+00:00 | Platform.sh Bot ran cron send-publisher-sugar-crm | 0%       | pending     |        | master         |
| xsr4ncsbvnfpu | 2024-03-04T12:50:12+00:00 | Platform.sh Bot ran cron send-publisher-email     | 0%       | in progress |        | master         |
+---------------+---------------------------+---------------------------------------------------+----------+-------------+--------+----------------+
Notice there’s only one activity in progress. The rest are all waiting on their turn. To figure out what that cron is doing, you have to dig a little deeper. Check the cron output first. Open the activity log in the Upsun Console and find the cron execution entry. It captures all the stdout and stderr output from your cron scripts. You might see something like “Processing record 40 of 100” and realize it’s still working through a batch. Or you might see the same error repeating in a loop, which tells you the cron is stuck and safe to kill. List processes. SSH into the environment and run ps faux to see what’s running. Crons tend to show up near the top. Check the CPU and memory usage columns: if both are at zero, the process is likely stuck waiting on something rather than actively processing data. Use strace for the brave. If you want to know exactly what a process is doing at the system call level, strace -p <pid> will show you. If it’s spamming write calls, it’s actively doing work. If it’s sitting on a single read call going nowhere, it’s probably stuck. This is power-user territory, but it’s often enough to tell you whether it’s safe to pull the plug. Kill the cron. Once you’ve decided the cron can be interrupted, you have two options. From the Console, use the “cancel activity” button on the cron entry. From the command line, SSH into the environment and kill the process directly with kill <pid>. Either way, the queued activities will start proceeding once the cron is gone.

Why crons block deployments

On Upsun, a cron and a deployment can’t run at the same time on the same environment. The platform waits for your cron to finish before it starts deploying. The reason is pretty pragmatic. A cron runs your code, and that code is often modifying data: updating database records, writing to the file system, processing files. If the platform killed it at 50% completion, you could end up with corrupt database entries or half-written files. Since the platform can’t know whether your cron is safe to interrupt (it’s your code, after all), it errs on the side of not destroying your data. That’s a reasonable trade-off until your cron takes 45 minutes and you’re trying to ship a hotfix.

Preventing is better than curing

If there’s one thing you take away from this article, let it be this: set a shutdown_timeout on your crons. In your .upsun/config.yaml, you can specify how long a cron is expected to run. If it exceeds that duration, the platform sends a SIGTERM to the process, giving it 10 seconds to clean up before sending SIGKILL. Without a shutdown timeout, crons can run indefinitely, and your deployments will wait indefinitely too. Think about how long your cron should reasonably take, then set the timeout a bit higher. If you expect it to finish in 5 minutes, set the timeout to 15 minutes. That gives you a safety net without being so tight that normal variance triggers a kill. You might have seen older recommendations to use the Linux timeout command in your cron script. Don’t do that. The YAML-based shutdown_timeout is the better approach because it integrates with the platform’s lifecycle management and lets you define the stop command that should be run to gracefully terminate your process. See the cron commands documentation for configuration details.

Should this be a cron at all?

Crons have been around for decades, and most of us reach for them out of habit. But it’s 2026, and if your cron runs every 5 or 10 minutes, what you actually have is a worker pretending to be a cron. Think about what happens every time a cron fires. The platform spins up the process, bootstraps your entire application framework, does some work, tears everything down, and then 5 minutes later does it all over again. That’s a lot of overhead for something that could be a single long-running process checking a queue. It gets worse when your cron takes longer than its interval. If you have a cron set to run every 5 minutes but it takes 10 minutes to complete, the next run can’t start until the first one finishes. So instead of running every 5 minutes, it runs back to back with no gap. At that point you’ve accidentally created the world’s least efficient worker. And crons aren’t free, even though they might look that way. They run inside your app container, sharing CPU and memory with the processes serving your web users. A heavy cron can degrade your site’s performance or, in extreme cases, cause outages. Moving that workload to a dedicated worker container keeps your app container focused on what it’s supposed to do: serve web requests. See the workers vs cron jobs documentation for a detailed comparison.

Workers handle deployments better too

When Upsun needs to deploy new code, it sends a SIGTERM to your worker process, giving it 30 seconds to finish what it’s doing and shut down gracefully. Your worker can catch that signal, wrap up its current task, and exit cleanly. Then the platform deploys the new code and starts the worker again. Compare that with crons, where the platform has no choice but to sit and wait for the cron to finish on its own. Workers give you a controlled shutdown mechanism; crons give you a “please finish whenever you’re ready.”

When crons are fine

Crons aren’t evil. They’re a poor fit for frequent, long-running tasks, but they’re perfectly reasonable for things that happen infrequently and finish quickly. A monthly cleanup script that takes 30 seconds? Great cron. A weekly report generation that runs for a minute? Perfectly fine. Something that needs to happen at a specific calendar time and doesn’t do heavy processing? That’s what crons are for. The line gets blurry somewhere around the hourly mark. If something runs every hour, it might not be worth spinning up a worker for it. But if you find yourself reaching for crons that run every few minutes, that’s a worker. No ambiguity there. One hybrid approach: keep your crons, but make them short. Instead of having a cron that processes 10,000 records, have the cron push a message to a queue and return immediately. A worker picks up the message and does the heavy lifting. Your cron takes milliseconds instead of minutes, and your deployments stay unblocked. For a deeper look at the queue-based approach, see Unstable website? Use queues.

Framework schedulers: the modern alternative

Many frameworks ship with built-in schedulers that blur the line between crons and workers in a good way. Laravel’s task scheduler and Symfony’s Scheduler component both let you define scheduled tasks in your application code rather than in YAML configuration. You run a single worker process that acts as a scheduler. It sits there in a loop, checking the clock, and fires off tasks at the times you’ve defined. From the platform’s perspective, it’s a worker. From your perspective, it feels like crons, but all the scheduling logic lives in your codebase where it belongs, not in infrastructure configuration.

The short version

If you’re seeing a lot of activities waiting, they’re most likely waiting on crons.
  • Diagnose the problem with upsun activity:list --incomplete.
  • At a bare minimum, add shutdown_timeout values to your cron configuration.
  • For anything that runs frequently or takes more than a few minutes, move it to a worker.
Last modified on April 14, 2026