Era Host hosting
EraHost – Free Domain, Cheap Hosting!
Client Area
Support 24/7
Menu

Configuring MySQL and the Web Server for OpenCart on a VPS

44 min read
11.09.2026

OpenCart on a VPS rarely becomes slow because of a single parameter. A store owner sees a slow category page, a 502 error during a traffic spike, or an import that appears to hang and starts increasing innodb_buffer_pool_size, pm.max_children, max_connections, and various timeouts one by one. Each change may look reasonable on its own, but together they can consume all available RAM and make the server less stable.

On an OpenCart VPS, the same memory is shared by Linux, MySQL, PHP-FPM, Nginx or Apache, the control panel, cron jobs, Redis, and other services. That is why MySQL cannot be sized independently from PHP-FPM, and the number of PHP workers cannot be chosen without considering the database. If the server is already swapping, a larger buffer pool will not make the store faster. If PHP-FPM is building a queue, increasing the MySQL cache will not fix that either.

A better workflow is to record the symptom and baseline metrics first, identify the layer where the delay appears, change one related parameter, and repeat the same test. For OpenCart, a useful order is: VPS resources, web server, PHP-FPM, application, MySQL, and disk I/O.

All numeric values below are examples of how to calculate limits, not universal recommendations. Two VPS instances with the same 8 GB of RAM may require completely different settings: one may run only OpenCart, while another also hosts a control panel, mail services, several websites, and Redis.

How many VPS resources are actually available to OpenCart before tuning?

Editing my.cnf before checking available resources is a poor starting point. First determine how much memory and CPU are actually left for MySQL and PHP-FPM. A slow OpenCart store with plenty of idle CPU may in fact be a server constantly pulling memory pages back from swap. In that situation the problem looks like poor database performance, while the database itself is waiting for storage.

Start with a quick snapshot of the VPS:

free -h
vmstat 1
df -h
lsblk
ps aux --sort=-%mem | head -20

free -h shows total RAM and swap usage. Do not focus only on the free column: Linux actively uses unused memory for cache, so available is usually more useful. In vmstat 1, pay particular attention to si and so. If swap-in or swap-out activity appears while the store is slowing down, the VPS is under memory pressure.

For storage diagnostics, the sysstat package and this command are useful:

iostat -xz 1

Do not judge the disk by %util alone. On SSD and NVMe storage, that value does not always describe saturation as clearly as it does on a single mechanical disk. Compare I/O latency, queue depth, and the exact moment OpenCart starts responding more slowly. If latency increases during a product import while the CPU remains mostly idle, the direction of the investigation becomes much clearer.

Also check whether the kernel has killed processes because of memory exhaustion:

journalctl -k | grep -Ei 'out of memory|oom|killed process'

If the log contains an OOM kill for php-fpm or mysqld, increasing memory-related limits without recalculating the entire configuration will only make the same failure happen again sooner.

Component How to check it What consumes RAM Risk of oversizing
Linux and system services free, ps, systemctl Kernel, filesystem cache, systemd services Without reserve memory, the server may start actively swapping
MySQL/InnoDB ps, SHOW VARIABLES Buffer pool, internal structures, connections MySQL can push PHP and filesystem cache out of RAM
PHP-FPM ps, FPM status PHP worker processes A high pm.max_children can consume several gigabytes under load
Nginx/Apache ps Workers, connections, modules Nginx is usually lightweight, while some Apache configurations can use much more memory
Redis redis-cli INFO memory Cached data stored in memory An unrestricted cache competes with MySQL and PHP for RAM
Control panel and other services ps aux --sort=-%mem Mail, antivirus, panels, background jobs They silently reduce the memory budget available to the store
Reserve free -h during peak load Traffic spikes, temporary jobs, OS cache No reserve makes the VPS unstable during short bursts

For an example VPS with 8 GB of RAM, do not start with the assumption that “MySQL can have 6 GB.” First measure how much the operating system actually uses, how much memory a working PHP process consumes, and how many PHP processes are needed concurrently. Only the remaining memory becomes a candidate for MySQL. For a broader methodology covering CPU, RAM, and storage measurements, see the guide to virtual server performance optimization. Calculate the memory budget first. Tune my.cnf second.

How do you check OpenCart, PHP, and MySQL compatibility before tuning?

An HTTP 500 error, a blank page, or module failures immediately after moving OpenCart to a VPS are poor reasons to start tuning MySQL. First make sure the OpenCart core, PHP-FPM version, PHP extensions, and database driver are compatible. Performance settings cannot fix code that fails on the selected PHP version.

The php -v command shows the CLI PHP version. It does not prove that the website is using the same interpreter. A VPS with a control panel or multiple websites may run several PHP-FPM pools: the shell uses one PHP version while the OpenCart virtual host sends requests to another socket.

php -v
php -m
php --ini
systemctl list-units 'php*-fpm.service'

With Nginx, inspect the actual fastcgi_pass value:

nginx -T | grep -n 'fastcgi_pass'

If the server block points to a socket such as /run/php/php8.x-fpm.sock, that PHP-FPM instance determines how OpenCart behaves over HTTP. Service names and socket paths vary by Linux distribution and installation method.

How can you determine the OpenCart version if the admin panel is unavailable?

The method depends on the OpenCart branch. On a working store, the easiest option is to check the version in the administration panel. If the admin area is unavailable, inspect the installed files instead of trusting the name of an old archive or migration directory. In several OpenCart branches the version is declared through the VERSION constant in startup PHP files, so a practical search can look like this:

grep -R "define('VERSION'" /var/www/opencart \
  --include='*.php' 2>/dev/null | head

This is a diagnostic technique, not a universal path that works for every release or third-party build. If nothing is found, do not guess the version. OpenCart directory structures have changed, and customized distributions may differ. Compatibility decisions should be based on the version that is actually installed.

