PHP ships with several hundred directives and about eight of them decide whether your site works. memory_limit at 128 MB, upload_max_filesize at 2 MB, post_max_size at 8 MB, max_execution_time at 30 seconds and max_input_vars at 1,000 are the defaults behind most of the errors people bring to a hosting ticket, and each one has a specific symptom that identifies it.
The second half of the problem is that changing a value and seeing no effect is normal. PHP reads a configuration file whose location depends on how it is run, ignores .htaccess directives unless it is running as an Apache module, only lets some settings be changed at runtime, and can be overridden by a PHP-FPM pool that wins over everything. So this starts with finding the file.
Which file PHP is actually reading#
There is one php.ini per PHP installation and often several installations on one machine - the web server's and the command line's are routinely different, which is why a script that works over SSH fails in a browser. Ask PHP rather than guessing:
$ php --iniConfiguration File (php.ini) Path: /etc/php/8.2/cliLoaded Configuration File: /etc/php/8.2/cli/php.iniScan for additional .ini files in: /etc/php/8.2/cli/conf.dThat is the command-line one. For the values your website runs under, put a single file in the web root with phpinfo(); in it, load it in a browser, read the Loaded Configuration File line at the top, and delete the file immediately afterwards - it publishes your paths, extensions and environment to anyone who finds the URL.
Four mechanisms can set a value, and they apply in this order:
| Mechanism | Applies to | Notes |
|---|---|---|
php.ini | Everything in that installation | The base |
conf.d/*.ini | Everything, loaded after | Where packages drop extension settings |
| PHP-FPM pool config | One pool | php_admin_value cannot be overridden later |
.user.ini | One directory tree | FPM and CGI only, cached for 300 seconds |
ini_set() in code | One request | Only settings marked as changeable at runtime |
Two consequences catch people out. First, php_value lines in .htaccess only work when PHP runs as an Apache module; under PHP-FPM, which is what nearly everything uses now, they are ignored at best and cause a 500 at worst. The equivalent is a .user.ini file in the same directory. Second, .user.ini changes are cached - the default user_ini.cache_ttl is 300 seconds - so waiting five minutes before deciding your edit did nothing saves a lot of confusion.
Also note that .user.ini can only set directives in the PHP_INI_PERDIR and PHP_INI_USER classes. upload_max_filesize is one of those. Plenty of others are not, and for those you need the real php.ini or the pool configuration, which on managed hosting means asking whoever runs it.
memory_limit#
memory_limit caps how much memory one PHP process may allocate for one request. The shipped default is 128 MB. When a request exceeds it, PHP stops with a fatal error that names the number exactly:
PHP Fatal error: Allowed memory size of 134217728 bytes exhausted(tried to allocate 20480 bytes) in /var/www/wp-includes/wp-db.php on line 2056For a small WordPress site, 128 MB is enough. For a site with a page builder, a shop and thirty plugins, 256 MB is the realistic figure, and 512 MB is what image-processing and import jobs want. Above that, suspect the code: a plugin loading every post into an array does not need more memory, it needs fixing.
memory_limit = 256MThree things worth knowing:
- It is per request, not per site. Ten concurrent requests at 256 MB each can ask for 2.5 GB. The limit is a ceiling on one process, and the number of processes is what multiplies it - see the PHP-FPM section below.
- Setting it to `-1` is a bad idea on a web server. Unlimited means one runaway request can take the whole container to its memory limit, at which point everything on it dies rather than one request failing cleanly. Many distributions do ship
-1for the command-line interpreter, which is exactly why long imports work from a shell and fail in a browser. - WordPress has its own limit on top.
WP_MEMORY_LIMITdefaults to 40 MB for the front end andWP_MAX_MEMORY_LIMITto 256 MB for admin pages. WordPress can only raise its own limit within what PHP allows, so both have to be right. Installing WordPress manually covers where those constants go.
On container-based hosting there is a third ceiling: the container's own memory. A plan with 1 GB and memory_limit set to 512 MB can support two heavy requests at once before the kernel stops the container. On RE:NODE, reaching the container limit stops it and restarts it clean rather than letting it swap, so the site comes back quickly but in-flight requests are lost. Size memory_limit and worker count together against the plan - how much RAM WordPress needs does the arithmetic.
Upload size: three limits, not one#
A failed upload is the most common php.ini question, and it is usually blamed on a single setting when three or four are involved.
| Setting | Default | What it limits |
|---|---|---|
upload_max_filesize | 2M | The largest single uploaded file |
post_max_size | 8M | The whole request body, all files plus fields |
max_file_uploads | 20 | Files in one request |
memory_limit | 128M | Must exceed post_max_size for large posts |
max_execution_time | 30 | Enough time to receive and process the file |
They have to be consistent, and the order matters: post_max_size must be larger than upload_max_filesize, because the request body contains the file plus its form fields plus the multipart overhead. A sensible set for a site that accepts 64 MB uploads:
upload_max_filesize = 64Mpost_max_size = 72Mmemory_limit = 256Mmax_execution_time = 120If the upload still fails with a 413 rather than a PHP error, the limit is not PHP's. A reverse proxy or web server in front has its own body size cap - in nginx that is client_max_body_size, which defaults to 1 MB - and it rejects the request before PHP ever sees it. On managed hosting you will not control that directly, which is a good reason to import large database dumps as a gzipped file rather than a raw one. What a reverse proxy does explains which limits live where.
max_execution_time and the timeouts around it#
max_execution_time is 30 seconds by default and counts only time PHP itself is executing - not time spent waiting on a database query or a remote API, on Unix builds. The command-line interpreter sets it to 0, unlimited, which is another reason shell scripts behave differently.
Raise it for the specific job rather than globally. A 300-second global limit means a stuck request occupies a worker for five minutes, and twenty of those fill the pool. Put long work where it belongs: a scheduled task, a queue, or the command line.
Around it sit three other timeouts that will cut a request short regardless of what PHP thinks:
- `max_input_time` limits parsing the request body, which is what matters on slow uploads. The built-in default of -1 means it falls back to
max_execution_time, while the shipped production file sets 60 seconds. - PHP-FPM's `request_terminate_timeout` kills the worker outright. It overrides
max_execution_timeand produces a 502 rather than a PHP error, so a request that dies with no entry in the PHP error log usually died here. - The proxy's own read timeout, commonly 60 seconds, which returns 504 to the visitor while PHP carries on working invisibly.
A plugin importer that "stops at 60 per cent every time" is almost always one of the last two, not the first.
max_input_vars, the silent one#
max_input_vars limits how many variables one request may submit, and it defaults to 1,000. It is the nastiest setting in the list because exceeding it produces no error at all: PHP truncates the input and hands the script a partial array.
The symptoms are the ones nobody connects to a configuration file. A WordPress menu with 120 items saves the first 90 and drops the rest. A theme options page reverts half its fields. A WooCommerce product with many variations loses them on save. A bulk edit applies to some rows only.
max_input_vars = 5000Each nested array element counts as a variable, so a large menu or a variable product hits 1,000 far sooner than the number suggests. 3,000 to 5,000 is a reasonable setting; going much higher is a way of hiding a form that should be paginated.
OPcache and the realpath cache#
Without OPcache, PHP parses and compiles every file on every request. With it, compiled opcodes live in shared memory and are reused, which is worth a large fraction of the PHP time on any application made of thousands of small files.
opcache.enable = 1opcache.memory_consumption = 192opcache.interned_strings_buffer = 16opcache.max_accelerated_files = 20000opcache.validate_timestamps = 1opcache.revalidate_freq = 2realpath_cache_size = 4096Krealpath_cache_ttl = 600What those values are doing, and why the shipped ones are often too small:
- `opcache.memory_consumption` ships at 128 MB. When it fills, OPcache stops caching new files silently, and the site gets slower without any error. Check
opcache_get_status()forused_memoryand the cached-key count against the maximum. - `opcache.max_accelerated_files` ships at 10,000, which a large application with plenty of plugins can genuinely exceed. The value is rounded up internally to a prime-based figure, so setting 20,000 is fine.
- `opcache.validate_timestamps` at 1 means PHP checks file modification times every
revalidate_freqseconds. Setting it to 0 is faster and correct for a deployed application where you restart PHP after each release. It is wrong for anything that updates its own files from a web interface, such as WordPress, because updates appear not to take effect until PHP restarts. - The realpath cache stores resolved file paths. The default 4 MB is small for a framework with deep autoload paths; raising it and lengthening the TTL removes a surprising number of
statcalls. - JIT is effectively off by default, because
opcache.jit_buffer_sizeis 0 until you set it. Leave it that way for web applications: the gains are in numeric and long-running work, not in request-shaped code that spends its time in the database. WordPress speed and caching puts OPcache in the order of things that actually move time to first byte.
Errors, logs and what visitors must never see#
The production defaults exist for a reason: an error message on a live page tells an attacker your absolute paths, your database structure and sometimes your credentials.
display_errors = Offdisplay_startup_errors = Offlog_errors = Onerror_log = /var/log/php/error.logerror_reporting = E_ALL & ~E_DEPRECATEDexpose_php = Offdisplay_errors off and log_errors on is the whole rule: nothing to the browser, everything to a file you can read. Set error_log to a path PHP can write to, or leave it unset and the errors go to the web server's log, which is where a panel console shows them. expose_php off removes the X-Powered-By header with your exact PHP version in it - minor, free.
On a development copy, flip display_errors on and set error_reporting to E_ALL, including deprecations. Deprecation notices are how you find out in advance which plugins will break on the next PHP version, which is much better than finding out on upgrade day.
Settings that are about security#
A handful of directives are worth setting deliberately, and a couple of popular ones are worth less than their reputation.
- `allow_url_fopen` is on by default and lets
file_get_contents()fetch a URL. Turning it off blocks a class of server-side request forgery, and breaks plugins that use it instead of cURL. Test before committing. Its dangerous relative,allow_url_include, was deprecated in PHP 7.4 and removed in 8.0, so that one is no longer your problem. - `disable_functions` can remove
exec,shell_exec,passthru,systemandproc_open. This genuinely limits what an uploaded web shell can do. It also breaks backup plugins that shell out tomysqldumpand image tools that callimagickbinaries, so check what your stack uses first. - Session cookies.
session.cookie_httponly = 1,session.cookie_secure = 1on an HTTPS site,session.use_strict_mode = 1, andsession.cookie_samesite = Lax. Frameworks usually set their own session cookies and ignore these, but anything using raw PHP sessions benefits. - `open_basedir` restricts which directories PHP may touch. Useful on a machine hosting several sites under one PHP process; largely redundant when each site is its own container, which is the model on RE:NODE, where every server runs in a container of its own with the CPU and memory the plan bought.
- `date.timezone` is not security, but set it to UTC and store everything in UTC. Scheduled tasks and log correlation both get easier, and daylight saving stops being an annual incident.
PHP-FPM: the settings that are not in php.ini#
The pool configuration decides how many requests can run at once, and it is a separate file - commonly /etc/php/8.2/fpm/pool.d/www.conf.
pm = dynamicpm.max_children = 12pm.start_servers = 3pm.min_spare_servers = 2pm.max_spare_servers = 6pm.max_requests = 500request_terminate_timeout = 120php_admin_value[memory_limit] = 256Mpm.max_children is the important one, and it is a memory calculation, not a preference:
pm.max_children = (memory available to PHP) / (average process size)A WordPress worker typically sits at 40 to 80 MB resident. On a 1 GB plan, after the web server and anything else, that is roughly eight to twelve children. Setting it to 50 does not create capacity; it creates a machine that dies under the load it was supposed to survive. pm = ondemand suits a small site with sparse traffic, since idle workers exit and give the memory back.
pm.max_requests recycles each worker after a number of requests, which papers over slow memory leaks in third-party extensions. 500 to 1,000 is normal and the cost is negligible.
Note php_admin_value[memory_limit]: values set that way cannot be overridden by ini_set() in code, which is how a host enforces a limit. If you are raising memory_limit in wp-config.php and nothing changes, this is usually why. On panel hosting, look for the same idea expressed as an environment variable or a startup field rather than a file you edit - and if the setting you need is one PHP will not let a directory-level file change, that is the point to open a ticket rather than keep editing. Deploying Laravel to production covers the same pool settings from the framework side.
FAQ#
I changed php.ini and nothing happened. Why?
Four usual reasons: you edited the command-line file rather than the web one, you did not reload PHP-FPM afterwards, a conf.d file or a pool php_admin_value overrides your value, or you used .htaccess on a server running PHP-FPM where those directives are ignored. Confirm with phpinfo() and read the Loaded Configuration File line.
What memory_limit should WordPress have?
256 MB covers almost every real site, including WooCommerce. 128 MB is enough for a small blog with few plugins. If you need more than 512 MB to load a page, something is loading far too much data and raising the limit only postpones the failure.
Why does my upload fail at exactly 2 MB or 8 MB?
Those are the defaults for upload_max_filesize and post_max_size. Raise both, keeping post_max_size larger, and confirm memory_limit exceeds the new post_max_size. If the request fails with a 413 instead of a PHP message, the proxy in front is rejecting it first.
Is it safe to set max_execution_time to 0?
On the command line, yes, that is already the default. On a web server, no: a stuck request holds a worker forever and enough of them exhaust the pool. Raise it to a bounded value for the job that needs it, and move genuinely long work to a scheduled task.
Which PHP version should I run?
The newest one all your dependencies support, which in practice means 8.2 or 8.3 for most sites. Each version is meaningfully faster than the last. Test on a staging copy with deprecation notices visible before upgrading production, because the breakages are in plugins and themes, not in PHP.
Do I need to tune anything if my host manages PHP?
Usually only memory_limit, the upload sizes and max_input_vars, and those are often exposed as settings rather than a file. OPcache and the pool sizing are normally set sensibly by the host already, and changing them without measuring is how a working site becomes a slow one.




კომენტარები
სრულიად ანონიმურად: ანგარიშის, ელფოსტის და cookie-ის გარეშე. ინახება მხოლოდ სახელი, ტექსტი და დრო - სხვა არაფერი. ბმულების რაოდენობა ლიმიტირებულია.