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

# Work with workers

> Interact with your worker instances to handle background tasks for your apps.

export const DynamicCodeBlock = ({language = 'yaml', filename, icon, lines, wrap, expandable, highlight, focus, children}) => {
  const STORAGE_KEY = 'upsun_versions_cache';
  const COMPOSABLE_STORAGE_KEY = 'upsun_composable_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = 'https://meta.upsun.com/images';
  const COMPOSABLE_API_URL = 'https://meta.upsun.com/composable';
  const DEBUG_PREFIX = '[DynamicCodeBlock cache]';
  const [versionData, setVersionData] = useState(null);
  const [versionError, setVersionError] = useState(false);
  const [composableData, setComposableData] = useState(null);
  const [composableError, setComposableError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) {
              return cachedData;
            }
          }
        } catch (err) {
          console.error('Failed to load from cache:', err);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      console.debug(`${DEBUG_PREFIX} revalidating`, {
        storageKey: STORAGE_KEY,
        hasCachedData: Boolean(cachedData),
        hasCachedEtag: Boolean(cachedEtag)
      });
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        console.debug(`${DEBUG_PREFIX} revalidated (304)`, {
          storageKey: STORAGE_KEY
        });
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (err) {
            console.error('Failed to refresh cache metadata:', err);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      console.debug(`${DEBUG_PREFIX} refreshed (200)`, {
        storageKey: STORAGE_KEY,
        etag
      });
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (err) {
          console.error('Failed to cache data:', err);
        }
      }
      return data;
    };
    fetchData().then(data => setVersionData(data)).catch(err => console.error('Failed to fetch version data:', err));
  }, []);
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  const getVersion = (lang, requestedVersion = 'latest') => {
    if (lang === 'composable') {
      if (!composableData || !composableData.versions || Object.keys(composableData.versions).length === 0) return null;
      if (requestedVersion && requestedVersion !== 'latest') {
        return (requestedVersion in composableData.versions) ? requestedVersion : null;
      }
      return findHighestVersion(composableData.versions);
    }
    if (!versionData) return null;
    const imageData = versionData[lang];
    if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
      return null;
    }
    if (requestedVersion && requestedVersion !== 'latest') {
      return (requestedVersion in imageData.versions) ? requestedVersion : null;
    }
    return findHighestVersion(imageData.versions);
  };
  let code = typeof children === 'string' ? children : String(children || '');
  const codeLines = code.split('\n');
  while (codeLines.length > 0 && codeLines[0].trim() === '') codeLines.shift();
  while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === '') codeLines.pop();
  if (codeLines.length > 0) {
    const indents = codeLines.filter(line => line.trim().length > 0).map(line => line.match(/^[ \t]*/)[0].length);
    const minIndent = Math.min(...indents);
    code = codeLines.map(line => line.slice(minIndent)).join('\n');
  }
  code = code.replace(/\{\{version:(.*?)\}\}/g, (match, params) => {
    const parts = params.split(':');
    const lang = parts[0];
    const ver = parts[1] || 'latest';
    const isComposable = lang === 'composable';
    const hasError = isComposable ? composableError : versionError;
    const dataReady = isComposable ? composableData !== null : versionData !== null;
    if (hasError) return '(unavailable)';
    if (dataReady) {
      const resolvedVersion = getVersion(lang, ver);
      return resolvedVersion || match;
    }
    return '...';
  });
  const codeBlockProps = {
    language,
    ...filename && ({
      filename
    }),
    ...icon && ({
      icon
    }),
    ...lines !== undefined && ({
      lines
    }),
    ...wrap !== undefined && ({
      wrap
    }),
    ...expandable !== undefined && ({
      expandable
    }),
    ...highlight && ({
      highlight
    }),
    ...focus && ({
      focus
    })
  };
  return <CodeBlock {...codeBlockProps}>{code}</CodeBlock>;
};