How do you determine which PHP version actually runs OpenCart?

Compare the FPM configuration with a small request executed through the web server. Avoid leaving a full phpinfo() page publicly accessible because it exposes paths, modules, and many environment details. A temporary file with minimal output is enough:

<?php
echo PHP_VERSION . ' ' . PHP_SAPI;

Open the file through the same domain and delete it immediately afterward. If its output differs from php -v, you have two different PHP contexts. OpenCart must be diagnosed using the PHP-FPM context that actually handles HTTP requests.

Which PHP extensions should be checked before blaming MySQL?

The exact list depends on the OpenCart release and installed extensions, so the command below should not be treated as exhaustive. It is useful, however, for quickly checking commonly required modules during migration:

php -m | grep -Ei 'curl|gd|imagick|mbstring|mysqli|pdo|zip|openssl'

Then compare the result with the real FPM context. A missing mysqli, curl, or image-processing extension usually leaves a much more specific error in the PHP log than anything related to “slow MySQL.”

A typical branch in the investigation looks like this:

PHP Fatal error:  Uncaught TypeError: ...
PHP Fatal error:  Uncaught Error: Call to undefined function ...

If an error like this appears immediately after changing PHP and points to a specific OpenCart module or extension, I would not touch innodb_buffer_pool_size at all. Restore a compatible PHP version or update the incompatible code first.

Why can OpenCart behave worse after a PHP upgrade?

A newer PHP version does not automatically make the store faster. The OpenCart core, theme, OCMOD modifications, and third-party payment, shipping, import, or analytics modules may have different requirements. OpenCart 4.x uses PHP 8, but exact compatibility still needs to be checked against the installed OpenCart release and its extensions. Older OpenCart 3.x stores are particularly sensitive to PHP changes when third-party code has not been updated.

Check the PHP error log, OpenCart log, and web-server error log at the same time you reproduce the failure. The OpenCart log location depends on the version and on where storage has been moved. If switching PHP introduces TypeError, Call to undefined function, or fatal errors from a specific extension, compatibility must be fixed first. Deprecated warnings alone do not necessarily explain an HTTP 500 response, so focus on the fatal line and its context.

After this check you should know four things: the exact OpenCart version, the PHP-FPM version serving the site, the type and version of MySQL or MariaDB, and the list of critical store extensions. If you are deploying the store rather than tuning an existing installation, the guides to installing OpenCart on hosting and creating an OpenCart database cover the earlier setup stage. Without this baseline, tuning easily turns into troubleshooting the wrong layer.

How do you size PHP-FPM so OpenCart does not consume all VPS memory?

A store can be fast with a single request and then degrade sharply under concurrent traffic. In that situation, one of the first PHP-FPM parameters to inspect is pm.max_children. Each active worker handles a separate request and consumes memory, so increasing the number of workers increases both possible concurrency and the upper bound of RAM usage.

Inspect the processes with:

ps -eo pid,ppid,rss,cmd --sort=-rss | grep '[p]hp-fpm'

RSS is shown in kilobytes. Do not take the memory usage of the smallest idle process and multiply it by pm.max_children. Open several heavy OpenCart pages, the admin area, search, product filters, or a controlled import and inspect multiple active workers. A PHP worker may retain significantly more memory after processing a heavy request than it used immediately after startup.

RSS is not perfect either because some memory is shared between processes. For a more accurate PSS estimate, use smem if it is installed. Even a set of realistic RSS values is still much better than copying a number from somebody else's tuning guide.

How do you calculate a starting point for pm.max_children?

The calculation starts with the amount of RAM that can safely be assigned to PHP-FPM after MySQL, the operating system, and other services have been accounted for:

RAM budget for PHP / typical memory per worker = approximate worker limit

Suppose, purely as an example, measurements show that PHP can safely use about 2.4 GB and active OpenCart workers consume roughly 120 MB each. Simple division gives approximately 20 workers. That still does not mean you should immediately set pm.max_children = 20. You need reserve memory for variation between requests, background processes, MySQL connections, and traffic spikes. The result is an upper starting point for testing, not a universal recommendation.

One hundred PHP workers on a small VPS is not optimization. It is an OOM request waiting to happen.

What should OpenCart use: dynamic or ondemand?

dynamic is often convenient when traffic is relatively steady because some workers remain ready to accept requests. ondemand starts workers only when needed and can save memory on low-traffic stores, but sudden bursts pay the cost of creating new processes. static keeps a fixed worker count and therefore requires especially careful resource planning.

For a dynamic pool, inspect at least:

pm = dynamic
pm.max_children = ...
pm.start_servers = ...
pm.min_spare_servers = ...
pm.max_spare_servers = ...
pm.max_requests = ...

pm.max_requests is sometimes used to recycle workers periodically and reduce the impact of gradual memory growth in third-party PHP code. A very low value creates unnecessary process restarts, while an extremely high value does not fix a genuine memory leak.

Look in the PHP-FPM log for messages showing that pm.max_children has been reached. When all workers are busy, new requests wait for a free process. Raising the limit is justified only when the VPS still has enough RAM and CPU. Otherwise the PHP queue will simply be replaced by swap activity or an OOM event.

Useful check: if you enable an FPM status page for diagnostics, do not expose it publicly. It is an administrative endpoint and should have restricted access.

How do you configure Nginx or Apache for OpenCart routes and PHP?

The home page loads and index.php works, but SEO URLs return 404. In that situation, MySQL is almost certainly not the first place to look. Check web-server routing: existing files should be served directly, PHP should be sent to the correct PHP-FPM pool, and virtual OpenCart routes should reach the front controller.

Nginx: what should you check in the server block?

