Fix WordPress “exceeds upload_max_filesize”: raise limits correctly (PHP, Nginx/Apache, hosting)

Was this helpful?

Nothing says “I love deadlines” like a marketing team trying to upload a 120 MB hero video five minutes before launch—only to get “exceeds the maximum upload size for this site” or the classic “exceeds upload_max_filesize” message.

This error is never one knob. It’s a chain. PHP has limits. The web server has limits. Proxies and CDNs have limits. WordPress has opinions. Your hosting panel has “helpful” defaults. Fixing it correctly means finding the tightest link, raising it intentionally, and verifying what actually changed.

Fast diagnosis playbook

If you do nothing else, do this. It turns “I changed php.ini and nothing happened” into a short, boring ticket. Boring is good.

1) Identify the real error surface: WordPress message, HTTP status, or server log

  • If the browser shows HTTP 413 or a plain Nginx/Cloudflare page: you’re blocked before PHP. Fix proxy/web server first.
  • If WordPress shows “exceeds upload_max_filesize” or “exceeds the maximum upload size”: you’re likely hitting PHP limits (or WordPress is reading PHP limits and reporting them).
  • If it spins then fails with “link expired” / “failed to write file to disk” / “missing a temporary folder”: not a size limit; that’s filesystem, tmp, or permissions.

2) Find the smallest limit in the chain

Uploads fail at the minimum of:

  • Browser/client quirks (rare, but mobile can time out)
  • CDN/WAF/proxy request body limit
  • Web server request body limit (Nginx/Apache)
  • PHP SAPI limits: upload_max_filesize, post_max_size, memory_limit, timeouts
  • Disk constraints: free space, tmp partition, inode exhaustion
  • WordPress + plugin constraints (less common, but real)

3) Verify what PHP is actually using (don’t guess)

PHP can load different ini files depending on SAPI (FPM vs CLI), pool, and distro packaging. Confirm the loaded configuration and the effective values.

4) Change one layer at a time and restart the right service

Restarting the wrong daemon is a rite of passage. It’s also a waste of time. Change the correct config, validate syntax, reload/restart, then test.

5) Re-test with an upload size that should fail, then one that should pass

Testing with a 2 MB file after you “raised it to 512 MB” tells you nothing. Pick a 50–80% of the new limit file, and try again.

One quote worth keeping in your runbook: Hope is not a strategy. (General H. Norman Schwarzkopf)

What the error really means (and what it doesn’t)

“exceeds upload_max_filesize” is PHP speaking—specifically the file upload subsystem. It’s thrown when PHP refuses to accept a file because it’s larger than upload_max_filesize. WordPress typically surfaces that as a friendly message in the Media uploader.

But production reality is messy. You can see this message even if:

  • Your proxy blocks the request body and WordPress guesses the limit based on PHP settings (you raise PHP, WordPress displays a bigger number, but uploads still die at the edge).
  • You edited the wrong php.ini file (common on multi-PHP hosts, containers, or when CLI != FPM).
  • post_max_size is still smaller than upload_max_filesize (your upload is within upload_max_filesize, but the full POST body isn’t).
  • You’re hitting timeouts (large uploads over slow links), not size caps.

Also: the error says nothing about disk. You can increase limits to 2 GB and still fail because /tmp is tiny or the volume is full. File uploads are not magic; they land somewhere.

The upload limit chain: where uploads get blocked

Think of a WordPress upload like a package moving through checkpoints. Any checkpoint can reject it. Your job is to find which guard is stopping it.

Checkpoint A: CDN/WAF/reverse proxy

If you have Cloudflare, a managed WAF, an ALB/ELB, an API gateway, or an Nginx ingress controller, you likely have a request size cap and sometimes a request buffering behavior that changes how big uploads behave.

Checkpoint B: Web server

Nginx defaults to a fairly small request body limit in many configs (client_max_body_size). Apache can also enforce body size via directives such as LimitRequestBody (though not always set by default).

