OneLinersCommand workbench
Guides
Services & Applications / Security

Configure PHP upload and request limits end to end

Align PHP, FPM, nginx or Apache, reverse-proxy, temporary storage, and application limits; then test accepted and rejected uploads.

35 min8 stepsChanges system stateRevision 2
Save or explore
Save to collectionCreate a collection in the sidebar first.
0 of 8 steps completed
Goal

Accept intended upload sizes while rejecting oversized or slow requests predictably at the correct layer.

Supported environments
  • PHP 8.2, 8.3, 8.4, 8.5
Prerequisites
  • Supported host and recovery access Use a supported distribution, keep an independent root-capable session, and record the current package repositories before changing the PHP runtime.cat /etc/os-release && uname -m
  • Application compatibility Confirm the application, framework, extensions, and deployment tooling explicitly support the target PHP branch.
  • Configuration backup Archive the active PHP, web-server, and application configuration before packages, pools, or handlers change.
Operating boundary

OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.

Full guide

What you will build

System
  • One documented upload budget spanning edge/CDN, load balancer, nginx or Apache, PHP-FPM, multipart parsing, private temporary storage and application validation.
  • A root-owned FPM INI override with explicit file/post sizes, file count, time, memory, input-variable and private temp-path limits, plus a route-scoped web-server body limit.
  • Concrete below/near/above-boundary tests that verify accepted and rejected status, application semantics, log reason, cleanup and rollback without using sensitive files.
Observable outcome
  • Intended uploads succeed; oversized requests fail predictably at the chosen outer layer before expensive PHP processing; PHP and application limits remain consistent.
  • Temporary files stay outside the document root under `{{appUser}}:{{appGroup}}` mode 0700, with sufficient bytes/inodes and cleanup for failures.
  • The selected nginx or Apache parser cannot be masked by another server's success, FPM reloads only after all checks, and tests prove response codes plus no orphan artifacts.

Architecture

How the parts fit together

A request crosses several independent ceilings. The earliest edge may reject before the host; nginx `client_max_body_size` or Apache `LimitRequestBody` can reject before FPM; PHP checks POST and per-file limits while streaming multipart data into `upload_tmp_dir`; the application validates authentication, count, content and business size before moving data to durable storage. Effective capacity must reserve disk, inodes, memory, execution time and concurrency. The outer web limit is slightly above PHP's POST budget to allow multipart overhead unless the deliberate policy is to reject at the edge. Every layer returns a known status and logs a bounded, non-secret reason.

Edge and web-server limitRejects clearly oversized bodies before PHP work.
PHP multipart limitsBounds total POST, individual file, count, parser, time and memory.
Private temporary directoryStores in-flight files outside the public tree under the pool identity.
Application policyAuthenticates and validates content/type/count before durable move.
Boundary testsProve accepted and rejected behavior at concrete byte sizes.
  1. The request passes the edge and route-scoped web body ceiling.
  2. PHP streams multipart parts into the private temp directory while enforcing its configured budgets.
  3. The application validates the uploaded file and atomically moves accepted content to non-executable storage.
  4. Rejected/completed requests clean temporary files and emit a documented status/log signal.

Assumptions

  • The upload endpoint, business maximum, concurrency and expected duration are known; tests use generated non-sensitive data and a non-production object name.
  • The exact PHP-FPM service/socket/INI scan path and active `{{webService}}` (`nginx` or `apache2`) are inventoried.
  • `{{postMaxMiB}}` is above `{{uploadMaxMiB}}` with measured multipart overhead, and edge/web limits are deliberately aligned.
  • Private temp storage has monitored bytes/inodes, cleanup, backup exclusion and MAC policy; durable object storage is outside the executable public tree.

Key concepts

upload_max_filesize
Maximum size of one uploaded file parsed by PHP.
post_max_size
Maximum total POST body PHP accepts, including multipart framing and all fields/files.
max_file_uploads
Maximum number of files accepted in one request.
multipart overhead
Boundaries, headers and form fields that make the POST body larger than file bytes.
outer limit
An edge/web-server ceiling that can reject before PHP allocates parser, worker and temporary-storage resources.
boundary test
Controlled requests below, near and above exact limits that prove expected status and cleanup.