The basic principle is simple: serve existing static files directly, route unknown paths into OpenCart, and pass only real PHP scripts to PHP-FPM. The exact routing rule depends on the OpenCart version and URL structure, so adapt the following example to the installed release:

server {
    server_name shop.example.com;
    root /var/www/opencart;

    index index.php index.html;

    location / {
        try_files $uri $uri/ @opencart;
    }

    location @opencart {
        rewrite ^/(.+)$ /index.php?_route_=$1 last;
    }

    location ~ \.php$ {
        try_files $uri =404;
        include fastcgi_params;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }
}

The socket path in this example is only a placeholder. Use the actual path on your system. SCRIPT_FILENAME tells FastCGI which PHP file to execute. An incorrect value can result in messages such as Primary script unknown, 404 errors, or 502 responses depending on the rest of the configuration.

Always validate the syntax before reloading Nginx:

nginx -t

Only after the test succeeds:

systemctl reload nginx

If the configuration passes nginx -t but the service still refuses to start or fails after a restart, move on to systemd logs and the dedicated guide for the Failed to start nginx error.

Apache: why does index.php work while SEO URLs return 404?

With Apache, OpenCart usually relies on mod_rewrite and rules from the CMS-provided .htaccess file or template. If index.php opens directly but friendly URLs return 404, check whether the rewrite module is enabled and whether overrides are allowed for the document root.

apachectl -M | grep rewrite
apachectl configtest

When Apache uses AllowOverride None, rules in .htaccess are ignored. In a fully managed Apache configuration, rewrite rules are often better placed directly in the virtual host, but when OpenCart relies on its standard .htaccess, overrides must be allowed for the relevant directory.

After changing routing, test more than the home page. Check the home page, a category, a product page, search, the admin area, and a deliberately nonexistent URL. Also request a nonexistent .php file. With Nginx, that request should not be forwarded to PHP-FPM without checking whether the script actually exists.

Symptom First layer to check What to inspect
SEO URL returns 404 while index.php works Nginx/Apache routing try_files, rewrite rules, .htaccess, access/error logs
502 Bad Gateway PHP-FPM Socket, service status, FPM log, available workers
504 Gateway Timeout Upstream request duration PHP slowlog, SQL, external APIs, web-server timeout
500 Internal Server Error PHP/OpenCart PHP error log, OpenCart log, web-server error log
413 Request Entity Too Large Request-body limit Nginx/Apache and PHP upload limits
Too many connections MySQL PROCESSLIST, Max_used_connections
Permission denied Filesystem/PHP-FPM user Owner, group, permissions, path
A process disappears unexpectedly RAM/OOM Kernel journal, swap, memory pressure

Which PHP and web-server limits are needed for imports, images, and long OpenCart operations?

The storefront may work normally while an import, extension installation, or image upload fails. The problem is often not one setting but a chain of limits: the web server accepts the request, PHP writes the upload to a temporary directory, PHP-FPM executes the code, and then OpenCart or an extension processes the file. A failure at any point can look like “the import does not work.”

First inspect the PHP values for the actual FPM pool serving OpenCart. The CLI command is useful as an initial reference:

php -i | grep -E 'upload_max_filesize|post_max_size|memory_limit|max_execution_time|max_input_time|upload_tmp_dir'

Remember that php -i in the shell refers to CLI PHP. If the VPS has multiple PHP versions, verify the final values in the correct FPM pool configuration or with a temporary diagnostic request through the website.

Setting What it limits Typical symptom What to check next
client_max_body_size Request-body size in Nginx HTTP 413 post_max_size and upload_max_filesize
upload_max_filesize Maximum size of one uploaded PHP file PHP does not accept the file post_max_size, temporary directory
post_max_size Total POST request size POST data arrives incomplete Uploaded file plus all other form fields
memory_limit Memory available to one PHP process Allowed memory size exhausted Actual operation, image processing, import code
max_execution_time PHP execution time PHP terminates a long operation Why the operation takes so long
request_terminate_timeout Hard execution limit in a PHP-FPM pool FPM terminates the worker even when the PHP timeout is higher The actual FPM pool configuration and log
fastcgi_read_timeout How long Nginx waits for data from FastCGI 504 while PHP is still processing PHP slowlog, SQL, external APIs

Why can increasing upload_max_filesize change nothing?

Suppose upload_max_filesize allows 128 MB and the file is 80 MB. If post_max_size is only 32 MB, the upload still fails. If Nginx has a lower client_max_body_size, PHP never receives the request body at all, and the PHP log may contain nothing useful.

With Nginx, inspect the effective configuration:

nginx -T | grep -n 'client_max_body_size\|fastcgi_read_timeout'

After changing Nginx, run:

nginx -t

and reload the service only after the syntax check succeeds.

Why can OpenCart fail to upload a file even when all size limits are high enough?

There is another common case: client_max_body_size, post_max_size, and upload_max_filesize are all larger than the file, yet the upload still fails or PHP reports a temporary-file error. At that point, stop changing timeouts and inspect the directory where PHP writes uploads before OpenCart processes them.

php -i | grep -i upload_tmp_dir
df -h /tmp
df -i /tmp
ls -ld /tmp

If upload_tmp_dir is not explicitly configured, PHP normally uses the system temporary directory. A full filesystem, exhausted inode count, or incorrect permissions can cause failure before OpenCart starts processing the file. Raising upload_max_filesize does nothing in this scenario.

If PHP uses a custom temporary directory, check that path instead:

df -h /path/to/php-tmp
df -i /path/to/php-tmp
namei -l /path/to/php-tmp

Watch the PHP error log immediately after a single test upload. If PHP cannot create the temporary file, fix the filesystem or PHP-FPM user's access before changing OpenCart settings.

Why does a large max_execution_time not guarantee that a long import will finish?