export const MetaImageVersion = ({language, version}) => {
  const [selectedVersion, setSelectedVersion] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const isComposable = language === 'composable';
  const STORAGE_KEY = isComposable ? 'upsun_composable_cache' : 'upsun_versions_cache';
  const CACHE_TTL = 5 * 60 * 1000;
  const API_URL = isComposable ? 'https://meta.upsun.com/composable' : 'https://meta.upsun.com/images';
  const findHighestVersion = versionsMap => {
    if (!versionsMap || Object.keys(versionsMap).length === 0) return null;
    const entries = Object.entries(versionsMap);
    const active = entries.filter(([, v]) => v.upsun && v.upsun.status === 'supported' || v.upsun && v.upsun.status === 'deprecated');
    const candidates = active.length > 0 ? active : entries;
    let [highestName] = candidates[0];
    for (let i = 1; i < candidates.length; i++) {
      const [currentName] = candidates[i];
      const cp = currentName.split('.').map(Number);
      const hp = highestName.split('.').map(Number);
      for (let j = 0; j < Math.max(cp.length, hp.length); j++) {
        if ((cp[j] || 0) > (hp[j] || 0)) {
          highestName = currentName;
          break;
        } else if ((cp[j] || 0) < (hp[j] || 0)) {
          break;
        }
      }
    }
    return highestName;
  };
  useEffect(() => {
    if (!language) {
      setLoading(false);
      return;
    }
    setLoading(true);
    setError(null);
    const fetchData = async () => {
      let cachedData = null;
      let cachedEtag = null;
      if (typeof localStorage !== 'undefined') {
        try {
          const cached = localStorage.getItem(STORAGE_KEY);
          if (cached) {
            const parsed = JSON.parse(cached);
            cachedData = parsed?.data || null;
            cachedEtag = parsed?.etag || null;
            if (cachedData && Date.now() - parsed.timestamp < CACHE_TTL) return cachedData;
          }
        } catch (error_) {
          console.error('Failed to load from cache:', error_);
        }
      }
      const requestHeaders = cachedEtag ? {
        'If-None-Match': cachedEtag
      } : {};
      const response = await fetch(API_URL, {
        headers: requestHeaders
      });
      if (response.status === 304 && cachedData) {
        if (typeof localStorage !== 'undefined') {
          try {
            const etag = response.headers.get('etag') || cachedEtag;
            localStorage.setItem(STORAGE_KEY, JSON.stringify({
              data: cachedData,
              etag,
              timestamp: Date.now()
            }));
          } catch (error_) {
            console.error('Failed to refresh cache metadata:', error_);
          }
        }
        return cachedData;
      }
      if (!response.ok) throw new Error(`API request failed: ${response.statusText}`);
      const data = await response.json();
      const etag = response.headers.get('etag');
      if (typeof localStorage !== 'undefined') {
        try {
          localStorage.setItem(STORAGE_KEY, JSON.stringify({
            data,
            etag,
            timestamp: Date.now()
          }));
        } catch (error_) {
          console.error('Failed to cache data:', error_);
        }
      }
      return data;
    };
    fetchData().then(data => {
      if (!data) {
        setSelectedVersion(null);
        setLoading(false);
        return;
      }
      const imageData = isComposable ? data : data[language];
      if (!imageData || !imageData.versions || Object.keys(imageData.versions).length === 0) {
        setSelectedVersion(null);
        setLoading(false);
        return;
      }
      let versionName = null;
      if (version && version !== 'latest') {
        versionName = (version in imageData.versions) ? version : null;
      } else {
        versionName = findHighestVersion(imageData.versions);
      }
      setSelectedVersion(versionName);
      setLoading(false);
    }).catch(error_ => {
      console.error('MetaImageVersion error:', error_);
      setError(error_.message);
      setLoading(false);
    });
  }, [language, version]);
  if (loading) return <span>…</span>;
  if (error) return <span title={error}>⚠ unavailable</span>;
  if (!selectedVersion) return <span>No version found</span>;
  return <span>{selectedVersion}</span>;
};

Workers are instances of your code that aren't open to connections from other apps or services or the outside world.
They're good for handling background tasks.
See how to [configure a worker](/docs/configure-apps/image-properties/workers) for your app.

## Access the worker container

Like with any other application container,
Upsun allows you to connect to the worker instance through SSH to inspect logs and interact with it.

Use the `--worker` switch in the Upsun CLI, like so:

```bash theme={null}
upsun ssh --worker=queue
```

## Stopping a worker

If a worker instance needs to be updated during a new deployment,
a `SIGTERM` signal is first sent to the worker process to allow it to shut down gracefully.
If your worker process can't be interrupted mid-task, make sure it reacts to `SIGTERM` to pause its work gracefully.

If the process is still running after 15 seconds, a `SIGKILL` message is sent that force-terminates the worker process,
allowing the container to be shut down and restarted.