Before you copy

Values used in this guide

{{phpVersion}}

Installed PHP-FPM branch used in configuration paths.

Example: 8.3
{{fpmService}}

Exact active systemd FPM service.

Example: php8.3-fpm
{{fpmSocket}}

Exact Unix socket for effective-value checks.

Example: /run/php/php8.3-billing.sock
{{app}}

Application identifier used in config and runtime paths.

Example: billing
{{appUser}}

Dedicated FPM worker identity.

Example: billing
{{appGroup}}

Private application group.

Example: billing
{{webService}}

Explicit active web server: `nginx` or `apache2`.

Example: nginx
{{uploadMaxMiB}}

Maximum bytes per file expressed in MiB for PHP.

Example: 50
{{postMaxMiB}}

Total POST MiB including multipart overhead.

Example: 55
{{maxFileUploads}}

Maximum file parts per request.

Example: 5
{{maxInputSeconds}}

Maximum request-input parsing seconds.

Example: 60
{{maxExecutionSeconds}}

Application execution seconds after parsing.

Example: 60
{{maxInputVars}}

Maximum parsed input variables.

Example: 2000
{{memoryLimitMiB}}

Per-request PHP memory ceiling.

Example: 256
{{acceptedBytes}}

Generated test file below the effective limit.

Example: 52428800
{{rejectedBytes}}

Generated test file above the outer limit.

Example: 62914560
{{domain}}

Canary hostname for upload tests.

Example: billing.example.com
{{uploadPath}}

Authenticated test endpoint path without leading slash.

Example: internal/upload-test
{{docroot}}

Public root used only for a temporary protected effective-value fixture.

Example: /srv/billing/current/public

Security and production boundaries

  • Size limits do not validate file type or safety. Authenticate, authorize, randomize names, scan where required and store uploads outside executable paths.
  • Concurrency multiplies disk, memory, worker and network cost. Rate limits and capacity monitoring must accompany per-request ceilings.
  • Never test with secrets/customer files, never publish diagnostic PHP, and do not make temp directories world-writable or web-readable.

Stop before continuing if

  • Stop if the business maximum, layer map, concurrency/storage budget, exact FPM/web services or rollback files are unknown.
  • Do not reload when either selected parser fails, INI contains duplicates/placeholders, private temp path is unsafe, or an outer limit is smaller unintentionally.
  • Do not accept until below-limit succeeds, above-limit rejects with documented status, logs explain the layer, and no orphan temp file remains.
01

instruction

Map every request-size enforcement layer

read-only

Record CDN, load balancer, reverse proxy, web server, PHP SAPI, and application limits before changing one layer in isolation.

Why this step matters

Changing one limit without mapping the full request path produces contradictory failures and misleading status codes.

What to understand

Record CDN/load balancer, selected virtual host/location, FPM socket/INI and application limits in request order.

Use expanded web configuration and FPM-effective values; CLI INI is only a path hint.

System changes

  • No persistent change; inventories current PHP and web-server request-size policy.

Syntax explained

php --ini
Shows CLI configuration roots to compare with FPM.
nginx -T
Prints effective included nginx configuration.
apachectl ... DUMP_RUN_CFG
Prints Apache runtime configuration context.
Command
php --ini; sudo nginx -T 2>/dev/null | grep client_max_body_size || true; sudo apachectl -t -D DUMP_RUN_CFG 2>/dev/null | grep -i limit || true
Example output / evidence
Current PHP and web-server policies are recorded.

Checkpoint: Checkpoint: map path

Continue whenEvery enforcement layer, selected service/socket/config file, existing byte limit and response code is documented.

Stop whenTraffic path or exact active web/FPM service is unknown, or an upstream provider limit cannot meet the business requirement.

If this step fails

No local directive is found but uploads are rejected.

Likely causeA default/inherited application or external edge limit applies.

Safe checks
  • curl -v -F file=@/tmp/upload-ok.bin https://billing.example.com/internal/upload-test

ResolutionTrace response headers/logs through each edge and record the actual rejecting layer before edits.