PHP and PHP-FPM have separate execution controls. Even when max_execution_time is increased, the pool may have its own request_terminate_timeout. Nginx may also stop waiting for FastCGI according to fastcgi_read_timeout. Setting PHP to 600 seconds does not guarantee the complete request will be allowed to run for ten minutes.

Search for the effective FPM pool setting:

grep -R "request_terminate_timeout" /etc/php* /etc/php-fpm* 2>/dev/null

Configuration paths vary by distribution, so use this as a search command rather than assuming one universal location.

Should you simply increase timeouts for a slow import?

If a normal import legitimately needs several minutes to process a large file in batches, a longer timeout may be justified. A 504 caused by one pathological SQL query, a stalled HTTP API, or slow storage should not be treated by simply waiting longer.

For a 504, I would first correlate three things: Nginx upstream_response_time, the PHP-FPM slowlog, and the MySQL slow query log. If PHP is waiting on an external API, MySQL is not the problem. If the slowlog points into database-processing code and a slow SQL query appears at the same time, the investigation now has a confirmed direction.

Increase a timeout only after you know how long the normal operation should take. Otherwise the admin area simply waits longer for the same bottleneck.

How do you size innodb_buffer_pool_size for MySQL on an OpenCart VPS?

The rule of giving InnoDB most of the server's RAM is dangerous on a mixed OpenCart VPS. Large buffer-pool percentages are more appropriate on servers where the database is the primary workload. On an OpenCart VPS, PHP-FPM, the web server, the operating system, and often several other services need memory too.

Check the current value first:

SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

Then inspect InnoDB counters:

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests';
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';

Innodb_buffer_pool_read_requests represents logical read requests served through the buffer pool, while Innodb_buffer_pool_reads counts reads that had to go to storage. A high cache-hit ratio is useful, but one attractive percentage does not prove the buffer pool is correctly sized. A small database can produce an excellent hit rate while a slow page is actually blocked by PHP or an inefficient SQL query.

Can MySQL use 70–80% of RAM on an OpenCart VPS?

Applying that rule blindly on a mixed server is risky. Consider an 8 GB VPS. Suppose measurements show that Linux and background services need around 1.3 GB during normal operation, PHP-FPM can consume around 2 GB at expected concurrency, and additional memory is needed for other services and safety reserve. Giving MySQL 6 GB would already conflict with the rest of the system.

Now consider another 8 GB VPS where MySQL is separated from PHP and almost every other service is disabled. The database could safely use much more memory. A table that says “8 GB RAM = 6 GB buffer pool” hides the most important variable: what else is using the memory?

Check the entire server, not only MySQL:

free -h
ps aux --sort=-%mem | head -20

The buffer pool should cache the active database working set, not push PHP into swap.

How do you tell whether the buffer pool is actually too small?

The case becomes stronger if physical InnoDB reads rise during load, disk activity increases, and the database working set is noticeably larger than the available cache. The final decision should still be based on several signals together: slow queries, InnoDB counters, I/O latency, and overall memory pressure.

If increasing the buffer pool reduces physical reads, improves response time for the same repeatable workload, and does not introduce swapping, the change has a measurable effect. If response time stays the same while available RAM disappears, the larger buffer pool did not help.

About innodb_dedicated_server: this mode assumes MySQL can treat the machine as dedicated to the database and automatically size several InnoDB parameters. A VPS that also runs OpenCart, PHP-FPM, and a web server should not be considered a dedicated database server without careful review. MySQL cannot know how much memory PHP or other services need.

How do you configure max_connections and MySQL buffers without exhausting RAM?

The Too many connections error encourages the most obvious response: raise max_connections. That should not be the first step. First determine why the server reached the connection limit. Real concurrency may have increased, queries may be taking too long, PHP-FPM may be creating more simultaneous database work, or idle connections may be accumulating.

SHOW VARIABLES LIKE 'max_connections';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW FULL PROCESSLIST;

Max_used_connections shows the historical maximum since MySQL started. Threads_connected shows current client connections, while Threads_running helps identify how many threads are actively doing work right now. PROCESSLIST adds the missing detail: which client is connected, what it is doing, and how long it has been in the current state.

How are 80 Sleep connections different from 40 concurrent Query connections?

The number of rows in PROCESSLIST means little without the Command field. Compare two snapshots:

Threads_connected: 82
Threads_running:   2

and:

Threads_connected: 82
Threads_running:   47

In the first case, most sessions are connected but are not actively doing work. I would inspect Sleep sessions, connection lifetime, and application behavior before blaming MySQL CPU usage. In the second case, dozens of threads are actively running, so the next checks should focus on the SQL itself, CPU usage, locks, and I/O.

An illustrative fragment of SHOW FULL PROCESSLIST might look like this:

Id   Command  Time  State               Info
41   Sleep    38                        NULL
42   Sleep    21                        NULL
57   Query    14    Sending data        SELECT ...
63   Query    12    Creating sort index SELECT ...

Two sleeping sessions and two long-running queries require different actions. If dozens of long queries accumulate, raising max_connections only allows the database to accept even more expensive work at the same time.

When should max_connections actually be increased?

Increasing the limit makes sense when the application legitimately reaches the current value, queries are not stuck, and the server has enough CPU and memory for additional concurrency. For example, the store may receive more simultaneous requests, PHP-FPM may be able to process them, MySQL may have available CPU and I/O capacity, and the configured connection ceiling may simply be too low for the real workload.

The opposite scenario looks similar from the outside. PROCESSLIST contains many long-running queries against the same table, Threads_running keeps increasing, and page response times degrade. Raising max_connections here only lets MySQL run more expensive queries concurrently. Eventually the store reaches the new limit too, but with higher RAM and CPU usage.

Why is it risky to increase join_buffer_size and sort_buffer_size globally?