Checkpoint C: PHP SAPI (FPM/mod_php)

The classic trio:

  • upload_max_filesize: per-file cap.
  • post_max_size: total POST body cap (must be larger than upload_max_filesize; add headroom).
  • memory_limit: affects processing and some plugins; not always required to be larger than file size, but practically you want headroom.

Also watch:

  • max_execution_time, max_input_time: timeouts for slow uploads and slow PHP processing.
  • max_file_uploads: number of files in one request (rarely a WordPress media issue, but bulk upload plugins can hit it).
  • upload_tmp_dir: where PHP stores upload temp files; if it points to a small filesystem, you lose.

Checkpoint D: Filesystem and OS

Uploads write to temporary space, then to wp-content/uploads (or wherever configured). Failure modes:

  • Full disk or inode exhaustion
  • Wrong permissions/ownership
  • noexec mount flags usually don’t matter for media, but SELinux/AppArmor policies can
  • Container overlay storage filling up while your attached volume has space

Checkpoint E: WordPress and plugins

WordPress reports “Maximum upload file size” based on what PHP tells it. Plugins can add their own validation, and some security plugins block certain MIME types or extensions, which users sometimes misinterpret as “size” errors.

Interesting facts & small history of upload limits

  1. PHP’s default upload limit used to be tiny by design. Defaults like 2 MB were common because shared hosting in the 2000s was a resource knife fight.
  2. post_max_size matters even for single-file uploads. The file is part of a multipart form; the full body includes boundaries and fields, so you need headroom.
  3. Nginx rejects oversized bodies early. When client_max_body_size is exceeded, Nginx can return 413 without ever touching PHP-FPM. That’s good for protecting backends, confusing for humans.
  4. “413 Request Entity Too Large” is older than most WordPress sites. It’s an HTTP status from the era when “entity” was common spec language for request payloads.
  5. WordPress doesn’t “set” PHP limits. It reads them. Any dashboard setting that claims to change PHP is either writing config files or is lying politely.
  6. PHP-FPM and PHP CLI are different SAPIs. The CLI can show you sane values while FPM uses different ini files. Many on-call incidents start with someone running php -i and trusting it.
  7. Uploads commonly hit temp storage first. PHP writes to a temp file before moving it. If /tmp is on a small partition, large uploads fail even if the final volume has space.
  8. CDNs often cap request size on lower plans. You can raise Nginx and PHP all day and still lose at the edge. The edge doesn’t negotiate.
  9. Large media uploads became normal only recently. Modern phones shoot 4K video by default; 200 MB “quick clips” are now standard and will destroy 2 MB defaults with enthusiasm.

Practical tasks: commands, outputs, decisions (12+)

These are the moves I expect an SRE to run on a real host. Each task includes: the command, what output means, and the decision you make.

Task 1: Confirm the effective PHP limits (CLI sanity check)

cr0x@server:~$ php -r 'echo "upload_max_filesize=",ini_get("upload_max_filesize"),"\npost_max_size=",ini_get("post_max_size"),"\nmemory_limit=",ini_get("memory_limit"),"\n";'
upload_max_filesize=2M
post_max_size=8M
memory_limit=128M

What it means: This is PHP CLI’s view. It might not match PHP-FPM or mod_php.

Decision: If CLI is already high but WordPress still fails, stop editing random ini files and check the web SAPI next.

Task 2: Identify which PHP SAPI you’re running (FPM vs Apache module)

cr0x@server:~$ ps -ef | egrep 'php-fpm|apache2|httpd' | head
root        1012       1  0 09:00 ?        00:00:02 php-fpm: master process (/etc/php/8.2/fpm/php-fpm.conf)
www-data    1044    1012  0 09:00 ?        00:00:00 php-fpm: pool www
root        1200       1  0 09:00 ?        00:00:01 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;

What it means: You’re on Nginx + PHP-FPM. Edits must target FPM ini/pool config, not Apache.