Security notes

  • Do not paste tokens, cookies or customer filenames from configuration/log inventory.

Alternatives

  • Use provider and configuration-management APIs to build the same ordered limit map.

Stop conditions

  • Traffic path or exact active web/FPM service is unknown, or an upstream provider limit cannot meet the business requirement.
02

instruction

Define the business limit and disk budget

read-only

Choose a maximum upload, concurrent upload count, request timeout, and temporary-storage headroom; do not set unlimited values.

Why this step matters

A per-file limit becomes a host outage when concurrent uploads exhaust bytes, inodes, workers, memory or execution time.

What to understand

Model `post budget × concurrent requests` plus existing temp files and headroom; also inspect inode availability.

Define timeouts and application behavior for client disconnect, partial upload, scan and durable move.

System changes

  • No persistent change; reads storage/inode and service task/memory ceilings.

Syntax explained

df -h
Shows capacity of temp filesystems.
df -i
Shows inode capacity for many multipart files.
TasksMax/MemoryMax
Shows systemd cgroup constraints affecting concurrency.
Command
Fill variables0/1 ready

Values stay on this page and are never sent or saved.

df -h /tmp /var/tmp; df -i /tmp /var/tmp; systemctl show php{{phpVersion}}-fpm -p TasksMax -p MemoryMax
Example output / evidence
Byte, concurrency, time, disk, and inode budgets are documented.

Checkpoint: Checkpoint: capacity

Continue whenApproved per-file/POST/count/time/concurrency budgets fit storage, inode, FPM and application capacity with alerts.

Stop whenWorst-case concurrent demand exceeds capacity, cleanup is absent, or limits are proposed as unlimited.

If this step fails

Disk has free GiB but uploads fail with no space.

Likely causeInodes, quota, cgroup, tmpfs or reserved space is exhausted.

Safe checks
  • df -h /srv/billing/tmp
  • df -i /srv/billing/tmp
  • quota -s billing 2>/dev/null || true

ResolutionFix the specific capacity/quota constraint and lower concurrency/limits until monitored headroom exists.

Security notes

  • Bounded capacity is defense against authenticated and unauthenticated resource exhaustion.

Alternatives

  • Offload large objects to scoped object-storage uploads when host capacity cannot safely absorb concurrency.

Stop conditions

  • Worst-case concurrent demand exceeds capacity, cleanup is absent, or limits are proposed as unlimited.
03

config

Align upload_max_filesize and post_max_size

caution

Set post_max_size above upload_max_filesize to account for form overhead and multiple files, with bounded parser, memory, execution, and file-count limits.

Why this step matters

One complete root-owned override keeps total/per-file/count/time/memory/temp policy reviewable and avoids scattered duplicate directives.

What to understand

`post_max_size` must exceed file bytes plus multipart overhead; choose exact values from boundary tests, not intuition.

The config includes no `max_multipart_body_parts` because PHP 8.2 lacks it; PHP 8.3+ can add a reviewed value separately.

System changes

  • Creates `/etc/php/{{phpVersion}}/fpm/conf.d/90-{{app}}-uploads.ini` with the displayed bounded policy.

Syntax explained

upload_max_filesize
Maximum bytes for one uploaded file.
post_max_size
Maximum entire POST body.
max_file_uploads
Maximum file parts in one request.
upload_tmp_dir
Private staging path outside document root.
File /etc/php/{{phpVersion}}/fpm/conf.d/90-{{app}}-uploads.ini
Configuration
Fill variables0/8 ready

Values stay on this page and are never sent or saved.

file_uploads=On
upload_max_filesize={{uploadMaxMiB}}M
post_max_size={{postMaxMiB}}M
max_file_uploads={{maxFileUploads}}
max_input_time={{maxInputSeconds}}
max_execution_time={{maxExecutionSeconds}}
max_input_vars={{maxInputVars}}
memory_limit={{memoryLimitMiB}}M
upload_tmp_dir=/srv/{{app}}/tmp/uploads
Command
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

sudoedit /etc/php/{{phpVersion}}/fpm/conf.d/90-{{app}}-uploads.ini
Example output / evidence
file_uploads, upload/post sizes, file count, time, memory, input variables, and temp path are explicit.