Some MySQL buffers are allocated per connection or per operation. A value that looks small next to several gigabytes of RAM can become significant when dozens or hundreds of sessions perform the same type of work simultaneously. At the same time, simply adding every session buffer together and multiplying by max_connections does not produce an exact memory maximum because many allocations happen only when required. The risk still remains: large per-session buffers combined with high concurrency can cause unexpected memory growth.

Parameter What it controls Risk of oversizing How to verify the need
innodb_buffer_pool_size InnoDB data and index cache Pushes PHP and the OS into swap InnoDB counters, I/O, RAM
max_connections Maximum client connections More concurrent workload and potential memory use Max_used_connections, PROCESSLIST
sort_buffer_size Memory for some sorting operations Higher RAM use during many concurrent sorts Actual queries and execution plans
join_buffer_size Buffer for some join operations Higher memory use and possible masking of query/index problems EXPLAIN, slow query log
tmp_table_size Influences limits for internal temporary tables More memory use; behavior depends on MySQL version Created_tmp_tables, Created_tmp_disk_tables
innodb_redo_log_capacity Redo log capacity in newer MySQL versions A large redo log does not replace analysis of the write workload Write bursts, checkpoints, I/O
wait_timeout Lifetime of inactive connections Too low can break required sessions; too high keeps idle sessions longer PROCESSLIST and application behavior

MySQL and MariaDB differ in individual parameters, default values, and internal behavior. Do not copy a configuration between them line for line. Run SELECT VERSION(); first and then consult documentation for the exact installed branch.

Three limits are connected here: pm.max_children controls the upper concurrency of PHP, PHP generates database workload, max_connections limits MySQL client sessions, and per-session buffers add memory for specific operations. These layers cannot be tuned independently.

Forex VPS
VPS for automated trading
  • RDP connection
  • Minimal ping
  • Uptime — 99.95%
  • 24/7 operation
Forex VPS

How do you find slow OpenCart SQL queries instead of blindly tuning MySQL?

One category takes several seconds to load while the home page and neighboring categories remain fast. In that situation, global MySQL tuning is not the first place to look. One query from a filter, module, report, or extension can create a large delay even when the buffer pool is perfectly adequate.

Check the slow query log configuration:

SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'slow_query_log_file';

For temporary diagnostics, some MySQL settings can be changed dynamically. For example:

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;

One second is only a diagnostic example, not a universal threshold. A fast store may need shorter thresholds, while aggressive logging on a busy database can generate a large amount of data. A SET GLOBAL change also does not replace persistent configuration if the setting needs to survive a restart.

How do you connect a slow OpenCart page to one SQL query?

Do not read the slow query log from top to bottom. Take one specific slow URL and record the exact request time. Then match that timestamp across the access log, PHP/OpenCart logs, and MySQL slow query log. This narrows the search dramatically.

  1. Open the slow URL and record the exact time.
  2. Find the same request in the access log.
  3. Check PHP-FPM and OpenCart errors around the same timestamp.
  4. Locate related SQL in the slow query log.
  5. Run EXPLAIN on a safe copy of the query.
  6. After making a change, repeat the same URL and compare timing.

For basic slow-log analysis, you can use:

mysqldumpslow /path/to/mysql-slow.log

If Percona Toolkit is installed, pt-query-digest is useful for grouping similar query patterns and ranking their overall cost. It is not required, however. For one problematic URL, the slow query log and EXPLAIN are often enough.

What should you look for in EXPLAIN for OpenCart?

EXPLAIN shows the execution plan MySQL intends to use. Inspect the selected indexes, access type, estimated rows, and extra operations. A query “using an index” is not automatically fast. The index may have poor selectivity, MySQL may still scan many rows, and sorting or joins can remain expensive.

EXPLAIN
SELECT ...
FROM ...
WHERE ...;

The important part is not merely running EXPLAIN, but recognizing a suspicious plan. For example:

type: ALL
possible_keys: idx_status, idx_category
key: NULL
rows: 250000
Extra: Using where; Using filesort

type=ALL and key=NULL mean MySQL has not chosen an index for table access in this plan. rows=250000 is an estimate that a large number of rows may need to be examined. This is not an automatic failure — full scans can be reasonable on small tables or for certain queries — but on one consistently slow category page it gives you a concrete place to investigate.

After changing an index or query, compare both the new execution plan and the response time of the same page. If the estimated row count drops sharply but HTTP response time remains unchanged, SQL was not the only bottleneck.

If the installed MySQL version supports an appropriate EXPLAIN ANALYZE, remember that it does more than generate a theoretical plan: it actually executes and analyzes the query. Do not run it blindly on an expensive production query. Start with plain EXPLAIN and use EXPLAIN ANALYZE only on a safe test environment or a controlled read-only query.

Do not automatically add an index for every column in a WHERE clause. Extra indexes consume storage, slow writes, and may never be selected by the optimizer. Indexing decisions should be based on the specific query, its filters and sort order, and the real data distribution.

Useful diagnostic clue: if one URL is consistently slow while the rest of the same store remains fast under low system load, investigate route-specific PHP code or SQL first. In that case, global my.cnf tuning is often aimed at the wrong layer.

How do you tell whether OpenCart is bottlenecked by InnoDB or disk I/O?

Idle CPU during a hanging import is a reason to inspect storage, but it is not proof that storage is the problem. You need correlation: OpenCart slows down at the same time as I/O latency rises, device queues grow, or InnoDB reports pending work. Having “NVMe” in the VPS plan name does not replace these measurements.

Collect metrics while the problematic operation is actually running:

iostat -xz 1
vmstat 1

There is no universal await value that makes every VPS “slow.” Comparing the server against its own baseline is more useful. For example, during idle periods the disk may show low latency and almost no queue, but as soon as an import starts, await and queue depth rise sharply while CPU remains far from saturation. That is meaningful evidence of an I/O bottleneck. If the storage metrics barely change, continue investigating elsewhere.

