> ## 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.

# Offload Heavy Jobs to Upsun Task Containers, Triggered by Cron

> Move long, resource-hungry jobs out of your crons and into on-demand Upsun task containers. Learn how to define a task, authorize your app to trigger it, and fire it from a lightweight cron for database imports or search reindexing.

export const PostMeta = ({data = {}}) => {
  const {author, date} = data;
  const authors = Array.isArray(author) ? author : author ? [author] : [];
  const toSlug = value => String(value).toLowerCase().trim().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
  const resolveAuthor = slug => {
    const entry = AUTHOR_MAP[slug] || ({});
    const name = entry.name || slug;
    const github = entry.github || null;
    const url = `/posts/authors/${toSlug(slug)}`;
    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 (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",
      "github": "jackrabbithanna"
    },
    "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",
      "linkedin": "https://www.linkedin.com/in/laurent-arnoud-861b44121/"
    },
    "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"
    },
    "maz-mohammadi": {
      "name": "Maz Mohammadi"
    },
    "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} 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>}
    </div>;
};

<PostMeta data={{ author: ["flovntp"], date: "2026-08-20T09:00:00.000Z" }} />

> This is **Part 1** of the *Upsun Task Containers* series, where we build up — episode by episode — from a simple
> background job to a fleet of autonomous AI agents. In this first episode, no AI yet: we focus on the foundation, using
> a cron to trigger an on-demand [task container](/docs/configure-apps/tasks) for a heavy workload.

Every team eventually hits the same wall: a nightly job that *almost* fits in a cron. A database import, a full search
reindex, a bulk export — something that runs for several minutes, eats memory, and competes with the very application
that is supposed to be serving traffic. You bump the cron, you cross your fingers, and you hope it finishes before the
next schedule fires.

There is a cleaner way. Instead of running the heavy work *inside* your application container, you can keep the cron
tiny and let it **trigger a dedicated [task container](/docs/configure-apps/tasks)** — an on-demand, run-to-completion
workload with its own image, its own resources, and its own timeout. The cron becomes a trigger; the task does the work.

