413 Request Entity Too Large: WordPress Said 2 MB, nginx Said 1 MB

20 min read

A 1.5 MB product photo failed to upload on my test store, on a screen that said Maximum upload file size: 2 MB. The progress bar in Media > Add New reached 100%, and then a red box appeared: "Unexpected response from the server. The file may have been uploaded successfully. Check in the Media Library or reload the page." I checked the Media Library. The photo wasn't there.

The refusal came from nginx, the web server that receives every request before WordPress does, and its answer was 413 Request Entity Too Large. nginx has its own size limit for every request (client_max_body_size, 1 MB unless someone changes it), and WordPress never asks nginx what that limit is. The 2 MB on the screen is PHP's limit.

PHP has two more limits after nginx, and Cloudflare adds one more before nginx when you use its proxy. Each limit stops an upload with a different message. I collected seven messages, and only nginx's plain error page contains the number 413. So which limit stopped your upload?

I wrote this for owners of WooCommerce and WordPress stores on their own server with nginx and PHP-FPM, for example a VPS that you or a developer installed. For each limit, you'll see the message on your screen, the log line that shows the limit and the setting that raises it. On Shopify, or with a host that doesn't let you edit server files, you can't change these settings yourself, but your host's support will ask for the log lines I show here.

The short answer, which I tested: put client_max_body_size 72m; in your store's nginx server block, set upload_max_filesize = 64M and post_max_size = 72M in PHP-FPM's settings, and reload nginx and PHP-FPM. After that, Media > Add New should show 64 MB.

I triggered each limit separately on a test server that I configured like most of these stores: Ubuntu 24.04 with nginx 1.24 and PHP 8.3 from Ubuntu's own packages, every config file unchanged from the package, WordPress 7.1.3 and WooCommerce 11.2.1, with the whole server inside Docker on my desktop PC. I used real files: a product photo, the Storefront theme zip, the WooCommerce plugin zip itself and two product CSVs that I made from WooCommerce's own sample file.

Four upload limits, in the order a file reaches them

On its way from your computer to WordPress, a file goes through as many as four limits. The first limit that refuses the file decides which message you'll see. These are the four limits with their defaults (for nginx and PHP, on a new Ubuntu server):

  1. Cloudflare, when your domain uses its proxy: 100 MB on the Free and Pro plans.
  2. nginx, client_max_body_size: 1 MB for the whole request.
  3. PHP, post_max_size: 8 MB, again for the whole request, the file and the form fields together.
  4. PHP, upload_max_filesize: 2 MB for each file.

The Media Library and the block editor also have their own check, before all four limits. In the browser, they compare the file with WordPress's limit and don't send the file at all when it's bigger. WordPress gets that limit from the function wp_max_upload_size(), which returns the smaller of the two PHP numbers, and that's the whole problem: WordPress only knows the two PHP limits. It can't see the limits of nginx or Cloudflare, so its screen can show 2 MB while nginx refuses every request over 1 MB.

413 Request Entity Too Large comes from nginx

nginx checks the size of a request before it passes anything to PHP, and it writes every refusal to its error log. Here's the line on my test server for the same photo, sent to the address the Media Library uploads to:

root@server:~# tail -n 1 /var/log/nginx/error.log
2026/10/28 00:07:27 [error] 651#651: *1008 client intended to send too large body: 1537623 bytes, client: 172.23.0.1, server: example.com, request: "POST /wp-admin/async-upload.php HTTP/1.1", host: "example.com"

The request was 1,537,623 bytes for a photo of 1,537,292, because nginx counts the whole request: the file and the form fields that the uploader sends with it. On Ubuntu, that log is /var/log/nginx/error.log, unless your site's server block sets its own error_log.

If you can read this log, a line with "client intended to send too large body" shows the cause, and Step 1 is your fix. (A 502 during an upload has other causes and other log lines: the six 502 Bad Gateway causes I reproduced on a test store.)

What you see on screen depends on where you uploaded. Upload Theme, Upload Plugin and WooCommerce's Products > Import send the file as an ordinary form, so the browser leaves the WordPress admin and shows nginx's own page, black text on white. The nginx documentation warns that "browsers cannot correctly display this error", but Chrome showed it for every form I tried.