What should you inspect in SHOW ENGINE INNODB STATUS when OpenCart slows down?

Capture InnoDB's internal state at the same moment:

SHOW ENGINE INNODB STATUS\G

The output is large, and you do not need to analyze every section for every incident. For initial troubleshooting, several areas are especially useful:

  • SEMAPHORES and waits — useful when there is contention inside InnoDB;
  • LATEST DETECTED DEADLOCK — relevant when deadlocks occur and the application reports corresponding errors;
  • FILE I/O — shows current and pending storage work;
  • BUFFER POOL AND MEMORY — provides buffer-pool and read information;
  • LOG — useful for redo and checkpoint behavior during heavy write activity.

If disk latency rises during a mass product update and InnoDB simultaneously shows pending I/O, I would not increase PHP-FPM workers. More workers would only create additional concurrent work on top of storage that is already busy.

If SHOW ENGINE INNODB STATUS shows no accumulated I/O while one SQL query in the slow log reads a huge amount of data, the issue may not be a “slow disk” at all. The query itself may be generating unnecessary I/O.

How do you distinguish slow storage from SQL that generates excessive I/O?

Use three data sources together:

  1. Record the slow URL or import and its exact time.
  2. Watch iostat -xz during the same interval.
  3. Check the slow query log and execution plan of expensive SQL.

If I/O spikes only while one query runs and EXPLAIN shows that it scans a huge number of rows, optimize the query or index strategy first. If many otherwise reasonable queries and background jobs all experience high latency at the same time, suspicion shifts toward the storage layer or insufficient RAM for the active working set.

Temporary tables are another useful signal:

SHOW GLOBAL STATUS LIKE 'Created_tmp_tables';
SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';

An increase in Created_tmp_disk_tables does not automatically mean that tmp_table_size should be raised. Query structure, data types, and MySQL-version behavior can all influence temporary table placement. Find the queries creating the workload first.

When should you investigate innodb_redo_log_capacity?

During large write bursts — for example, bulk product updates — redo-log capacity can affect checkpoint behavior and background writes. Modern MySQL branches use innodb_redo_log_capacity for total redo capacity, while MariaDB and older MySQL releases use different sets of parameters. Check the actual database version first:

SELECT VERSION();

Do not increase redo capacity simply because the VPS has fast storage. First confirm that the write workload and checkpoint behavior are actually involved in the slowdown.

Should you change innodb_flush_log_at_trx_commit for more speed?

Settings related to transaction flushing and binlog synchronization affect more than performance. They also affect how much recently committed data can be lost during power failure, kernel panic, or a hypervisor crash. For an online store, that can include orders and other transactional data.

The advice to “use a less durable mode because it is faster” is not normal tuning. It is an explicit durability trade-off that requires a clear understanding of possible data loss and the backup or replication model. By default, investigate heavy SQL, I/O, and the reason for write bursts before sacrificing durability for a benchmark result.

The same rule applies to innodb_io_capacity. The theoretical IOPS of NVMe hardware are not the guaranteed IOPS of a particular VPS. Measure the disk first. Tune it second.

How do you configure OpenCart caching without caching the cart or checkout?

For OpenCart, server-side caching is safest when it starts with static assets. A bad full-page cache configuration can make a page fast while showing another visitor's cart state, currency, price, or personalized content. For an online store, that is worse than a slow page.

How are browser cache, OpenCart cache, and FastCGI cache different?

Do not mix up three separate caching layers:

  • browser cache — the visitor's browser reuses CSS, JavaScript, fonts, and images;
  • OpenCart application cache — cached data is created by OpenCart or an extension;
  • FastCGI/full-page cache — the web server stores the complete HTTP response generated by PHP.

The failure modes are different. Stale CSS after a theme update is one problem. A cached checkout page carrying another session's state is a completely different one.

What can Nginx cache with relatively low risk?

Images, CSS, JavaScript, and fonts are usually suitable for long browser caching, especially when asset URLs or versions change after updates. The basic idea can look like this:

location ~* \.(css|js|jpg|jpeg|png|webp|svg|woff2)$ {
    expires 7d;
    add_header Cache-Control "public";
}

Seven days is only an example. The correct lifetime depends on whether the theme and build process provide reliable asset versioning or cache busting. If a file can change while keeping the same URL, an overly long cache lifetime causes visitors to keep old assets.

Check the resulting headers:

curl -I https://shop.example.com/catalog/view/stylesheet/stylesheet.css

Inspect Cache-Control, Expires, and Vary when relevant. After a theme update, verify that the new CSS receives a new URL or can otherwise be invalidated correctly.

gzip or Brotli compression reduces the size of suitable text resources, but it does not fix slow PHP or SQL. If page TTFB is several seconds, saving a few kilobytes on CSS does not address the primary delay.

Why is FastCGI cache dangerous for an online store?

An OpenCart response can depend on session cookies, authentication state, the cart, selected currency, language, customer group, tax logic, and third-party extensions. Enabling one global FastCGI cache for every PHP response is therefore unsafe.

At minimum, the following areas need bypass rules or separate handling:

  • cart;
  • checkout;
  • account;
  • admin;
  • authenticated sessions;
  • responses that depend on cookies or user-specific parameters.

Older OpenCart routing creates an additional trap: checkout may not appear as /checkout/ at all. It may use a URL such as index.php?route=checkout/.... If the cache bypass rule checks only the path and ignores the query string, a dynamic checkout response may accidentally enter the page cache.

Even product and category pages are not guaranteed to be identical for every visitor. Extensions may change prices according to customer group, currency, geography, or other session data. Full-page caching therefore needs to be validated against the actual store, not just a clean OpenCart core installation.

How do you verify that the page cache does not mix user sessions?

