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

# PHP performance tuning

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 VariableBlock = ({name}) => {
  return <var spellCheck={false} title={`Replace '${name}' with your own data`}>{name}</var>;
};

export const DisclaimerNix = () => <Tip>
    You can now use composable image to install runtimes and tools in your application container. To find out more, see the <a href="/docs/configure-apps/app-reference/composable-image">Composable image</a> topic.
  </Tip>;

<DisclaimerNix />

Once your app is up and running it still needs to be kept fast.
Upsun offers a wide degree of flexibility in how PHP behaves,
but that does mean you may need to take a few steps to ensure your site is running optimally.

The following recommendations are guidelines only.
They're also listed in about the order to investigate them.

## Upgrade to PHP 8

To make a PHP-based site run faster, the first step is to upgrade the PHP version.
Upgrading the PHP version might require changes to your app.
For more details and recommendations, see the [PHP migration guides](https://www.php.net/manual/en/migration80.php).

To change your PHP version, change the [`type` in your app configuration](/docs/configure-apps/app-reference/single-runtime-image#type).
Before merging to production, test the change on a branch and make sure that your app is working as expected.

## Optimize the FPM worker count

PHP-FPM uses a fixed number of simultaneous worker processes to handle incoming requests.
If more simultaneous requests are received than the number of workers,
then some requests wait until worker processes are available.

The default worker count is set to a conservative default value.
To determine and set the optimal value for your app, see [PHP-FPM sizing](/docs/languages/php/fpm).

## OPcache preloading

OPcache preloading loads selected files into shared memory,
making their content (functions, classes) globally available for requests.
It also removes the need to include these files later.
When OPcache is correctly configured, it can result in significant improvements to both CPU and memory usage.

Consult your framework's documentation to see
if there are recommendations for optimal preload configuration or ready-to-use preload scripts.

OPcache is only available on PHP 7.4+ and uses PHP-CGI.
If your PHP version doesn't support OPcache, this is a good reason to upgrade.

Note that the only way to clear the preload cache is by [restarting PHP-FPM](#restart-php-fpm).

If you have [disabled OPcache timestamp validation](#disable-opcache-timestamp-validation),
you need to clear the OPcache explicitly on deployment (which can be done by restarting PHP-FPM).

### Enable OPcache preloading

To enable preloading, add a variable that specifies a preload script:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          type: 'php:{{version:php:latest}}'
          variables:
            php:
              opcache.preload: '<PRELOAD_SCRIPT>'`
  }
</DynamicCodeBlock>

`<PRELOAD_SCRIPT>` is a file path relative to the [app root](/docs/configure-apps/app-reference/single-runtime-image#root-directory).
It may be any PHP script that calls `opcache_compile_file()`.

The following example uses a `preload.php` file as the preload script.
This script loads all `.php` files in the `vendor` directory (and subdirectories):

```php preload.php theme={null}
<?php
$directory = new RecursiveDirectoryIterator(getenv('PLATFORM_APP_DIR') . '/vendor');
$iterator = new RecursiveIteratorIterator($directory);
$regex = new RegexIterator($iterator, '/^.+\.php$/i', RecursiveRegexIterator::GET_MATCH);

foreach ($regex as $key => $file) {
    // This is the important part!
    opcache_compile_file($file[0]);
}
```

### Configure OPcache

OPcache needs to be tuned before production usage and can be configured the [same way as PHP](/docs/languages/php#customize-php-settings).

Let the app run for a while before tuning OPcache
since the preload script may change some of the configuration.

#### Set the maximum number of cached files

`opcache.max_accelerated_files` is the maximum number of files that OPcache can cache at once.
If this value is lower than the number of files in the app,
the cache becomes less effective because it starts [thrashing](https://en.wikipedia.org/wiki/Thrashing_\(computer_science\)).

To determine the maximum number of files to cache, follow these steps:

1. Connect to the container via SSH using the [CLI](/docs/development/ssh)
   by running `upsun ssh`.

2. Determine roughly how many `.php` files your app has by running this command from [your app root](/docs/configure-apps/app-reference/single-runtime-image#root-directory):

   ```bash theme={null}
   find . -type f -name '*.php' | wc -l
   ```

   Note that the returned valued is an approximation.
   Some apps have PHP code in files that don't end in `.php` or files that are generated at runtime.

3. Set `opcache.max_accelerated_files` to a value slightly higher than the returned number.
   PHP automatically rounds the value up to the next highest prime number.

An example configuration:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          type: 'php:{{version:php:latest}}'
          variables:
            php:
              'opcache.max_accelerated_files': 22000`
  }
</DynamicCodeBlock>

#### Set memory consumption

`opcache.memory_consumption` is the total memory (in megabytes) that OPcache can use with FastCGI.
If the app uses more than this, the cache starts [thrashing](https://en.wikipedia.org/wiki/Thrashing_\(computer_science\)) and becomes less effective.

Determining the optimal limit to memory consumption requires executing code via a web request to get adequate statistics.
[CacheTool](https://github.com/gordalina/cachetool) is an open-source tool to help you get the statistics.

To determine the total amount of memory to use, follow these steps:

1. Connect to the container via SSH using the [CLI](/docs/development/ssh)
   by running `upsun ssh`.
2. Change to the `/tmp` directory (or any other non-web-accessible writable directory) with `cd /tmp`.
3. Download CacheTool with `curl -sLO https://github.com/gordalina/cachetool/releases/latest/download/cachetool.phar`.
4. Make CacheTool executable with `chmod +x cachetool.phar`.
5. Check the OPcache status for FastCGI commands by running the following command:

   ```bash theme={null}
   php cachetool.phar opcache:status --fcgi=$SOCKET
   ```

   The `--fcgi=$SOCKET` option ensures the PHP-FPM process on the server connects through the right socket.
6. Analyze the output to determine the optimal value for `opcache.memory_consumption`.
   The most important values from CacheTool's output are the following:

   * `Memory used`
   * `Memory free`
   * `Oom restarts` (out of memory restarts)
     If the value is different than 0, you don't have enough memory allocated to OPcache.

   If `Memory free` is too low or `Oom Restarts` too high,
   set a higher value for memory consumption.
7. Set `opcache.memory_consumption`.
   Note: The unit for `opcache.memory_consumption` is megabytes.

   An example configuration:

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          type: 'php:{{version:php:latest}}'
          variables:
            php:
              'opcache.memory_consumption': 96`
  }
</DynamicCodeBlock>

8. [Restart PHP-FPM](#restart-php-fpm) and make sure that OPcache works as expected by rerunning CacheTool
   with the following command:

   ```bash theme={null}
   php cachetool.phar opcache:status --fcgi=$SOCKET
   ```

9. Remove CacheTool by deleting the `cachetools.phar` file with `rm -rf cachetools.phar`.

### Disable OPcache timestamp validation

By default, OPcache checks that the cached version of a file is always up-to-date.
This means that every time a cached file is used, OPcache compares it to the file on disk.
If that file has changed, it gets reloaded and re-cached.
This allows to support apps that generate compiled PHP code from user configuration.

If you know your code isn't going to change outside a new deployment,
you can disable that check and get a small performance improvement.

Timestamp validation can be disabled by adding the following variable to your [app configuration](/docs/configure-apps):

<DynamicCodeBlock language="yaml" filename=".upsun/config.yaml">
  {`
      applications:
        myapp:
          type: 'php:{{version:php:latest}}'
          variables:
          php:
            'opcache.validate_timestamps': 0`
  }
</DynamicCodeBlock>

When you have disabled OPcache timestamp validation,
you need to explicitly clear OPcache on deployment by [restarting PHP-FPM](#restart-php-fpm).

Note: If your app generates PHP code at runtime based on user configuration, don't disable timestamp validation.
Doing so would prevent updates to the generated code from being loaded.

## Restart PHP-FPM

To force a restart of PHP-FPM:

1. Connect to your app container via SSH using the [CLI](/docs/development/ssh) by running `upsun ssh`.
2. Run `pkill -f -u "$(whoami)" php-fpm`.

## Optimize your code

To optimize your app, consider using a [profiler](/docs/observability/application-metrics).
A profiler helps determine what slow spots can be found and addressed and helps improve performance.