With curl, you get the same page with its headers (I removed the Date line). probe.php is a short script on my test server that prints what PHP received, and I'll use it again for the PHP limits:

$ curl -s -i -F "[email protected]" http://example.com/probe.php
HTTP/1.1 413 Request Entity Too Large
Server: nginx/1.24.0 (Ubuntu)
Content-Type: text/html
Content-Length: 192
Connection: close

<html>
<head><title>413 Request Entity Too Large</title></head>
<body>
<center><h1>413 Request Entity Too Large</h1></center>
<hr><center>nginx/1.24.0 (Ubuntu)</center>
</body>
</html>

The Media Library and the block editor upload in the background, so you won't see that page there. They show their own guess instead: the "Unexpected response from the server" box with the bar at 100%, and in the block editor a black notice, "The response is not a valid JSON response." Both mean that the server answered with nginx's HTML page, and WordPress expected its own data.

The status has had three names. The HTTP standard called it Request Entity Too Large in 1999 and Payload Too Large in 2014, and the current one, RFC 9110, calls it 413 Content Too Large. nginx still prints the oldest name. It's one status code, so a search for "payload too large", "content too large" or "http 413" leads to the same fix.

"Request failed with status code 413" is axios's message for this status. axios is a JavaScript library that many plugin screens and web apps use to send requests to the server, and that sentence is in its source code. When a plugin shows it during an upload, the cause is usually the same nginx limit.

Step 1: Raise client_max_body_size in nginx

Put client_max_body_size into the server block of your store, in the file in /etc/nginx/sites-available/ that contains your domain name. It also works in the http block of /etc/nginx/nginx.conf, where it applies to every site on the server. The sample nginx.conf in the WordPress Advanced Administration Handbook puts it there, with a comment that PHP's upload limit can't be larger.

A value in the server block replaces the value from the http block, and a value in a location block replaces both. This is the server block from my test server, with the new line:

server {
    listen 80 default_server;
    server_name example.com;
    root /var/www/shop;
    index index.php;
    client_max_body_size 72m;

    location / {
        try_files $uri $uri/ /index.php?$args;
    }

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

Test the file, reload, and send the photo again. The same curl got 413 before the reload and 200 after it:

root@server:~# nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
root@server:~# systemctl reload nginx

$ curl -s -o /dev/null -w '%{http_code}\n' -F '[email protected]' http://example.com/probe.php
200

My test server is a container without systemd, so I used nginx -s reload there, which does the same reload. During a reload, nginx keeps answering requests while it reads the new settings, so a reload is safe on a live store.

Why 72m? It's the post_max_size I set in the next step, so nginx never refuses a request that PHP would accept. I wouldn't use 0, which switches the check off (the nginx documentation says so): nginx would then accept a request of any size and write it to disk before PHP checks it. My nginx configuration for WordPress and WooCommerce explains every other line of that server block.

The uploaded file exceeds the upload_max_filesize directive in php.ini

This sentence comes from PHP's limit for each file, upload_max_filesize, which is 2 MB on Ubuntu. WordPress shows the sentence exactly like this in only one of the places I tried. After I raised the nginx limit, I uploaded the Storefront theme (WooCommerce's own free theme, a 2.9 MB zip) through Upload Theme and got a white page with only that sentence. The HTTP status was 500, the code for a server error, although nothing on the server was broken.

A plain WordPress error box on a phone with the text The uploaded file exceeds the upload_max_filesize directive in php.ini.
Upload Theme with the 2.9 MB Storefront zip and PHP's default upload_max_filesize of 2M

Other screens show other messages. WooCommerce's product import took the 2.8 MB CSV and answered "File is empty. Please upload something more substantial", followed by a sentence that blames disabled uploads or a post_max_size smaller than upload_max_filesize. The file wasn't empty, and neither guess was right. The Media Library and the block editor don't send a file over the limit at all: they stop it in the browser with "photo-4.7mb.jpg exceeds the maximum upload size for this site."

WooCommerce Import Products on a phone, step Upload CSV file: a red box says File is empty. Please upload something more substantial. This error could also be caused by uploads being disabled in your php.ini or by post_max_size being defined as smaller than upload_max_filesize in php.ini. Under the file field: Maximum size: 2 MB
Products > Import with a 2.8 MB CSV: upload_max_filesize, reported as an empty file