Checkpoint: Checkpoint: php size

Continue whenAll variables are replaced, total exceeds file budget, limits are finite, one authoritative file exists and FPM scans it.

Stop whenValues exceed capacity, total is not above file/overhead, duplicates exist, or the file is writable by application/deployer.

If this step fails

FPM effective values remain defaults.

Likely causeWrong branch/SAPI scan directory, filename not scanned, or later override wins.

Safe checks
  • sudo php-fpm8.3 -tt
  • grep -R -n 'upload_max_filesize' /etc/php/8.3/fpm

ResolutionPlace one root-owned override in the exact FPM scan directory and remove only unintended later duplicates.

Security notes

  • Application workers must not modify resource ceilings.

Alternatives

  • Set the same values as pool-level `php_admin_value` when only one pool should receive them.

Stop conditions

  • Values exceed capacity, total is not above file/overhead, duplicates exist, or the file is writable by application/deployer.
04

config

Bound time, input, and memory

caution

Verify max_input_time, max_execution_time, max_input_vars, and memory_limit; on PHP 8.3+ separately review max_multipart_body_parts before adding it.

Why this step matters

Multipart parsing and post-processing need bounded time, variables and memory independent of raw file bytes.

What to understand

Input time covers parsing; execution time behavior can differ by platform/SAPI and application work must be measured.

On PHP 8.3+, `max_multipart_body_parts` can further bound total parts; omit it on 8.2 instead of deploying an unknown directive.

System changes

  • No separate mutation; verifies time, memory and parser values in the authoritative INI.

Syntax explained

max_input_time
Bounds request-data parsing time.
max_execution_time
Bounds PHP script execution according to SAPI/platform semantics.
max_input_vars
Bounds parsed input-variable count.
memory_limit
Bounds PHP memory per request.
max_multipart_body_parts
PHP 8.3+ optional total multipart part ceiling.
Configuration
Fill variables0/2 ready

Values stay on this page and are never sent or saved.

sudo grep -E '^(max_input_time|max_execution_time|max_input_vars|memory_limit|max_multipart_body_parts)' /etc/php/{{phpVersion}}/fpm/conf.d/90-{{app}}-uploads.ini
Example output / evidence
Runtime and parser limits are explicit, and version-specific multipart policy is documented.

Checkpoint: Checkpoint: php time memory

Continue whenFinite values match workload and version; slow valid uploads succeed while abusive/oversized work fails predictably.

Stop whenValues are unlimited, PHP version does not support a directive, or legitimate concurrency exceeds host budget.

If this step fails

Large valid upload times out despite sufficient size limits.

Likely causeEdge/body/read/input/execution timeout is shorter than measured transfer/processing.

Safe checks
  • curl -v --limit-rate 1m -F file=@/tmp/upload-ok.bin https://billing.example.com/internal/upload-test
  • journalctl -u php8.3-fpm -n 100

ResolutionIdentify the exact timeout and raise only the route-specific layer within capacity/security policy.

Security notes

  • Long timeouts and high memory multiply denial-of-service cost; combine with authentication and concurrency/rate controls.

Alternatives

  • Use asynchronous direct-to-object-storage upload/finalization for long transfers.

Stop conditions

  • Values are unlimited, PHP version does not support a directive, or legitimate concurrency exceeds host budget.
05

config

Use a private temporary upload directory

caution

Create a directory owned by the pool identity with no web access and verify the already declared upload_tmp_dir without appending duplicate directives on reruns.

Why this step matters

PHP writes multipart files before application validation; a private non-executable path prevents exposure/execution and contains cleanup.

What to understand

Mode 0700 limits access to the FPM identity; parent paths, quota, MAC policy and filesystem mount options still matter.

The command verifies the existing INI line instead of appending duplicates on every run.

System changes

  • Creates `/srv/{{app}}/tmp/uploads` with application ownership/mode 0700 and verifies its configured path.

Syntax explained

install -d
Creates or converges the directory with explicit metadata.
grep -Fx
Requires one exact configured path text.
namei -l
Shows permissions for every parent component.
Configuration
Fill variables0/4 ready