<Note>
  Task containers are currently in **prerelease**. To enable them on your project,
  [open this prepopulated support ticket](https://console.upsun.com/-/users/-/tickets/open?isGeneral=true\&category=feature_request_cat\&priority=normal\&subject=Enable%20task%20containers%20\(prerelease\)%20on%20my%20project\&description=Hello%2C%20I%27d%20like%20to%20enable%20task%20containers%20on%20my%20project.%0A%0AProject%20ID%3A%20%3CPROJECT_ID%3E)
  and add your project ID before submitting.
</Note>

## Prerequisites

Before starting, make sure you have:

* An [Upsun account](https://auth.upsun.com/register) with task containers enabled (see the note above)
* The [Upsun CLI](/cli/install#install-the-upsun-cli) installed
* An existing application deployed on Upsun (this guide uses a PHP/Symfony app, but the pattern is language-agnostic)
* A heavy application command you want to offload (for example `bin/console app:reindex` or a database import script)

## Why not just run it in the cron?

[Crons](/docs/configure-apps/image-properties/crons) are the right tool for *scheduling*, but they run **inside your
application container**. That means the heavy job shares CPU and memory with the process serving HTTP traffic, and it is
bound by the app's resource profile. A long reindex can starve your web requests, and a deploy will cancel a running
cron mid-flight.

A [task container](/docs/configure-apps/tasks) flips the model. It is a separate, on-demand workload that is injected
into your environment's cluster only while it runs, then removed when the command exits. Here is how the four workload
types compare:

|                         | Web            | Worker               | Cron                         | Task                         |
| ----------------------- | -------------- | -------------------- | ---------------------------- | ---------------------------- |
| Lifecycle               | Always running | Always running       | Scheduled, run-to-completion | On-demand, run-to-completion |
| HTTP exposure           | Yes            | No                   | No                           | No                           |
| Has its own image/build | Yes            | No (shares app slug) | No (shares app slug)         | Yes                          |
| Triggered by            | Deploy         | Deploy               | Schedule                     | API call                     |
| Dedicated resources     | Yes            | Yes                  | No (uses app)                | Yes                          |

The pattern we build in this episode keeps the best of both worlds: a **lightweight cron** does nothing but fire an
**API call**, and a **task container** with its own resources does the heavy lifting.

<img src="https://mintcdn.com/upsun-c9761871/JXiGrz01JTAExqYV/images/posts/tutorials/task-containers/upsun-task-flow.png?fit=max&auto=format&n=JXiGrz01JTAExqYV&q=85&s=fa532f9c1b5ce817f5b27a501d4a4b60" alt="Upsun Task Container Flow" width="2760" height="1800" data-path="images/posts/tutorials/task-containers/upsun-task-flow.png" />

Notice how the heavy CPU and memory live entirely in the task container's **own** profile: the application container
stays small and responsive, while the reindex gets the resources it needs only for the seconds it runs.

## Step 1 - Define the task

Tasks are declared at the top level of `.upsun/config.yaml`, alongside `applications:` and `services:`. Let's define a
task that runs a heavy search reindex command against the same database the app uses.

```yaml title=".upsun/config.yaml" theme={null}
applications:
  app:
    type: php:8.5
    relationships:
      database: {}
    # ...

services:
  database:
    type: postgresql:18

tasks:
  reindex:
    source:
      root: /
    type: php:8.5
    run:
      command: php bin/console app:reindex --no-interaction
      timeout: 3600
    relationships:
      database: {}
    mounts:
      "/var/cache": { source: instance, source_path: var/cache }
```

A few things worth calling out:

* **`type`** uses the same runtime syntax as an application. Here the task reuses the app's PHP image so the
  application command and its dependencies are available.
* **`run.command`** is the heavy job. It runs to completion, not as a daemon. If you need setup steps that require
  service access, chain them at the start of the command, for example `php bin/console cache:warmup && php bin/console app:reindex`.
* **`run.timeout`** caps the run at one hour (the default). The maximum is `86400` (one day). On timeout, the platform
  sends `SIGTERM`, then `SIGKILL` after a short grace period.
* **`relationships`** give the task its own connection to the database. Inside the task container, these appear in
  `PLATFORM_RELATIONSHIPS` exactly as they do for an application.
* **`mounts`** declare writable directories. Remember that `instance` and `tmp` mounts are **reset between runs** — use
  a `storage` or service mount if you need to persist data across runs.

<Tip>
  A task gets its **own** resources, configured against the task name with `upsun resources:set`. Give the reindex task
  the CPU and memory it needs without inflating your always-on application — billing is per-second for the duration of
  each run.
</Tip>

## Step 2 - Authorize the app to trigger the task

A task does not trigger itself. Something has to call the Upsun API — in our case, a cron running in the application
container. For that, the application needs permission to operate the task. Declare it with a
[workload authorization](/docs/configure-apps/image-properties/authorizations):

```yaml title=".upsun/config.yaml" theme={null}
applications:
  app:
    type: php:8.5
    relationships:
      database: {}
    authorizations:
      - type: task        # allow the app to trigger and operate this task
        resource: reindex
        action: operate
```

With this in place, the application container can request a short-lived token and call the task run endpoint — no
long-lived credentials stored anywhere.

## Step 3 - Trigger the task from a cron

Now the lightweight part. The cron does not run the reindex itself; it mints a token and POSTs to the task run endpoint.
Every Upsun container exposes a local credential broker at `http://localhost:8200/oauth2/token` that exchanges the
container's ambient credentials for a short-lived access token.

Add a small trigger script to your repository:

```bash title="bin/trigger-reindex.sh" theme={null}
#!/usr/bin/env bash
set -euo pipefail

# Mint a short-lived access token from the container-local credential broker.
token="$(curl -fsS http://localhost:8200/oauth2/token \
  -d grant_type=client_credentials | jq -r .access_token)"

# Fire the task. PLATFORM_PROJECT and PLATFORM_BRANCH are injected by Upsun.
curl -fsS -X POST \
  -H "Authorization: Bearer ${token}" \
  "https://api.upsun.com/projects/${PLATFORM_PROJECT}/environments/${PLATFORM_BRANCH}/tasks/reindex/run"
```

Then wire it into a [cron](/docs/configure-apps/image-properties/crons) that runs on schedule but returns in
milliseconds:

```yaml title=".upsun/config.yaml" theme={null}
applications:
  app:
    type: php:8.5
    # ...
    crons:
      nightly-reindex:
        # Every night at 02:30 — only on the production environment.
        spec: '30 2 * * *'
        commands:
          start: |
            if [ "$PLATFORM_ENVIRONMENT_TYPE" = "production" ]; then
              bash bin/trigger-reindex.sh
            fi
```

The cron itself is now trivial: it makes one HTTP call and exits. The reindex runs in its own container, with its own
resources, completely decoupled from the app serving traffic.

<Note>
  Multiple task runs can execute in parallel up to a default limit of **3**. Further triggers queue behind running ones,
  so a cron that fires while a previous run is still going will not pile up unbounded.
</Note>

## Step 4 - Pass run-time variables

Often the same task needs to do slightly different work depending on when it runs. Pass run-time variables in the POST
body and read them as environment variables inside the task:

```bash title="bin/trigger-reindex.sh" theme={null}
curl -fsS -X POST \
  -H "Authorization: Bearer ${token}" \
  -H "Content-Type: application/json" \
  -d '{"variables": {"env": {"BATCH_SIZE": "500", "INDEX": "catalog"}}}' \
  "https://api.upsun.com/projects/${PLATFORM_PROJECT}/environments/${PLATFORM_BRANCH}/tasks/reindex/run"
```

Inside the task, `BATCH_SIZE` and `INDEX` are available as regular environment variables your command can read. In a
Symfony command, that is simply `$_ENV` (or `getenv()`):

```php title="src/Command/ReindexCommand.php" theme={null}
protected function execute(InputInterface $input, OutputInterface $output): int
{
    // Run-time variables passed by the trigger arrive as environment variables.
    $batchSize = (int) ($_ENV['BATCH_SIZE'] ?? 100);
    $index     = $_ENV['INDEX'] ?? 'default';

    $output->writeln(sprintf('Reindexing "%s" in batches of %d…', $index, $batchSize));

    // … your reindex logic here …

    return Command::SUCCESS;
}
```

<Warning>
  Only use `variables` for **non-sensitive** values. Set secrets such as API keys or passwords ahead of time with the CLI
  so they stay out of version control and request bodies, for example:

  ```bash title="Terminal" theme={null}
  upsun variable:create --level environment --name env:SOME_SECRET --sensitive true
  ```
</Warning>

## Step 5 - Deploy and observe

Commit your configuration and push:

```bash title="Terminal" theme={null}
git add .upsun/config.yaml bin/trigger-reindex.sh
git commit -m "Offload reindex to an on-demand task container"
upsun push
```

Each task invocation creates an **activity** — the same mechanism used for deploys, backups, and crons — which gives you
a unique activity ID, live status (`pending`, `in_progress`, `complete`, `cancelled`), and streamed logs. You can watch
the activity feed while the task runs:

```bash title="Terminal" theme={null}
upsun activity:list --type environment.task -e main
```

You can also trigger the task manually to test it, without waiting for the cron, by running the same script over SSH:

```bash title="Terminal" theme={null}
upsun ssh -e main "bash bin/trigger-reindex.sh"
```

## Wrapping up

You have moved a heavy, long-running job out of your application's critical path. The cron stays lightweight and
predictable, while the actual work runs in a dedicated [task container](/docs/configure-apps/tasks) with its own image,
resources, and timeout — billed only for the seconds it runs.

This is the foundation for everything that follows. In **Part 2**, we keep the exact same cron-triggers-a-task pattern,
but instead of a database command we launch an **OpenCode AI agent** that reads your cron and application logs and
writes you a health report. See you in the next episode.