Use two completely independent browser sessions. Add different products to each cart and, if supported, select different currencies or languages. Then open the same category and checkout pages in both sessions while inspecting response headers and cookies.

If HTML generated for one session begins appearing in the other, the page cache is configured incorrectly. Do not try to improve the hit rate. Disable caching for the unsafe route or build a correct bypass strategy.

A fast checkout that serves another user's state is not useful optimization.

Which OpenCart files and directories should be protected at the VPS and web-server level?

Once OpenCart is installed and working, check whether service data remains accessible through the document root. Web-server configuration for OpenCart is not only about performance: storage directories, installation files, and forgotten backups must not be downloadable through ordinary HTTP requests.

After installation, verify that the installation directory has been removed and check where the storage directory is located. For current OpenCart branches, storage should normally be placed outside the public web root. Actual paths depend on the version and directory layout.

find /var/www/opencart -maxdepth 3 \
  \( -name '*.sql' -o -name '*.zip' -o -name '*.bak' -o -name '*.old' \) \
  -print

This check often finds migration archives, SQL dumps, or old configuration copies that were supposed to remain on the server for only a few days. If a backup sits inside a public directory and Nginx or Apache serves it as a static file, PHP-level protection will not help.

Why is chmod 777 the wrong fix for Permission denied?

If OpenCart cannot write cache data, logs, or images, first identify the PHP-FPM user and the owner of the target directory. Setting permissions to 777 simply removes access restrictions for everyone and hides the original ownership problem.

ps -eo user,group,cmd | grep '[p]hp-fpm'
stat /var/www/opencart
namei -l /var/www/opencart/system/storage

namei -l is especially useful when the final directory has correct permissions but access is blocked by one of its parent directories. It is easy to inspect only the destination directory and miss the fact that PHP-FPM cannot traverse a directory higher in the path.

After identifying the PHP-FPM user, grant only the permissions OpenCart actually needs for writable directories. Application code, configuration files, and public static files do not need to be globally writable.

What else should you test directly over HTTP?

Try direct requests to suspected backup files, storage locations, and configuration artifacts. A 403 or 404 response is better than assuming nobody will guess the filename.

This section does not need to become a full Linux hardening guide. Firewall configuration, SSH security, fail2ban, and DDoS protection deserve separate treatment. If one VPS hosts several projects, separating permissions and directories is also useful when managing websites and applications on a VPS. For OpenCart itself, close the file-access risks directly related to the document root and PHP-FPM user.

How do you use logs to find out whether Nginx, PHP-FPM, or MySQL is slowing OpenCart down?

“OpenCart is sometimes slow” is too broad to troubleshoot. Pick one slow request, record the exact time, and trace it across the entire stack: Nginx or Apache access logs, PHP-FPM, OpenCart, MySQL, and system metrics. One correlated time window is more useful than thousands of unrelated log lines.

Nginx does not always log every timing metric needed for this by default. A dedicated diagnostic format can include:

log_format timed '$remote_addr $request '
                 'status=$status '
                 'rt=$request_time '
                 'uct=$upstream_connect_time '
                 'uht=$upstream_header_time '
                 'urt=$upstream_response_time';

access_log /var/log/nginx/opencart-access.log timed;

request_time shows the total request duration in Nginx, while the upstream fields help show how much time was spent talking to PHP-FPM. After changing the configuration:

nginx -t
systemctl reload nginx

If request_time is high and is close to upstream_response_time, the delay is behind Nginx — usually in PHP, OpenCart, MySQL, or an external dependency PHP is waiting for. If the upstream responds quickly while total request time is much higher, investigate another part of the request path.

What does a 502 error mean in the Nginx error log?

The HTTP 502 code alone does not mean “increase the timeout.” Start with the actual error-log line. For example:

connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)
while connecting to upstream

After a message like this, I would not touch fastcgi_read_timeout. Nginx could not find the PHP-FPM socket. Check the configured fastcgi_pass, whether the socket exists, and whether the FPM service is running:

systemctl status php8.3-fpm
ls -l /run/php/
nginx -T | grep -n fastcgi_pass

A different 502 can have a different cause, such as a PHP-FPM worker crashing or closing the connection unexpectedly. The HTTP status selects the troubleshooting branch; the error-log line narrows the actual diagnosis.

How do you distinguish slow PHP from slow MySQL?

A slow PHP request does not prove MySQL is slow. PHP may be waiting for a shipping API, payment gateway, DNS lookup, filesystem operation, or another locked resource. Compare the slow query log with the duration of the PHP request itself.

For PHP-FPM, a temporary slowlog can be configured:

request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/opencart-slow.log

Five seconds is only an example diagnostic threshold. After reloading PHP-FPM, the slowlog can show the stack trace of a worker that runs longer than the threshold. If PHP spends most of that time inside an external HTTP client, a MySQL index change will not help. If the stack points to database-related code and the MySQL slow log shows an expensive query at the same time, two independent sources now point to the same layer.

What does a real pm.max_children limit look like?

When a pool reaches its worker limit, PHP-FPM usually leaves a clear message. A typical line looks like:

WARNING: [pool www] server reached pm.max_children setting (10), consider raising it

At this point the worker limit is no longer a guess — the pool actually reached it. Still, do not increase pm.max_children yet. First check:

free -h
vmstat 1
top

If available RAM still has reserve, swap is inactive, and CPU is not saturated, increasing the worker count can be tested. If memory is already exhausted, additional PHP processes only replace the FPM queue with swapping or an OOM kill. In that case, find out why workers remain busy for so long: use the PHP slowlog, inspect external API calls, identify heavy SQL, or consider whether the VPS simply lacks resources.

What does an OOM event look like in the kernel log?

If PHP-FPM or mysqld suddenly disappears, inspect the kernel journal:

journalctl -k | grep -Ei 'out of memory|oom|killed process'

A representative message has this meaning:

Out of memory: Killed process ... (php-fpm)

After seeing this, do not raise pm.max_children, memory_limit, or the InnoDB buffer pool without recalculating the entire memory budget. The kernel has already shown that the server reached its physical memory limit.

Symptom What to measure Likely layer Next check
CPU constantly saturated Processes by CPU usage PHP or MySQL top, pidstat, slowlog
Swap activity increases si/so, available RAM Overall memory pressure PHP workers and mysqld memory use
I/O latency rises iostat -xz Storage/MySQL/filesystem Slow SQL, import, backup activity
502 Nginx error log PHP-FPM/upstream Socket, FPM service, worker crash
504 Upstream response time PHP/backend PHP slowlog, SQL, external API
Too many connections Connections and process list MySQL SHOW FULL PROCESSLIST
One category is slow Timing of the specific URL Module/SQL Slow log and EXPLAIN
All PHP URLs are slow FPM queue, CPU, RAM PHP-FPM/VPS FPM status, vmstat

Do not read the entire log. Trace one slow request all the way through the stack. Each step should either confirm a problematic layer or give you a reason to rule it out.

How do you validate OpenCart after tuning and know when the VPS is no longer enough?

After changing MySQL or PHP-FPM, do not rely on the impression that “pages seem faster.” Repeat the same requests and collect the same system metrics you recorded before the change. That is the only way to demonstrate improvement and detect new side effects.

For a single HTTP request, curl can record total response time:

curl -s -o /dev/null \
  -w 'code=%{http_code} total=%{time_total}\n' \
  https://shop.example.com/category/example

Test several different OpenCart scenarios:

  • home page;
  • a heavy category;
  • a product page;
  • search and filters;
  • cart;
  • checkout up to a safe stage without making a real payment;
  • admin area;
  • import or cron jobs if those were the original problem.

Record CPU, RAM, swap, and I/O at the same time. If a category was previously slow while InnoDB performed many physical reads and the same request becomes more stable after a buffer-pool adjustment without introducing swap, the improvement has a reasonable technical explanation. If the page becomes slightly faster but the VPS starts swapping under several concurrent requests, the new configuration is more fragile.

For controlled concurrent testing, tools such as wrk, ab, or another load generator can be used. Do not run an aggressive benchmark against a production store just to get an attractive number. A load test can create an outage, and requests to cart, checkout, or other state-changing URLs can alter store data. A staging environment or a small controlled test during low traffic is safer.

Which signs show that the problem is no longer just configuration?

A VPS can genuinely become too small for the store, but make that conclusion only after obvious software problems have been ruled out. If CPU remains saturated under a repeatable legitimate workload, PHP-FPM uses a reasonable worker count, there is no single pathological SQL query, memory is not being wasted by incorrect limits, and requests still hit the resource ceiling, further tuning cannot create CPU or RAM that the server does not have.

The same applies to storage. If reasonable queries and a properly sized buffer pool still experience high latency under normal workload, the bottleneck may be the storage layer or insufficient RAM for the database working set. On the other hand, if one third-party function performs hundreds of SQL queries per page, fix the application before increasing the VPS size.

Why is a baseline more useful than one benchmark?

A single request immediately after a reload may hit a warm cache and produce an impressive number. An hour later, cron jobs, imports, and real concurrent visitors change the situation. Compare the same scenarios before and after a change, and where possible modify one major parameter at a time.

OpenCart VPS acceptance checklist

  • RAM and swap have been checked both at idle and under real workload.
  • No new OOM events appear.
  • The real memory usage of PHP-FPM workers is known.
  • pm.max_children fits within the overall memory budget.
  • innodb_buffer_pool_size leaves enough memory for PHP and the operating system.
  • Max_used_connections and the current PROCESSLIST have been reviewed.
  • There are no unexplained slow queries in the MySQL slow query log.
  • SEO URLs, admin, cart, and checkout still work after web-server changes.
  • HTTP 413, 500, 502, and 504 errors are not being hidden by oversized universal limits.
  • Static files are cached separately from personalized store logic.
  • Storage directories and service files cannot be downloaded as public website files.
  • The validation test uses the same URLs and comparable conditions as the original baseline.

A good OpenCart VPS configuration is not a collection of maximum values. For every major parameter, the administrator should be able to explain why it was changed, which metric showed the need for the change, and what happened when the same test was repeated afterward. If those answers are missing, the server has not really been tuned — settings have only been changed blindly.

Frequently asked questions
Do not use a fixed percentage of total RAM. Reserve memory for Linux, PHP-FPM, and other services first, then size innodb_buffer_pool_size according to the actual workload.
Measure several PHP-FPM workers under realistic load and divide the RAM budget available to PHP by typical per-worker memory usage, leaving additional safety reserve.
Check the Nginx error log first. Common causes include an incorrect PHP-FPM socket, a stopped FPM service, or a worker that terminated unexpectedly.
Not immediately. Check Max_used_connections, Threads_connected, Threads_running, and SHOW FULL PROCESSLIST to see whether long-running SQL or idle sessions are causing the limit.
The request may still be limited by client_max_body_size, post_max_size, request_terminate_timeout, or PHP's temporary directory. Also check free space and inode availability.
Match the timestamp of a slow URL with the MySQL slow query log and PHP-FPM slowlog. If a slow SQL query appears, inspect it with EXPLAIN and repeat the same request after the change.
Yes, but only with careful bypass rules. Cart, checkout, account, admin, authenticated sessions, and responses that depend on cookies must not be cached blindly.
Related articles
Osclass Hosting — A Complete Guide for System Administrators
Configure Apache/Nginx for OpenCart on a VPS
Setting Up PrestaShop on a VDS (Virtual Dedicated Server)