Decision: Proceed to FPM configuration locations and reload PHP-FPM and Nginx.

Task 3: Find the loaded php.ini for PHP-FPM (not CLI)

cr0x@server:~$ php-fpm8.2 -i | egrep 'Loaded Configuration File|Scan this dir for additional .ini files'
Loaded Configuration File => /etc/php/8.2/fpm/php.ini
Scan this dir for additional .ini files => /etc/php/8.2/fpm/conf.d

What it means: That is the ini file and scanned directory for the FPM SAPI.

Decision: Edit /etc/php/8.2/fpm/php.ini or create a dedicated conf.d override, then reload PHP-FPM.

Task 4: Check the effective values in the FPM ini file

cr0x@server:~$ sudo egrep -n '^(upload_max_filesize|post_max_size|memory_limit|max_execution_time|max_input_time)' /etc/php/8.2/fpm/php.ini
861:upload_max_filesize = 2M
701:post_max_size = 8M
436:memory_limit = 128M
409:max_execution_time = 30
415:max_input_time = 60

What it means: These are current configured defaults for FPM.

Decision: Raise them to values aligned with your expected uploads, then reload PHP-FPM. Also make sure post_max_size is larger than upload_max_filesize.

Task 5: Check Nginx request body limit across the config

cr0x@server:~$ sudo nginx -T 2>/dev/null | egrep -n 'client_max_body_size'
45:    client_max_body_size 1m;
212:        client_max_body_size 16m;

What it means: You have multiple scopes. The most specific matching location/server block wins.

Decision: Set client_max_body_size where your WordPress vhost actually matches (usually the server block). Remove contradictory values if you can.

Task 6: Validate Nginx config before reloading

cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

What it means: Safe to reload. If syntax fails, do not reload; fix the file first.

Decision: Reload Nginx only after a clean -t.

Task 7: Reload services correctly (and verify they actually reloaded)

cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ sudo systemctl reload php8.2-fpm
cr0x@server:~$ systemctl is-active nginx php8.2-fpm
active
active

What it means: Reload succeeded and services are still active.

Decision: If reload fails or service is not active, stop—fix the service first before testing uploads.

Task 8: Inspect PHP-FPM pool overrides (they can silently cap values)

cr0x@server:~$ sudo egrep -n 'php_admin_value\[(upload_max_filesize|post_max_size|memory_limit)\]|request_terminate_timeout' /etc/php/8.2/fpm/pool.d/www.conf
;php_admin_value[upload_max_filesize] = 10M
;php_admin_value[post_max_size] = 12M
;request_terminate_timeout = 0

What it means: Commented out here, but on some systems these are active and override php.ini.

Decision: If pool overrides exist, set values there intentionally, document them, and stop pretending php.ini is authoritative.

Task 9: Check Apache body size directives (if you run Apache)

cr0x@server:~$ sudo apachectl -M | head -n 5
Loaded Modules:
 core_module (static)
 so_module (static)
 watchdog_module (static)
 http_module (static)

What it means: Apache is present; if it’s your frontend, you need to check Apache limits and PHP integration (mod_php or proxy_fcgi).

Decision: If you’re on Apache, search for LimitRequestBody and check your vhost config.

Task 10: Search Apache config for request size limits

cr0x@server:~$ sudo grep -R --line-number -E 'LimitRequestBody' /etc/apache2/sites-enabled /etc/apache2/apache2.conf 2>/dev/null
/etc/apache2/sites-enabled/000-default.conf:27:LimitRequestBody 10485760

What it means: Apache is hard-capped to 10 MB on that vhost. PHP changes won’t matter.

Decision: Raise LimitRequestBody (or remove it if you manage limits elsewhere), then reload Apache.

Task 11: Confirm disk space and inode availability (uploads need both)

cr0x@server:~$ df -h /var/www/html /tmp
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda1        80G   62G   15G  81% /
tmpfs           1.0G  120M  904M  12% /tmp
cr0x@server:~$ df -i /var/www/html /tmp
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/vda1      5242880 910000 4332880   18% /
tmpfs           262144    980 261164    1% /tmp

