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.
Accept intended upload sizes while rejecting oversized or slow requests predictably at the correct layer.
- PHP 8.2, 8.3, 8.4, 8.5
- 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.
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
- 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.
- 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.
- The request passes the edge and route-scoped web body ceiling.
- PHP streams multipart parts into the private temp directory while enforcing its configured budgets.
- The application validates the uploaded file and atomically moves accepted content to non-executable storage.
- 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/publicSecurity 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.
instruction
Map every request-size enforcement layer
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.
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 || trueCurrent 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.
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.
instruction
Define the business limit and disk budget
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.
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 MemoryMaxByte, 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.
df -h /srv/billing/tmpdf -i /srv/billing/tmpquota -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.
config
Align upload_max_filesize and post_max_size
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.
/etc/php/{{phpVersion}}/fpm/conf.d/90-{{app}}-uploads.iniValues 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/uploadsValues stay on this page and are never sent or saved.
sudoedit /etc/php/{{phpVersion}}/fpm/conf.d/90-{{app}}-uploads.inifile_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.
sudo php-fpm8.3 -ttgrep -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.
config
Bound time, input, and memory
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.
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.iniRuntime 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.
curl -v --limit-rate 1m -F file=@/tmp/upload-ok.bin https://billing.example.com/internal/upload-testjournalctl -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.
config
Use a private temporary upload directory
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.
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/uploadsThe 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.
namei -l /srv/billing/tmp/uploadssudo -u billing test -w /srv/billing/tmp/uploadssudo 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.
config
Set the matching web-server limit
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.
sudo grep -R --line-number -E 'client_max_body_size|LimitRequestBody' /etc/nginx /etc/apache2 2>/dev/nullThe 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.
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.
verification
Validate and reload affected services
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.
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}}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.
systemctl is-active nginx apache2sudo 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.
verification
Test below, at, and above the limit
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.
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}}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.
stat -c '%s %n' /tmp/upload-*.bincurl -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
SCRIPT_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.sudo 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.
sudo nginx -T 2>&1 | grep -n client_max_body_sizesudo apachectl -t -D DUMP_RUN_CFGcurl -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.
journalctl -u php8.3-fpm --since '-10 min' -n 100df -h /srv/billing/tmp/uploadsdf -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.
sudo find /srv/billing/tmp/uploads -type f -printf '%TY-%Tm-%Td %TT %s %p\n' | tailjournalctl -u php8.3-fpm -n 100
ResolutionFix lifecycle/permissions, add a conservative age-based cleanup with open-file awareness, and monitor bytes/inodes.
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.
- Restore archived INI and virtual-host configuration; validate both services.
- Reload PHP-FPM and the web server, then repeat the original application smoke test.
- Delete generated /tmp upload test files; retain logs needed for investigation.
Evidence