What surprised me is that upload_max_filesize writes no line to any error log, neither to nginx's log nor to PHP-FPM's. PHP keeps the request, removes the file and marks it with error code 1 (UPLOAD_ERR_INI_SIZE in the PHP manual), and then WordPress or the plugin has to show a message. This is what probe.php saw with the 4.7 MB photo:

$ curl -s -F "[email protected]" -F "note=hello" http://example.com/probe.php
upload_max_filesize=2M post_max_size=8M memory_limit=128M
CONTENT_LENGTH=4740614
_POST fields: 1, _FILES entries: 1
_FILES[file]: name=photo-4.7mb.jpg size=0 error=1

The form field arrived, and the file arrived with 0 bytes and an error code. When a screen says that your file is empty and you know it isn't, this is usually the reason.

post_max_size empties the whole form, and WordPress says the link has expired

post_max_size is the limit for the whole request, 8 MB on Ubuntu. When a request is bigger, PHP deletes everything in it, and the PHP manual says $_POST and $_FILES are then empty. The largest photo shows this:

$ curl -s -F "[email protected]" -F "note=hello" http://example.com/probe.php
upload_max_filesize=2M post_max_size=8M memory_limit=128M
CONTENT_LENGTH=9478822
_POST fields: 0, _FILES entries: 0

Unlike upload_max_filesize, this limit writes a line to a log. PHP-FPM sends a warning to nginx, and nginx writes it into its own error log, so on Ubuntu you'll find nginx's refusals and this warning in the same file:

root@server:~# tail -n 1 /var/log/nginx/error.log
2026/10/27 23:53:12 [error] 211#211: *289 FastCGI sent in stderr: "PHP message: PHP Warning:  POST Content-Length of 9478822 bytes exceeds the limit of 8388608 bytes in Unknown on line 0" while reading response header from upstream, client: 172.23.0.1, server: example.com, request: "POST /probe.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.3-fpm.sock:", host: "example.com"

WordPress loses more than the file. Every admin form contains a security token (a nonce), the empty request arrives without it, and WordPress treats the form as expired. I uploaded the WooCommerce plugin zip through Upload Plugin and got "The link you followed has expired." with HTTP 403. The link hadn't expired. The zip was 18.1 MB, more than twice the limit.

A plain WordPress error box on a phone: The link you followed has expired. Please try again.
Upload Plugin with the 18.1 MB WooCommerce zip and PHP's default post_max_size of 8M

WooCommerce's import showed no message at all. With the bigger CSV, Continue showed the same empty upload form again, because the importer only works when the request contains its form fields. If you press Continue and see the same screen again, compare the file size with post_max_size before you try again.

Seven messages for one problem, and only one of them says 413

Here's every failed upload from the test: where I uploaded, what stopped it and what the screen said. If you remember only one row, remember the first one: a progress bar at 100% and a box that says the file may have been uploaded.

Where I uploadedFileStopped byWhat the screen saidHTTP status
Media > Add NewPhoto, 1.5 MBnginx, 1mUnexpected response from the server. The file may have been uploaded successfully. (bar at 100%)413
Block editor, Image blockPhoto, 1.5 MBnginx, 1mThe response is not a valid JSON response.413
Upload Theme, Upload PluginStorefront zip, 2.9 MB; WooCommerce zip, 18.1 MBnginx, 1mnginx's own page: 413 Request Entity Too Large413
Products > ImportCSV, 2.8 MBnginx, 1mnginx's own page: 413 Request Entity Too Large413
Upload ThemeStorefront zip, 2.9 MBupload_max_filesize, 2MThe uploaded file exceeds the upload_max_filesize directive in php.ini.500
Products > ImportCSV, 2.8 MBupload_max_filesize, 2MFile is empty. Please upload something more substantial.200
Media > Add New, block editorPhoto, 4.7 MBWordPress, in the browserphoto-4.7mb.jpg exceeds the maximum upload size for this site.nothing sent
Upload PluginWooCommerce zip, 18.1 MBpost_max_size, 8MThe link you followed has expired.403
Products > ImportCSV, 9.1 MBpost_max_size, 8MNo message: the same empty form appeared again200

I ran all of them on the same test server and took every screenshot in Chrome at phone width.