What it means: You have 15 GB free and inodes are fine. /tmp is 1 GB, which might be tight for large uploads.

Decision: If you want to allow 2 GB uploads, you must enlarge temp storage or set upload_tmp_dir to a bigger filesystem.

Task 12: Check WordPress’s reported max upload size (what the admin sees)

cr0x@server:~$ sudo -u www-data php /var/www/html/wp-admin/includes/file.php 2>/dev/null || true

What it means: This isn’t a great way to query WordPress, but it reminds you: WordPress is PHP code. The admin UI value is derived from PHP settings, not vice versa.

Decision: Use a proper phpinfo() check in the web context if needed (see next task), then remove it.

Task 13: Create a temporary phpinfo page to confirm FPM values (then delete it)

cr0x@server:~$ sudo tee /var/www/html/phpinfo.php >/dev/null <<'EOF'
/etc/php/8.2/fpm/php.ini 

What it means: You’re now inspecting PHP as served by the web stack, which is what matters.

Decision: Confirm the values; then delete phpinfo.php. Leaving it around is an invitation to trouble.

Task 14: Inspect logs for 413 or PHP upload errors

cr0x@server:~$ sudo tail -n 50 /var/log/nginx/error.log
2025/12/27 09:22:18 [error] 1432#1432: *991 client intended to send too large body: 24681231 bytes, client: 203.0.113.9, server: example.com, request: "POST /wp-admin/async-upload.php HTTP/1.1", host: "example.com", referrer: "https://example.com/wp-admin/media-new.php"

What it means: This is not PHP. Nginx is rejecting the request body (here ~24.6 MB).

Decision: Raise client_max_body_size in the right scope. Don’t touch PHP until Nginx stops being the bouncer.

Task 15: Validate PHP error log for upload-specific failures

cr0x@server:~$ sudo tail -n 50 /var/log/php8.2-fpm.log
[27-Dec-2025 09:25:10] WARNING: [pool www] child 2123 said into stderr: "PHP message: PHP Warning:  POST Content-Length of 52459012 bytes exceeds the limit of 8388608 bytes in Unknown on line 0"

What it means: post_max_size is 8 MB (8388608 bytes). Your user tried ~52 MB. PHP rejected it before WordPress could do anything meaningful.

Decision: Increase post_max_size above the desired maximum upload, with headroom.

Task 16: Confirm a proxy/CDN isn’t enforcing a smaller cap (example: Cloudflare via response headers)

cr0x@server:~$ curl -I -sS https://example.com/wp-admin/media-new.php | egrep -i 'server:|cf-ray|via:|x-cache'
server: cloudflare
cf-ray: 88f0c1d2ab123456-LHR

What it means: Cloudflare is in the path. Upload size limits might be plan-dependent and can manifest as 413/520 style errors.

Decision: If you see edge involvement, reproduce by bypassing it (direct origin test) or check the edge’s body limit settings. Don’t assume the origin is the problem.

Joke 1: Upload limits are like corporate expense policies—everyone discovers them only when they try to do something normal.

Fix by stack: Apache, Nginx, PHP-FPM, proxies, panels, containers

Pick sane target limits (and stop using “just set it to 2G” as a plan)

Start with an actual requirement: “We need to upload up to 200 MB videos” or “We need 64 MB PDFs.” Then set:

  • upload_max_filesize = requirement (e.g., 256M)
  • post_max_size = upload_max_filesize + overhead (e.g., 300M)
  • memory_limit = depends on plugins and image processing; for media-heavy WP, 256M–512M is common
  • Web server/proxy body limit = at least post_max_size

If you permit huge uploads, also plan for bandwidth, timeouts, and storage growth. “Uploads work” is not the same as “uploads won’t melt the server next month.”

Nginx + PHP-FPM (most common modern setup)

1) Raise Nginx client_max_body_size

