> ## Documentation Index
> Fetch the complete documentation index at: https://developer.upsun.com/llms.txt
> Use this file to discover all available pages before exploring further.

# That cron should be a worker

> Long-running crons block your deployments, steal resources from your app container, and create support tickets. Here's when to use workers instead.


export const PostMeta = ({data = {}}) => {
  const {author, date, image} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const linkedin = entry.linkedin || null;
    const url = github ? `https://github.com/${github}` : linkedin || null;
    const avatarUrl = github ? `https://github.com/${github}.png?size=64` : null;
    return {
      name,
      url,
      avatarUrl
    };
  };
  const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
  }) : null;
  if (!image && authors.length === 0 && !formattedDate) return null;
  const AUTHOR_MAP = {
    "aaron-collier": {
      "name": "Aaron Collier"
    },
    "aaron-dudenhofer": {
      "name": "Aaron Dudenhofer"
    },
    "aaron-porter": {
      "name": "Aaron Porter"
    },
    "adriaan-odendaal": {
      "name": "Adriaan Odendaal"
    },
    "ajmal": {
      "name": "Ajmal Siddiqui"
    },
    "akalipetis": {
      "name": "Antonis Kalipetis"
    },
    "alexander-varwijk": {
      "name": "Alexander Varwijk"
    },
    "alicia-bevilacqua": {
      "name": "Alicia Bevilacqua"
    },
    "amelie-deguerry": {
      "name": "Amelie Deguerry"
    },
    "anacidre": {
      "name": "Ana Cidre",
      "linkedin": "https://www.linkedin.com/in/ana-cidre"
    },
    "andoni": {
      "name": "Andoni Auzmendi"
    },
    "andrei-taranu": {
      "name": "Andrei (Alex) Taranu",
      "linkedin": "https://www.linkedin.com/in/andrei-alex-taranu/"
    },
    "andrew-baxter": {
      "name": "Andrew Baxter"
    },
    "andrew-melck": {
      "name": "Andrew Melck"
    },
    "antoine-crochet-damais": {
      "name": "Antoine Crochet Damais"
    },
    "augustin-delaporte": {
      "name": "Augustin Delaporte",
      "linkedin": "https://www.linkedin.com/in/augustindelaporte/"
    },
    "branislav-bujisic": {
      "name": "Branislav Bujisic"
    },
    "carl-smith": {
      "name": "Carl Smith"
    },
    "caroline-leroy": {
      "name": "Caroline Leroy"
    },
    "cati-mayer": {
      "name": "Cati Mayer"
    },
    "catplat": {
      "name": "C Trinkwon"
    },
    "ceelolulu": {
      "name": "Celeste van der Watt"
    },
    "chadwcarlson": {
      "name": "Chad Carlson",
      "github": "chadwcarlson",
      "linkedin": "https://www.linkedin.com/in/chadwcarlson"
    },
    "chris-ward": {
      "name": "Chris Ward"
    },
    "chris-yates": {
      "name": "Chris Yates"
    },
    "christian-sieber": {
      "name": "Christian Sieber"
    },
    "christopher-lockheardt": {
      "name": "Christopher Lockheardt"
    },
    "christopher-skene": {
      "name": "Christopher Skene"
    },
    "chuck-morgan": {
      "name": "Chuck Morgan"
    },
    "corey-dockendorf": {
      "name": "Corey Dockendorf"
    },
    "crell": {
      "name": "Crell"
    },
    "damz": {
      "name": "Damz"
    },
    "dan-morrison": {
      "name": "Dan Morrison"
    },
    "davidbonachera": {
      "name": "David Bonachera",
      "github": "davidbonachera",
      "linkedin": "https://www.linkedin.com/in/davidbonachera"
    },
    "dereliahmet1": {
      "name": "Ahmet Faruk Dereli"
    },
    "devicezero": {
      "name": "Jonas Kröger",
      "github": "devicezero",
      "linkedin": "https://www.linkedin.com/in/jonaskroeger/"
    },
    "doug-goldberg": {
      "name": "Doug Goldberg"
    },
    "duncan-naves": {
      "name": "Duncan Naves",
      "github": "duncannaves",
      "linkedin": "https://www.linkedin.com/in/duncan-naves-a94423aa"
    },
    "erika-bustamante": {
      "name": "Erika Bustamante"
    },
    "fabpot": {
      "name": "Fabien Potencier"
    },
    "flovntp": {
      "name": "Florent Huck",
      "github": "flovntp",
      "linkedin": "https://www.linkedin.com/in/florenthuck"
    },
    "fred-plais": {
      "name": "Fred Plais"
    },
    "gauthier-garnier": {
      "name": "Gauthier Garnier"
    },
    "gilzow": {
      "name": "Paul Gilzow"
    },
    "gmoigneu": {
      "name": "Guillaume Moigneu",
      "github": "gmoigneu",
      "linkedin": "https://www.linkedin.com/in/guillaumemoigneu/"
    },
    "gregqualls": {
      "name": "Greg Qualls"
    },
    "guguss": {
      "name": "Augustin Delaporte"
    },
    "haylee-millar": {
      "name": "Haylee Millar"
    },
    "ivana-kotur": {
      "name": "Ivana Kotur"
    },
    "jackrabbithanna": {
      "name": "Mark Hanna"
    },
    "jared-wright": {
      "name": "Jared Wright",
      "github": "jww-sh",
      "linkedin": "https://www.linkedin.com/in/jaredwaynewright"
    },
    "jessica-orozco": {
      "name": "Jessica Orozco"
    },
    "joey-stanford": {
      "name": "Joey Stanford"
    },
    "john-grubb": {
      "name": "John Grubb"
    },
    "jonas-kruger": {
      "name": "Jonas Kruger"
    },
    "kathryn-frazer": {
      "name": "Kathryn Frazer"
    },
    "kemiojo": {
      "name": "Kemi Elizabeth Ojogbede"
    },
    "kieronsambrook-smith": {
      "name": "Kieronsambrook Smith"
    },
    "laurent-arnoud": {
      "name": "Laurent Arnoud"
    },
    "letoya-boyne": {
      "name": "Letoya Boyne"
    },
    "lolautruche": {
      "name": "Jérôme Vieilledent"
    },
    "lyly-lepinay": {
      "name": "Lyly Lepinay"
    },
    "manauwar-alam": {
      "name": "Manauwar Alam"
    },
    "marc-antoine-porri": {
      "name": "Marc Antoine Porri"
    },
    "maria-antinkaapo": {
      "name": "Maria Antinkaapo"
    },
    "maria-de-anton": {
      "name": "Maria De Anton"
    },
    "mark-dorison": {
      "name": "Mark Dorison"
    },
    "markus-hausammann": {
      "name": "Markus Hausammann"
    },
    "mary-thomas": {
      "name": "Mary Thomas"
    },
    "mathias-bolt-lesniak": {
      "name": "Mathias Bolt Lesniak"
    },
    "mathieu-strauch": {
      "name": "Mathieu Strauch"
    },
    "matthias-van-woensel": {
      "name": "Matthias Van Woensel",
      "linkedin": "https://www.linkedin.com/in/matthias-van-woensel-267a069"
    },
    "michael-sharp": {
      "name": "Michael Sharp"
    },
    "mupsi": {
      "name": "Marine Gandy"
    },
    "natalie-harper": {
      "name": "Natalie Harper"
    },
    "ngommenginger": {
      "name": "Nicolas Gommenginger",
      "linkedin": "https://www.linkedin.com/in/nicolas-gommenginger"
    },
    "nicholas-bennison": {
      "name": "Nicholas Bennison"
    },
    "nicholas-vahalik": {
      "name": "Nicholas Vahalik"
    },
    "nick-hardiman": {
      "name": "Nick Hardiman"
    },
    "nickanderegg": {
      "name": "Nickanderegg"
    },
    "nicolas-grekas": {
      "name": "Nicolas Grekas",
      "github": "nicolas-grekas",
      "linkedin": "https://www.linkedin.com/in/nicolasgrekas/"
    },
    "niti-malwade": {
      "name": "Niti Malwade"
    },
    "opensocialteam": {
      "name": "Opensocialteam"
    },
    "ori-pekelman": {
      "name": "Ori Pekelman"
    },
    "otavio-santana": {
      "name": "Otavio Santana"
    },
    "palwandi": {
      "name": "Pawan Alwandi",
      "github": "pawpy",
      "linkedin": "https://www.linkedin.com/in/pawanalwandi"
    },
    "patrick-boest": {
      "name": "Patrick Boest"
    },
    "patrick-dawkins": {
      "name": "Patrick Dawkins",
      "github": "pjcdawkins",
      "linkedin": "https://www.linkedin.com/in/patrickdawkins"
    },
    "patrick-klima": {
      "name": "Patrick Klima"
    },
    "pjcdawkins": {
      "name": "Pjcdawkins"
    },
    "prineet-kaurbhurji": {
      "name": "Prineet Kaurbhurji"
    },
    "quentin-sinig": {
      "name": "Quentin Sinig"
    },
    "ralt": {
      "name": "Florian Margaine",
      "github": "ralt",
      "linkedin": "https://www.linkedin.com/in/florian-margaine-43971136"
    },
    "ramanathanramakrishnamurthy": {
      "name": "Ramanathanramakrishnamurthy"
    },
    "remi-lejeune": {
      "name": "Rémi Lejeune"
    },
    "ribel": {
      "name": "Taras Kruts"
    },
    "robert-douglass": {
      "name": "Robert Douglass"
    },
    "rudy-weber": {
      "name": "Rudy Weber"
    },
    "ryan-hicks": {
      "name": "Ryan Hicks"
    },
    "sabri-helal": {
      "name": "Sabri Helal"
    },
    "savannah-bergeron": {
      "name": "Savannah Bergeron"
    },
    "shannon-vettes": {
      "name": "Shannon Vettes"
    },
    "shawn-ogasawara": {
      "name": "Shawn Ogasawara",
      "linkedin": "https://www.linkedin.com/in/shawn-ogasawara-83a9a0/"
    },
    "shawna-spoor": {
      "name": "Shawna Spoor"
    },
    "shedrack-akintayo": {
      "name": "Shedrack Akintayo"
    },
    "simon-ruggier": {
      "name": "Simon Ruggier"
    },
    "sophie-van-der-kindere": {
      "name": "Sophie Van Der Kindere"
    },
    "stefanos-thampis": {
      "name": "Stefanos Thampis"
    },
    "stephen-weinberg": {
      "name": "Stephen Weinberg"
    },
    "sukhman-virk": {
      "name": "Sukhman Virk"
    },
    "sumaira-nazir": {
      "name": "Sumaira Nazir"
    },
    "sumer": {
      "name": "Sümer Cip"
    },
    "syed-raza": {
      "name": "Syed Raza"
    },
    "tamara-bacchia": {
      "name": "Tamara Bacchia"
    },
    "tara-arnold": {
      "name": "Tara Arnold"
    },
    "theosakamg": {
      "name": "Mickael Gaillard",
      "github": "theosakamg"
    },
    "thomasdiluccio": {
      "name": "Thomas di Luccio"
    },
    "tim-anderson": {
      "name": "Tim Anderson"
    },
    "tom-helmer-hansen": {
      "name": "Tom Helmer Hansen"
    },
    "tylermills": {
      "name": "Tyler Mills"
    },
    "upsun": {
      "name": "Upsun"
    },
    "veronika-tolkachova": {
      "name": "Veronika Tolkachova",
      "linkedin": "https://www.linkedin.com/in/veronika-tolkachova-169167a2"
    },
    "vince-parker": {
      "name": "Vince Parker"
    },
    "vinnie-russo": {
      "name": "Vincenzo Russo"
    },
    "vrobert78": {
      "name": "Vincent Robert",
      "github": "vrobert78",
      "linkedin": "https://www.linkedin.com/in/vincent-robert-498a883"
    },
    "yuriy-babenko": {
      "name": "Yuriy Babenko"
    },
    "yuriy-gerasimov": {
      "name": "Yuriy Gerasimov"
    }
  };
  return <div className="post-meta">
      {(authors.length > 0 || formattedDate) && <div className="post-meta-info">
          {authors.length > 0 && <div className="post-meta-authors">
              {authors.map(slug => {
    const {name, url, avatarUrl} = resolveAuthor(slug);
    const inner = <>
                    {avatarUrl && <img src={avatarUrl} alt={name} className="post-meta-avatar" />}
                    <span className="post-meta-author-name">{name}</span>
                  </>;
    return url ? <a key={slug} href={url} target="_blank" rel="noopener noreferrer" className="post-meta-author">
                    {inner}
                  </a> : <span key={slug} className="post-meta-author">{inner}</span>;
  })}
            </div>}
          {authors.length > 0 && formattedDate && <span className="post-meta-separator" aria-hidden="true">·</span>}
          {formattedDate && <span className="post-meta-date">{formattedDate}</span>}
        </div>}
      {image && <img src={image} alt="" className="post-meta-image" aria-hidden="true" />}
    </div>;
};

<PostMeta data={{ author: ["matthias-van-woensel"], date: "2026-03-04T13:12:00.000Z", image: "/images/posts/hands-on/that-cron-should-be-a-worker/that-cron-should-be-a-worker.webp" }} />

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:

```bash theme={null}
upsun activity:list --incomplete
```

```text theme={null}
+---------------+---------------------------+---------------------------------------------------+----------+-------------+--------+----------------+
| 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](https://fixed.docs.upsun.com/create-apps/image-properties/crons.html#cron-commands) 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](https://docs.upsun.com/create-apps/workers.html#workers-vs-cron-jobs) 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](/posts/hands-on/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.