Compare the "Stopped by" column with the message on your screen. The uploads in the nginx rows and in the post_max_size rows wrote a line to nginx's error log, but the uploads in the upload_max_filesize rows wrote nothing to any log. So when a screen says that your file is empty or that a link has expired, don't trust it until you've checked the file size.

Step 2: Set upload_max_filesize and post_max_size for PHP-FPM

First, find the right folder. On Ubuntu, PHP has a separate settings folder for each way it runs: PHP-FPM, which runs your store, reads /etc/php/8.3/fpm/, and the php command you type in a terminal reads /etc/php/8.3/cli/. That's why php -i shows the limits of the PHP that the terminal uses, and a change in the cli folder doesn't change anything for your store:

root@server:~# php --ini | head -2
Configuration File (php.ini) Path: /etc/php/8.3/cli
Loaded Configuration File:         /etc/php/8.3/cli/php.ini

I put the two settings in a small file of their own in PHP-FPM's conf.d folder, which PHP reads after php.ini. Changing the same two lines in /etc/php/8.3/fpm/php.ini works too; a separate file is easier to find a year later.

root@server:~# printf 'upload_max_filesize = 64M\npost_max_size = 72M\n' > /etc/php/8.3/fpm/conf.d/99-uploads.ini
root@server:~# php-fpm8.3 -t
[28-Oct-2026 00:17:44] NOTICE: configuration file /etc/php/8.3/fpm/php-fpm.conf test is successful
root@server:~# systemctl reload php8.3-fpm

# what probe.php reported before the reload, then after it
upload_max_filesize=2M post_max_size=8M memory_limit=128M
upload_max_filesize=64M post_max_size=72M memory_limit=128M

post_max_size is a little higher than upload_max_filesize for the same reason that nginx counted 1,537,623 bytes for a smaller photo: the request contains the file and the form fields with it, and the PHP manual says post_max_size must be larger. The rule I use is client_max_body_size is at least post_max_size, and post_max_size is above upload_max_filesize. WordPress then shows the smaller of the two PHP numbers, 64 MB.

Don't skip the reload. Until I reloaded PHP-FPM, the site still used 2M and 8M, although the new file was already there. And php-fpm8.3 -i won't tell you whether you've reloaded, because it reads the files on disk: when I changed the file to 65M, it printed 65M while the running PHP workers still used 64M. Only a page that PHP-FPM serves shows the limits your store really uses: the line under the uploader in Media > Add New, or Tools > Site Health > Info > Media Handling, where Max effective file size said 64 MB.

If each site on the server has its own PHP-FPM pool, you can put the two limits at the end of that pool's file in /etc/php/8.3/fpm/pool.d/ instead, and then they apply to one site only:

php_admin_value[upload_max_filesize] = 64M
php_admin_value[post_max_size] = 72M

That worked after the reload too, but php-fpm8.3 -i still printed the old defaults, because it doesn't read pool files at all. The WordPress screens showed the new limit.

memory_limit and max_execution_time often appear next to these two in upload guides, so I checked both. With memory_limit at the default 128M, WordPress added the 9.5 MB photo to the Media Library and made its scaled copy. The time limits never stopped an upload either.

I sent the same photo at 150 KB a second. The upload took 61.7 seconds and worked, with max_execution_time at 30 seconds and max_input_time at 60. nginx receives the whole request before it passes anything to PHP-FPM, so the slow upload happens in nginx, before PHP starts to count its time.

Is a PHP-FPM reload safe in the middle of the day? In my test, it caused one slow page. The first shop page after the reload took about a second in both runs, compared with 0.09 seconds with a warm cache, because the new workers start with an empty OPcache and compile WooCommerce's files again (I measured what OPcache saves a WooCommerce store in the 600-product speed test). The next page was almost as fast as before the reload.

Core Web Vitals Monitoring

Watch your server response from real visits after every change

One snippet, or our WordPress plugin, shows the speed and traffic of every real visit, with an email the week your store gets slower. Free accounts keep 7 days of history, Premium keeps 180.

Start free monitoring

Why .htaccess, ini_set and wp-config.php don't change these limits

Many of the fixes you'll find in a search were written for Apache, and they do nothing on nginx with PHP-FPM. I tested the three you'll see most often.

nginx never reads .htaccess, Apache's settings file for one folder. The php_value lines that guides put there work only when PHP runs inside Apache as a module, as the PHP manual says. With both lines in the web root of my test store, the site stayed at 2M.