Set it in the correct server block (or http block if you want global). Example vhost snippet:

cr0x@server:~$ sudo tee /etc/nginx/sites-available/example.com.conf >/dev/null <<'EOF'
server {
    listen 80;
    server_name example.com;
    root /var/www/html;

    client_max_body_size 300m;

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
    }
}
EOF

Decision: Set Nginx to at least post_max_size. If you set Nginx lower than PHP, users get 413; if you set PHP lower than Nginx, users get PHP/WordPress errors. Either way, you’ll get a ticket.

2) Raise PHP-FPM limits in the right ini

Edit the FPM ini or drop a dedicated override file in conf.d (cleaner for config management).

cr0x@server:~$ sudo tee /etc/php/8.2/fpm/conf.d/99-wordpress-uploads.ini >/dev/null <<'EOF'
upload_max_filesize = 256M
post_max_size = 300M
memory_limit = 512M
max_execution_time = 120
max_input_time = 120
EOF

Decision: Use a clearly named override. It makes audits and incident response faster than spelunking a vendor-shipped php.ini.

3) Reload, then verify via phpinfo in web context

Reload FPM and Nginx. Confirm in served phpinfo that the new values are active. Then delete phpinfo.

Apache setups

Apache can run PHP via mod_php (old school) or via PHP-FPM (proxy_fcgi). The limits differ depending on integration.

Apache request body caps

If you have LimitRequestBody in your vhost or directory context, it’s a hard ceiling. Raise it (bytes) or remove it if you’re controlling limits elsewhere.

cr0x@server:~$ sudo sed -i 's/LimitRequestBody 10485760/LimitRequestBody 314572800/' /etc/apache2/sites-enabled/000-default.conf
cr0x@server:~$ sudo apachectl configtest
Syntax OK
cr0x@server:~$ sudo systemctl reload apache2

Decision: If you use Apache as a reverse proxy in front of FPM, align Apache’s body size with PHP’s post_max_size.

PHP limits (mod_php vs FPM)

If you run mod_php, the ini path is usually /etc/php/X.Y/apache2/php.ini. If you run FPM, it’s /etc/php/X.Y/fpm/php.ini. Don’t “fix” the Apache ini if Apache is not serving PHP directly.

Reverse proxies and load balancers

Common culprits:

  • Nginx in front of Nginx (yes, really). Each layer can have its own client_max_body_size.
  • Kubernetes ingress controllers with their own request body settings.
  • WAF rules that block large bodies or certain multipart patterns.

The method is the same: find which layer emits 413 (or resets the connection), then raise that layer. If you can bypass the proxy temporarily (direct origin), do it to narrow scope.

cPanel / Plesk / managed hosting panels

Panels often provide “increase upload size” toggles. Under the hood they edit ini files or per-site configs. The trap is split-brain configuration:

  • Panel writes to one ini, but your site uses a different PHP version or handler.
  • Panel raises PHP, but Nginx/Apache still caps the body.
  • Panel changes are overwritten by updates or by your config management.

Use the panel if that’s your governance model, but still verify with phpinfo and server logs. Panels are UI, not truth.

Docker and containerized WordPress

Containers add two classic wrinkles:

  • You might be editing a config file on the host that the container doesn’t use.
  • The frontend proxy (Traefik/Nginx/Ingress) often has the real cap, not the PHP container.

If you run the official WordPress image with Apache, you typically adjust PHP via ini snippets mounted into the container.

cr0x@server:~$ docker exec -it wordpress php -i | egrep 'Loaded Configuration File|upload_max_filesize|post_max_size' | head
Loaded Configuration File => /usr/local/etc/php/php.ini
upload_max_filesize => 2M => 2M
post_max_size => 8M => 8M

Decision: If the container still shows 2M/8M, you need to mount an ini override into /usr/local/etc/php/conf.d (or rebuild the image), then restart the container.

WordPress-layer constraints: what matters, what’s cargo cult

WordPress “Maximum upload file size” is a symptom display