Values stay on this page and are never sent or saved.

sudo install -d -o {{appUser}} -g {{appGroup}} -m 0700 /srv/{{app}}/tmp/uploads; grep -Fx 'upload_tmp_dir=/srv/{{app}}/tmp/uploads' /etc/php/{{phpVersion}}/fpm/conf.d/90-{{app}}-uploads.ini; sudo namei -l /srv/{{app}}/tmp/uploads
Example output / evidence
The exact upload temp directive exists once and the private path is outside the document root.

Checkpoint: Checkpoint: temp

Continue whenThe exact directory is private, outside document root, writable by FPM, monitored for bytes/inodes and not executable/served.

Stop whenPath is within public root, symlinked unexpectedly, shared across tenants, lacks capacity/cleanup, or worker cannot write.

If this step fails

PHP reports missing temporary folder.

Likely causeFPM sees another path, identity/MAC blocks it, or directory disappeared after reboot/deploy.

Safe checks
  • namei -l /srv/billing/tmp/uploads
  • sudo -u billing test -w /srv/billing/tmp/uploads
  • sudo ausearch -m AVC -ts recent

ResolutionRestore the exact persistent private path and narrow ownership/policy; never fall back to a public directory.

Security notes

  • Treat temp files as untrusted sensitive content; do not execute, index or back them up by default.

Alternatives

  • Use a dedicated quota-controlled encrypted filesystem mounted outside the public tree.

Stop conditions

  • Path is within public root, symlinked unexpectedly, shared across tenants, lacks capacity/cleanup, or worker cannot write.
06

config

Set the matching web-server limit

caution

Set nginx client_max_body_size or Apache LimitRequestBody slightly above the PHP post limit, scoped to the upload endpoint where possible.

Why this step matters

A route-scoped outer limit rejects obviously oversized bodies before consuming an FPM worker/temp space while leaving unrelated endpoints unchanged.

What to understand

For nginx set `client_max_body_size` in the exact upload location/server; for Apache set `LimitRequestBody` in the exact directory/location.

Choose whether the outer limit is slightly above PHP POST (PHP owns semantics) or deliberately lower (edge owns 413), then document response/log behavior.

System changes

  • Requires an explicit edit to the selected web virtual-host route; the command only audits current directives before that reviewed edit.

Syntax explained

client_max_body_size
nginx maximum request body for the selected context.
LimitRequestBody
Apache request-body ceiling in bytes for the selected context.
grep -R --line-number
Audits inherited/duplicate directives before editing.
Configuration
sudo grep -R --line-number -E 'client_max_body_size|LimitRequestBody' /etc/nginx /etc/apache2 2>/dev/null
Example output / evidence
The upload route has an explicit request-body limit.

Checkpoint: Checkpoint: web limit

Continue whenOnly the intended upload route has the documented outer ceiling and other routes retain their prior policy.

Stop whenSelected virtual host/location is unclear, a global edit would affect other tenants, or edge maximum is smaller and immutable.

If this step fails

All site requests, not only uploads, inherit the larger limit.

Likely causeDirective was placed globally/server-wide instead of exact upload location.

Safe checks
  • sudo nginx -T 2>&1 | grep -n -A8 -B8 client_max_body_size

ResolutionMove the directive to the exact authenticated upload context and retest unrelated paths.

Security notes

  • Prefer the smallest route scope and never raise a global ceiling for one endpoint.

Alternatives

  • Keep the smaller outer limit and use direct scoped object-storage uploads for larger objects.

Stop conditions

  • Selected virtual host/location is unclear, a global edit would affect other tenants, or edge maximum is smaller and immutable.
07

verification

Validate and reload affected services

caution

Test PHP-FPM and the explicitly selected active web server before reload; never let an Apache success hide a failed nginx parser or vice versa.

Why this step matters

Both FPM and the one explicitly selected web server must validate; shell fallback after a parser failure can hide the service that actually handles traffic.

What to understand

Resolve the FPM binary, require its test, then branch on literal `nginx` or `apache2`; any other value fails closed.