The ini_set() line that many guides tell you to add to wp-config.php can't change these two settings. They're PHP_INI_PERDIR settings: the PHP manual lists php.ini, .htaccess, httpd.conf and .user.ini as the places to set them, PHP-FPM adds its pool files, and code can't change them. When wp-config.php runs, PHP has already read the upload. probe.php tried anyway:

ini_set('upload_max_filesize', '64M') returned false; now 2M

ini_set() gave no warning and no error, only false. So with or without an @ before ini_set() (many copied lines have one), nothing tells you that the line failed.

A .user.ini file does work with PHP-FPM (the manual says PHP reads these files when it runs as CGI or FastCGI, and PHP-FPM is FastCGI), but with a delay. Each PHP worker keeps the values it read for 300 seconds (the user_ini.cache_ttl setting). I saved a .user.ini with 64M in the web root and checked the site every 30 seconds, starting seven seconds later:

23:56:30 upload_max_filesize=2M post_max_size=8M memory_limit=128M
23:57:01 upload_max_filesize=64M post_max_size=64M memory_limit=128M
23:57:31 upload_max_filesize=2M post_max_size=8M memory_limit=128M
...
00:00:32 upload_max_filesize=2M post_max_size=8M memory_limit=128M
00:01:02 upload_max_filesize=64M post_max_size=64M memory_limit=128M

For about four minutes, the answer depended on which PHP worker handled the request, and the last old value appeared a little more than four minutes after I saved the file (I removed five lines from the middle of that output). A .user.ini also can't change a php_admin_value from the pool: a file with 16M didn't change the pool's 64M. On a server that you manage yourself, the PHP-FPM file from Step 2 is faster and more reliable.

Cloudflare's 100 MB upload limit comes before your server

If your domain uses Cloudflare's proxy, there's one more limit before nginx. Cloudflare's Error 413 page (last updated September 28, 2026) lists the maximum upload size for each plan:

Cloudflare planMaximum upload size
Free100 MB
Pro100 MB
Business200 MB
EnterpriseUp to 5 GB

Cloudflare answers a bigger request with its own 413 and doesn't send the request to your server. Your server never sees it, so your nginx log stays empty and none of the PHP messages appear. I didn't test this limit (my test server doesn't use Cloudflare), so these numbers are Cloudflare's.

Cloudflare's page gives three solutions: split the upload into smaller parts, set the DNS record to DNS-only so that traffic doesn't go through the proxy, or move to a bigger plan. It also says that the Maximum Upload Size can be changed on the zone's Network page, so if someone lowered it there, that's the limit you'll reach. For a store, 100 MB is far above a product photo or the theme and plugin zips I tested. A backup restore, a product video or a very large catalog CSV can reach that size.

The same files after both fixes

After both fixes, I ran every failed upload again. Media > Add New said Maximum upload file size: 64 MB, and the largest photo uploaded on the first try. The WooCommerce zip that had "expired" reached the screen that compares the installed plugin with the uploaded one, the Storefront zip did the same, and the bigger CSV reached Column mapping with the new maximum under the file field.

WordPress Media, Upload Media on a phone after the fix: the line Maximum upload file size: 64 MB, and photo-9.5mb uploaded as photo-9.5mb-scaled.jpg with Edit and Copy URL to clipboard
After the nginx and PHP-FPM reloads: 64 MB, and the 9.5 MB photo in the Media Library

Remember one more thing for later. These limits are in a folder named after the PHP version, so when you upgrade the server to the next PHP version, PHP-FPM reads a new folder (/etc/php/8.4/fpm/ for PHP 8.4), and both limits are back at the defaults until you copy your 99-uploads.ini there. After that upgrade, the line under the uploader is the first thing I'd check.

Sources

nginx, PHP manual, HTTP and 3 more
6
Learning CenterModule 14/17Hosting & Security

This guide is part of the free CartHacker Learning Center. Create an account to track your reading progress and keep your place across devices, with playbooks matched to your store's platform.

Progress tracking is paused0%
Mark as Read
15Up NextRedis Object Cache Made My Test Store No Faster, Until the Database Was 2 ms Away 20 min readHosting & Security

Discussion 0

Ask a question or share what you saw on your own store.

No comments yet.