In Media → Add New, WordPress prints a maximum. That number is usually derived from PHP’s upload_max_filesize and post_max_size, sometimes with extra logic. It’s not a guarantee that the entire path supports that size.

wp-config.php “hacks”

You will see advice to add constants or ini_set calls in wp-config.php:

  • ini_set('upload_max_filesize','256M')
  • define('WP_MEMORY_LIMIT','256M')

Here’s the truth: ini_set() cannot change PHP_INI_SYSTEM settings in many configurations, and FPM pool php_admin_value can block it entirely. If it works, it’s fragile. If it doesn’t, people waste days believing WordPress ignored them out of spite.

Prefer the boring, enforceable approach: set PHP limits in ini/pool config, set web server request limits in web server config, verify in logs.

Memory limit is not an upload limit (but can still kill uploads)

Uploading a file doesn’t require PHP to hold the entire file in memory; it streams to a temp file. But plugins may process files (image resizing, video metadata, PDF previews) and can eat memory. If you raise upload limits and suddenly see white screens or fatal errors after upload, that’s often memory or execution time during processing, not the upload itself.

MIME type and extension blocks masquerade as “upload errors”

Some hosts block certain extensions. WordPress also has upload MIME restrictions. When a user hears “upload failed,” they assume size. Check the actual message and logs. A blocked SVG, EXE, or odd video container is not a size problem.

Joke 2: “Just raise the limit” is the operational equivalent of “have you tried turning it off and on?”—sometimes correct, always suspicious.

Common mistakes (symptom → root cause → fix)

1) Symptom: WordPress says max upload is 256MB, but uploads fail with HTTP 413

Root cause: Nginx/edge proxy request body limit is lower than PHP.

Fix: Raise client_max_body_size (and any upstream proxy cap). Confirm in Nginx error log you no longer see “client intended to send too large body”.

2) Symptom: You edited php.ini, restarted PHP, nothing changed

Root cause: Edited the wrong ini file (CLI vs FPM vs Apache), or a pool override is superseding it.

Fix: Check the loaded configuration file for the web SAPI with phpinfo; search pool configs for php_admin_value.

3) Symptom: Upload fails at ~8MB or ~16MB exactly, regardless of settings

Root cause: Upstream device cap (WAF, ALB, ingress controller), or Apache LimitRequestBody.

Fix: Identify the layer returning 413 via logs and headers; raise that cap. Don’t blindly raise PHP first.

4) Symptom: Upload starts, then fails with “The link you followed has expired.”

Root cause: Often timeouts or nonce expiration during slow uploads; sometimes max_execution_time or proxy timeouts.

Fix: Increase PHP timeouts and proxy read timeouts; ensure the user isn’t uploading over a painfully slow link relative to server timeouts.

5) Symptom: Upload fails with “Missing a temporary folder” or “Failed to write file to disk”

Root cause: upload_tmp_dir misconfigured, permissions wrong, disk full, or /tmp too small.

Fix: Check df -h, df -i, permissions on /tmp and wp-content/uploads, set upload_tmp_dir to a writable path with space.

6) Symptom: Only large images fail; small images succeed

Root cause: Image processing after upload hits memory_limit or max_execution_time; or a plugin does heavy processing.

Fix: Raise memory_limit, profile plugin behavior, consider offloading image resizing or limiting auto-generated sizes.

7) Symptom: Works when bypassing CDN, fails through CDN

Root cause: Edge body size cap or WAF rule blocks multipart/form-data.

Fix: Adjust CDN/WAF settings, or route admin/media uploads directly to origin if your architecture allows it.

Checklists / step-by-step plan