To restart a worker manually, [access the container](#access-the-worker-container) and run the following commands:

```bash theme={null}
sv stop app
sv start app
```

## Workers vs cron jobs

Worker instances don't run cron jobs.
Instead, both worker instances and cron tasks address similar use cases.
They both address out-of-band work that an application needs to do
but that shouldn't or can't be done as part of a normal web request.
They do so in different ways and so are fit for different use cases.

A cron job is well suited for tasks when:

* They need to happen on a fixed schedule, not continually.
* The task itself isn't especially long, as a running cron job blocks a new deployment.
* It's long but can be divided into many small queued tasks.
* A delay between when a task is registered and when it actually happens is acceptable.

A dedicated worker instance is a better fit if:

* Tasks should happen "now", but not block a web request.
* Tasks are large enough that they risk blocking a deploy, even if they're subdivided.
* The task in question is a continually running process rather than a stream of discrete units of work.

The appropriateness of one approach over the other also varies by language;
single-threaded languages would benefit more from either cron or workers than a language with native multi-threading, for instance.
If a given task seems like it would run equally well as a worker or as a cron,
cron is generally more efficient as it doesn't require its own container.

## Commands

The `commands` key defines the command to launch the worker application.
For now there is only a single command, `start`, but more will be added in the future.
The `commands.start` property is required.

The `start` key specifies the command to use to launch your worker application.
It may be any valid shell command, although most often it runs a command in your application in the language of your application.
If the command specified by the `start` key terminates, it's restarted automatically.

Note that [`deploy` and `post_deploy` hooks](/docs/configure-apps/hooks) as well as [`cron` commands](/docs/configure-apps/image-properties/crons)
run only on the [`web`](/docs/configure-apps/image-properties/web) container, not on workers.

## Inheritance

Any top-level definitions for [`relationships`](/docs/configure-apps/image-properties/relationships),
[`access`](/docs/configure-apps/image-properties/access), [`mounts`](/docs/configure-apps/image-properties/mounts), and [`variables`](/docs/configure-apps/image-properties/variables)
are inherited by every worker, unless overridden explicitly.

Likewise [resources defined for the application container](/docs/manage-resources) are inherited by every worker, unless overridden explicitly.

That means, for example, that the following two `.upsun/config.yaml` definitions produce identical workers.

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp: #The name of the app, which must be unique within the project.
          type: python:{{version:python:latest}}
          mounts:
            test:
              source: storage
              source_path: test
          relationships:
            mysql:
          workers:
            queue:
              commands:
                start: |
                  python queue-worker.py
            mail:
              commands:
                start: |
                  python mail-worker.py
      services:
        mysql:
          type: mariadb:{{version:mariadb:latest}}`
  }
</DynamicCodeBlock>

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp: #The name of the app, which must be unique within the project.
          type: python:{{version:python:latest}}
          workers:
            queue:
                commands:
                  start: |
                    python queue-worker.py
                mounts:
                  test:
                    source: storage
                    source_path: test
                mysql:
            mail:
              commands:
                start: |
                  python mail-worker.py
              mounts:
                test:
                  source: storage
                  source_path: test
              mysql:
      services:
        mysql:
          type: mariadb:{{version:mariadb:latest}}`
  }
</DynamicCodeBlock>

In both cases, there are two worker instances named `queue` and `mail`.
Both have access to a MySQL/MariaDB service defined in `.upsun/config.yaml`,
through a [relationship](/docs/configure-apps/image-properties/relationships) that is identical to the *name* of that service (`mysql`).
Both also have their own separate [`storage` mount](/docs/configure-apps/image-properties/mounts).

## Customizing a worker

The most common properties to set in a worker to override the top-level settings are `variables` and its resources.
`variables` lets you instruct the application to run differently as a worker than as a web site,
whereas you can allocate [fewer worker-specific resources](/docs/manage-resources) for a container that is running only a single background process
(unlike the web site which is handling many requests at once).

For example, consider the following configuration:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {` applications:
    <APP_NAME>: #The name of the app, which must be unique within the project.
      type: "python:{{version:python:latest}}"
      hooks:
        build: |
          pip install -r requirements.txt
          pip install -e .
          pip install gunicorn
      relationships:
        mysql:
        rabbitmq:
      variables:
        env:
            type: 'none'
      web:
        commands:
          start: "gunicorn -b $PORT project.wsgi:application"
        variables:
          env:
            type: 'web'
        mounts:
          uploads:
            source: storage
            source_path: uploads
        locations:
          "/":
            root: ""
            passthru: true
            allow: false
          "/static":
            root: "static/"
            allow: true
      workers:
        queue:
          commands:
            start: |
              python queue-worker.py
          variables:
            env:
              type: 'worker'
          mounts:
            scratch:
              source: storage
              source_path: scratch
        mail:
          commands:
            start: |
              python mail-worker.py
          variables:
            env:
              type: 'worker'
          mounts: {}
          relationships:
            rabbitmq:
    services:
    mysql:
      type: 'mariadb:{{version:mariadb:latest}}'
    rabbitmq:
      type: 'rabbitmq:{{version:rabbitmq:latest}}'
    `}