Reload web and `{{fpmService}}` only after all parsers succeed and keep previous config for immediate recovery.

System changes

  • Reloads the selected web service and exact FPM service after successful configuration tests.

Syntax explained

&& case
Prevents reload when FPM validation fails and selects one explicit web server.
nginx -t/apachectl configtest
Runs only the parser belonging to active traffic.
systemctl reload {{fpmService}}
Applies INI to the inventoried FPM workers.
Command
Fill variables0/3 ready

Values stay on this page and are never sent or saved.

FPM_BIN="$(command -v php-fpm{{phpVersion}} || command -v php-fpm)"; sudo "$FPM_BIN" -tt && case '{{webService}}' in nginx) sudo nginx -t && sudo systemctl reload nginx;; apache2) sudo apachectl configtest && sudo systemctl reload apache2;; *) printf 'Choose nginx or apache2\n' >&2; exit 1;; esac && sudo systemctl reload {{fpmService}}
Example output / evidence
The exact FPM and selected web-server parsers succeed, reloads complete, and both services remain active.

Checkpoint: Checkpoint: validate reload

Continue whenExact parsers succeed, both selected services stay active, effective FPM values match and health remains green.

Stop whenAny parser/reload/health check fails, selected service is wrong, or rollback copy/session is unavailable.

If this step fails

Command prints `Choose nginx or apache2`.

Likely causePlaceholder was not replaced with the explicit supported service.

Safe checks
  • systemctl is-active nginx apache2
  • sudo ss -lntp 'sport = :80 or sport = :443'

ResolutionIdentify the actual serving process and rerun with exactly `nginx` or `apache2`; create a separate procedure for another server.

Security notes

  • Fail closed rather than allowing another installed server's parser to mask the active server's error.

Alternatives

  • Deploy and validate the exact configuration through service-specific configuration management.

Stop conditions

  • Any parser/reload/health check fails, selected service is wrong, or rollback copy/session is unavailable.
08

verification

Test below, at, and above the limit

read-only

Generate non-sensitive files around the boundary and verify the accepted response, rejection status, cleanup, and logs.

Why this step matters

Concrete requests are the only proof that every layer agrees; configuration output cannot show CDN routing, multipart overhead, application behavior or cleanup.

What to understand

Use generated non-sensitive files below and above exact byte boundaries, authenticated test identity and unique object names.

Record status/body marker, rejecting layer/log, durable result and temp cleanup; delete generated `/tmp` files afterward.

System changes

  • Creates two sparse local test files and sends controlled multipart requests; the application may create one disposable test object.

Syntax explained

truncate -s
Creates exact-size sparse test files without customer data.
curl -F file=@
Builds a real multipart upload.
-w '%{http_code}'
Records concrete HTTP status.
Command
Fill variables0/4 ready

Values stay on this page and are never sent or saved.

truncate -s {{acceptedBytes}} /tmp/upload-ok.bin; truncate -s {{rejectedBytes}} /tmp/upload-too-large.bin; curl -sS -o /dev/null -w 'ok=%{http_code}\n' -F file=@/tmp/upload-ok.bin https://{{domain}}/{{uploadPath}}; curl -sS -o /dev/null -w 'large=%{http_code}\n' -F file=@/tmp/upload-too-large.bin https://{{domain}}/{{uploadPath}}
Example output / evidence
The intended file succeeds and the oversized file is rejected with the documented status.

Checkpoint: Checkpoint: boundary tests

Continue whenBelow-limit request succeeds with expected app marker, above-limit request gets documented rejection, logs identify layer and no orphan temp/test object remains.

Stop whenEndpoint is production-mutating without cleanup, authentication is absent, test size could exhaust capacity, or statuses/logs are ambiguous.

If this step fails

Both files succeed or both fail.

Likely causeTest sizes do not straddle effective limits, sparse transfer behaves differently, another layer/endpoint is selected, or app ignores policy.

Safe checks
  • stat -c '%s %n' /tmp/upload-*.bin
  • curl -v -F file=@/tmp/upload-ok.bin https://billing.example.com/internal/upload-test

ResolutionConfirm exact bytes, selected route/effective limits and application marker; choose sizes one MiB inside/outside and repeat.