Step-by-step: raise limits safely on Nginx + PHP-FPM

  1. Pick a target upload size. Example: 256M. Decide post_max_size=300M.
  2. Check current blockers: Nginx error log for 413; PHP-FPM log for “POST Content-Length exceeds”.
  3. Set Nginx limit in the correct vhost: client_max_body_size 300m;
  4. Set PHP-FPM ini override: upload_max_filesize, post_max_size, timeouts, and optionally memory_limit.
  5. Validate and reload: nginx -t, reload Nginx and PHP-FPM.
  6. Verify via web-context phpinfo (temporarily) and delete it.
  7. Test uploads at 50–80% of limit and just over limit to confirm failure behavior is sane.
  8. Watch disk: Ensure /tmp and your WordPress volume can handle concurrent uploads.
  9. Document the new limits in a runbook, including where they are configured.

Checklist: what to align across layers (minimum viable correctness)

  • client_max_body_size (or Apache equivalent) ≥ post_max_size
  • post_max_sizeupload_max_filesize + overhead
  • Temp storage capacity ≥ maximum upload (times expected concurrency)
  • PHP timeouts and proxy timeouts consistent with upload durations
  • Origin, proxy, and CDN settings consistent (no hidden smaller cap)

Checklist: operational guardrails (so “raising limits” doesn’t become “oops”)

  • Decide whether you want to allow large uploads in admin only (some stacks can scope limits by location).
  • Add monitoring: disk usage, inode usage, and error rates for 413 and PHP upload warnings.
  • Keep a rollback plan: revert ini snippet, revert vhost directive, reload services.
  • If you allow very large uploads, consider offloading media to object storage and using multipart uploads.

Three corporate-world mini-stories

Mini-story 1: The incident caused by a wrong assumption

A retail company ran WordPress as the “lightweight CMS” behind a reverse proxy tier. One day, product launched a new landing page with short videos. Uploads started failing intermittently, mostly for remote staff. The on-call engineer saw the WordPress error about upload_max_filesize, edited /etc/php/8.1/fpm/php.ini, bumped the limits, reloaded PHP-FPM, and declared victory.

The next upload still failed. Same message. They bumped the limits again, because if 256M didn’t work, surely 512M would. It didn’t. Now the team had two problems: uploads still failing, and PHP configured to accept bigger payloads without any discussion about disk, timeouts, or abuse resistance.

The root cause was painfully mundane: the reverse proxy tier had client_max_body_size set to 20M. WordPress was reporting PHP’s new higher limit, but the proxy blocked requests at 20M. Users close to HQ occasionally succeeded because their files were smaller and their workflow differed; remote folks uploaded larger assets more often.

The fix took ten minutes: raise the proxy limit to match post_max_size, reload, and add a log alert on 413 responses. The real lesson took longer: stop assuming the error message points to the layer at fault. In multi-tier systems, the first component to reject the request writes the truth, and WordPress is usually not that component.

Mini-story 2: The optimization that backfired

A media-heavy site decided to “optimize performance” by tightening limits everywhere. They dropped Nginx client_max_body_size to 10M because “nobody should upload bigger than that” and it reduced some bot noise. They also lowered PHP timeouts because long-running requests were considered suspicious.

Then the business started doing webinars. Suddenly, marketing had legitimate 80M thumbnails-for-video (don’t ask) and 150M audio uploads. Staff worked around it by using FTP and manually moving files into wp-content/uploads, which bypassed WordPress metadata generation and broke the media library. Great for uptime, terrible for correctness.

The real backfire was operational: the workaround increased manual steps and created inconsistent state. Some files existed on disk but not in the database. Some were referenced by URLs that never got regenerated after a domain change. When they finally raised limits again, they inherited a pile of “ghost media” and a trust problem: users no longer believed the upload system worked, even when it did.

The eventual solution was not “open the floodgates.” They set a clear upload policy (256M), aligned limits across edge, proxy, and PHP, and added a dedicated media offload later. Optimization is fine. Optimization without understanding workflows is how you end up with a fast system that can’t do the job.

Mini-story 3: The boring but correct practice that saved the day

A large corporate intranet ran multiple WordPress instances on the same fleet. Nothing exciting. The platform team kept a small set of hardened defaults, including upload size limits. But they also enforced a rule: every limit change must be made via a single configuration snippet deployed by automation, and verified by a health check that reads the effective values.