</DynamicCodeBlock>

There's a lot going on here, but it's all reasonably straightforward.
The configuration in `.upsun/config.yaml` takes a single Python <MetaImageVersion language="python" /> code base from your repository,
downloads all dependencies in `requirements.txt`, and then installs Gunicorn.
That artifact (your code plus the downloaded dependencies) is deployed as three separate container instances, all running Python <MetaImageVersion language="python" />.

The `web` instance starts a Gunicorn process to serve a web application.

* It runs the Gunicorn process to serve web requests, defined by the `project/wsgi.py` file which contains an `application` definition.
* It has an environment variable named `TYPE` with value `web`.
* It has a writable mount at `/app/uploads`.
* It has access to both a MySQL database and a RabbitMQ server, both of which are defined in `.upsun/config.yaml`.

The `queue` instance is a worker that isn't web-accessible.

* It runs the `queue-worker.py` script, and restart it automatically if it ever terminates.
* It has an environment variable named `TYPE` with value `worker`.
* It has a writable mount at `/app/scratch`.
* It has access to both a MySQL database and a RabbitMQ server,
  both of which are defined in `.upsun/config.yaml` (because it doesn't specify otherwise).

The `mail` instance is a worker that isn't web-accessible.

* It runs the `mail-worker.py` script, and restart it automatically if it ever terminates.
* It has an environment variable named `TYPE` with value `worker`.
* It has no writable file mounts at all.
* It has access only to the RabbitMQ server, through a different relationship name than on the `web` instance.
  It has no access to MySQL.

<Note>
  Upsun automatically allocates [default resources](/docs/manage-resources/resource-init) to each instance,
  unless you [define a different resource initialization strategy](/docs/manage-resources/resource-init#specify-a-resource-initialization-strategy).
  You can also [adjust resources](/docs/manage-resources/adjust-resources) after your project has been deployed.
</Note>

For example, if you want the `web` instance to have a large upload space
and the `queue` instance to have a small amount of scratch space for temporary files,
you can allocate more CPU and RAM to the `web` instance than to the `queue` instance.

The `mail` instance has no persistent writable disk space at all, as it doesn't need it.
The `mail` instance also doesn't need any access to the SQL database, so for security reasons it has none.

Each instance can also check the `TYPE` environment variable to detect how it's running
and, if appropriate, adjust its behavior accordingly.

## Mounts

When defining a [worker](/docs/configure-apps/image-properties/workers) instance,
keep in mind what mount behavior you want.

`tmp` and `instance` local mounts are a separate storage area for each instance,
while `storage` mounts can be shared between instances.

For example, you can define a `storage` mount (called `shared_dir`) to be used by a `web` instance,
and a `tmp` mount (called `local_dir`) to be used by a `queue` worker instance:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp: #The name of the app, which must be unique within the project.
          type: "nodejs:{{version:nodejs:latest}}"

          # Define a web instance
          web:
            locations:
              "/":
                root: "public"
                passthru: true
                index: ['index.html']

          mounts:
            # Define a storage mount that's available to both instances together
            'shared_dir':
              source: storage
              service: files
              source_path: our_stuff

            # Define a local mount that's available to each instance separately
            'local_dir':
              source: tmp
              source_path: my_stuff

          # Define a worker instance from the same code but with a different start
          workers:
            queue:
              commands:
                start: ./start.sh`
  }
</DynamicCodeBlock>

Both the `web` instance and `queue` worker have their own, dedicated `local_dir` mount.
Note that:

* Each `local_dir` mount is a [`tmp` mount](/docs/configure-apps/image-properties/mounts) with a **maximum allocation of 8 GB**.<br />
* `tmp` mounts **may be removed** during infrastructure maintenance operations.

Both the `web` instance and `queue` worker also have a `shared_dir` mount pointing to the same network storage space.
They can both read and write to it simultaneously.