Security notes

  • Use test-only authentication/data and remove artifacts; never test with credentials in URL or real user files.

Alternatives

  • Run the same fixtures in a staging environment mirroring every edge, then a smaller canary in production.

Stop conditions

  • Endpoint is production-mutating without cleanup, authentication is absent, test size could exhaust capacity, or statuses/logs are ambiguous.

Finish line

Verification checklist

Effective FPM valuesSCRIPT_NAME=/limits.php SCRIPT_FILENAME={{docroot}}/limits.php REQUEST_METHOD=GET cgi-fcgi -bind -connect {{fpmSocket}}The FPM SAPI reports the intended size, time, memory, and temp-directory values.
No orphan temp filessudo find /srv/{{app}}/tmp/uploads -type f -mmin +10 -lsNo abandoned files remain after completed and rejected tests.

Recovery guidance

Common problems and safe checks

The client receives 413 before the PHP application logs anything.

Likely causeAn edge/nginx/Apache body limit is lower than PHP or the request reaches another route/server block.

Safe checks
  • sudo nginx -T 2>&1 | grep -n client_max_body_size
  • sudo apachectl -t -D DUMP_RUN_CFG
  • curl -v -F file=@/tmp/upload-ok.bin https://billing.example.com/internal/upload-test

ResolutionIdentify the rejecting layer and set its route-scoped ceiling deliberately relative to PHP; retest exact byte boundaries.

PHP receives an empty `$_POST`/`$_FILES` without a useful application error.

Likely causeThe total body exceeded `post_max_size`, input parsing timed out or temporary storage failed.

Safe checks
  • journalctl -u php8.3-fpm --since '-10 min' -n 100
  • df -h /srv/billing/tmp/uploads
  • df -i /srv/billing/tmp/uploads

ResolutionCorrect total POST/overhead or storage/time budget and make the application detect/report this server-side condition safely.

Accepted/rejected tests leave files in the temp directory.

Likely causeWorker termination, application move/cleanup failure, long requests or filesystem permissions prevented cleanup.

Safe checks
  • sudo find /srv/billing/tmp/uploads -type f -printf '%TY-%Tm-%Td %TT %s %p\n' | tail
  • journalctl -u php8.3-fpm -n 100

ResolutionFix lifecycle/permissions, add a conservative age-based cleanup with open-file awareness, and monitor bytes/inodes.

After the procedure

Alternatives and next steps

Consider these alternatives

  • Upload directly to object storage with short-lived scoped URLs when large bodies should bypass web/PHP workers, while preserving application authorization and finalization.
  • Use route-specific stricter limits rather than raising a global web/PHP default for every application.
  • Use resumable/chunked application protocols for large unreliable-network transfers, with per-chunk and total-object validation.

Operate it safely

  • Add rate/concurrency limits, storage headroom alerts, upload latency/error metrics and safe stale-temp cleanup.
  • Validate MIME/content, filename normalization, archive expansion, antivirus policy and durable non-executable storage in the application layer.
  • Repeat boundaries through the real CDN/load balancer and after any web/PHP/application upgrade.

Reference

Frequently asked questions

Why is `post_max_size` larger than `upload_max_filesize`?

POST includes multipart framing, form fields and possibly several files; equal values can reject a nominally allowed file.

Which layer should reject oversized uploads?

Usually the earliest trusted route-specific layer, with PHP/application limits as defense in depth and clear status semantics.

Does `memory_limit` need to exceed file size?

Not necessarily when streaming, but the framework may buffer/transform data. Measure the actual request path and retain a bounded per-request budget.

Recovery

Rollback

Restore the previous PHP and web-server limits, reload services, and remove only test artifacts.

  1. Restore archived INI and virtual-host configuration; validate both services.
  2. Reload PHP-FPM and the web server, then repeat the original application smoke test.
  3. Delete generated /tmp upload test files; retain logs needed for investigation.

Evidence

Sources and review

Verified 2026-07-25Review due 2026-10-23
PHP core configuration directivesofficialPHP-FPM installation and configurationofficialnginx FastCGI moduleofficial