When a new site demanded larger training video uploads, the team didn’t touch php.ini directly. They added a single ini file in conf.d, bumped Nginx vhost settings, and updated a canary check that uploads a file slightly under the limit to a staging instance. Then they rolled it out gradually.

Two weeks later, a base image update replaced the vendor php.ini. The sites that had hand-edited that file would have reverted to 2M and set off a helpdesk fire drill. This fleet didn’t. The override snippet survived, the canary check kept passing, and nobody noticed.

This is what “boring” buys you: immunity to the kind of change that ruins a Friday night. People love clever fixes. Production loves repeatable ones.

FAQ

1) What’s the difference between upload_max_filesize and post_max_size?

upload_max_filesize limits a single uploaded file. post_max_size limits the entire HTTP POST body. For file uploads, the POST body is always larger than the file because of multipart overhead. Set post_max_size higher.

2) I raised upload_max_filesize but WordPress still shows the old limit. Why?

Either you edited the wrong PHP ini (CLI vs FPM vs Apache), the change didn’t reload, or another config (FPM pool php_admin_value) overrides it. Confirm using a web-context phpinfo page.

3) Why do I get HTTP 413 instead of a WordPress error?

Because something in front of WordPress rejected the request body before PHP saw it—often Nginx client_max_body_size, Apache LimitRequestBody, a load balancer, or a CDN.

4) Do I need to increase memory_limit to match my upload size?

Not strictly for the upload transfer itself, but often for what happens after: image resizing, metadata extraction, PDF previews, plugin processing. If uploads succeed but processing fails, memory is a suspect.

5) How high should I set upload limits for WordPress?

Set them to the smallest value that supports real user needs with some headroom. For many sites, 64M–256M is practical. If you need bigger, consider media offload to object storage and review timeouts and disk capacity.

6) Can I set limits in .htaccess?

Sometimes, on Apache with mod_php, directives like php_value upload_max_filesize in .htaccess can work. On PHP-FPM, they usually won’t. Treat .htaccess tuning as legacy and verify results.

7) Why do uploads fail only for some users or networks?

Slow uplinks can hit timeouts. Corporate proxies can interfere. Also, users may upload different files. Check timeouts, check whether edge protections vary by region, and test from the origin network path.

8) Is raising upload limits a security risk?

It can be. Larger bodies mean more resource exposure: bandwidth, temp disk, CPU for processing, and longer request lifetimes. Set limits intentionally, scope them if possible (admin paths), and monitor.

9) How do I know which layer is blocking the upload?

Look for 413 in the browser and in Nginx/Apache logs. If Nginx logs “client intended to send too large body,” it’s Nginx. If PHP logs “POST Content-Length exceeds,” it’s PHP. If neither, suspect CDN/WAF or filesystem issues.

10) Why did it work yesterday and fail today with no changes?

Common reasons: a package update replaced configs, a panel reset settings, disk filled up, or a proxy/WAF policy changed. This is why configuration snippets + verification checks are worth the effort.

Conclusion: next steps that won’t bite you later

Fixing “exceeds upload_max_filesize” isn’t about finding the magic php.ini line. It’s about aligning limits across the whole request path and proving the effective configuration in the web context.

Do these next:

  1. Run the fast diagnosis playbook and identify whether the blocker is edge, web server, or PHP.
  2. Set a clear target size and align client_max_body_size/LimitRequestBody, post_max_size, and upload_max_filesize.
  3. Reload the right services, then verify using served phpinfo (briefly) and logs.
  4. Check disk and temp storage. If you allow big uploads, plan for concurrency and growth.
  5. Write down where the settings live. Future-you is a stranger; leave them a map.
← Previous
Subscription fatigue: how the industry rented you your own tools
Next →
Proxmox LDAP/AD Login Fails: Where the Auth Chain Breaks and How to Fix

Leave a comment