skip to content

In the PHP engine, what do the MINIT, RINIT, RSHUTDOWN and MSHUTDOWN phases do, and what survives from one request to the next?

level: seniorimportance: should knowfreq 30%

answer

  1. module phases once per process
  2. request phases once per request
  3. ini_set reverted at request end
  4. emalloc request memory vs persistent
  5. bootstrap cost paid every request

basics

~20 s

MINIT and MSHUTDOWN run once per process, when extensions and php.ini are loaded and unloaded; RINIT and RSHUTDOWN run around every request. Only engine-level state from module startup survives between requests; all script state and request memory is torn down.

solid answer

~40 s

PHP's lifecycle has two nested loops. **Module startup (MINIT)** runs once when a process starts: php.ini is parsed, extensions register their functions, classes, constants and ini entries. Then, for **each request**, **RINIT** lets each extension set up per-request state, the script runs, and **RSHUTDOWN** cleans it up; after that the engine restores any `ini_set()` changes and frees the request's memory. **MSHUTDOWN** runs once when the process exits. So what survives between requests is what MINIT or a persistent allocation created: loaded extensions, parsed ini values, the realpath cache, persistent connections and an opcode cache's shared memory. Script variables, objects and statics never do. The senior point is the cost: each request rebuilds the application from scratch, re-running autoloader registration, configuration loading and container setup.

code

php · 7 lines
php
<?php
// Request A
ini_set('memory_limit', '512M');
echo ini_get('memory_limit'); // 512M

// Request B, served later by the same FPM worker
echo ini_get('memory_limit'); // back to the php.ini value, e.g. 128M

go deeper

for a junior

Know that PHP starts each request from scratch and that php.ini and extensions are loaded once when the process starts.

for a middle

Name the four phases and what each does, and explain why ini_set() and statics are reset between requests on the same worker.

for a senior

Separate request memory from persistent memory, list what does survive, and reason about bootstrap cost per request and how to shrink it.

for a principal

Judge when per-request bootstrap cost justifies moving away from share-nothing, weighing the automatic cleanup and isolation you would give up.

## Two loops, not one The share-nothing model is easiest to understand as two nested loops inside a PHP process, whether that process is a PHP-FPM worker, an Apache child with the PHP module, or a single CLI run. 1. **Module startup (MINIT)**, once per process. 2. For each request: 1. **Request startup (RINIT)**; 2. compile and execute the script; 3. **Request shutdown (RSHUTDOWN)**, followed by the engine's own teardown. 3. **Module shutdown (MSHUTDOWN)**, once per process. The names come from the hooks an extension written in C declares, `PHP_MINIT_FUNCTION`, `PHP_RINIT_FUNCTION`, `PHP_RSHUTDOWN_FUNCTION` and `PHP_MSHUTDOWN_FUNCTION`, but the phases apply to the whole engine. ## What each phase does | Phase | How often | Typical work | |---|---|---| | MINIT | once per process | parse php.ini, load extensions, register functions, classes, constants and ini entries | | RINIT | once per request | reset per-request extension state, populate superglobals, start sessions if configured | | script | once per request | compile to opcodes, execute | | RSHUTDOWN | once per request | extensions release per-request resources | | MSHUTDOWN | once per process | extensions free global resources, unregister ini entries | On the CLI, the request loop runs exactly once, so both loops collapse into one run. On PHP-FPM, one worker process goes through MINIT once and then runs the request loop many times. ## The request shutdown sequence In `php_request_shutdown()` the engine performs, in this order, among other steps: 1. calls functions registered with `register_shutdown_function()`; 2. destroys objects still alive; 3. flushes all output buffers; 4. runs every extension's RSHUTDOWN; 5. destroys the superglobals; 6. shuts down the executor and compiler and **restores ini entries changed at runtime**; 7. frees the request's memory through the Zend memory manager. Step 6 is why `ini_set('memory_limit', '512M')` in one request never affects the next: the directive goes back to its php.ini value. Step 7 is why a leak inside one web request is usually harmless: memory allocated with the engine's request allocator (`emalloc`) is released wholesale. ## Request memory and persistent memory The engine has two allocation lifetimes: - **Request memory** (`emalloc` and friends): everything a script creates, including compiled opcodes when no opcode cache stores them. Freed at the end of every request. - **Persistent memory** (`pemalloc` with the persistent flag): allocated by MINIT or by features designed to outlive requests. Kept until the process ends. ## What survives between requests - loaded extensions and the classes and functions they registered; - ini values parsed from php.ini at startup; - the **realpath cache**, which saves filesystem lookups for include paths (its sizing is a separate topic); - persistent database connections, when requested; - an opcode cache's shared memory, which holds compiled code. What never survives: variables, statics, objects, user-defined functions and classes loaded by the script, registered autoloaders and error handlers, and `ini_set()` changes. ## Why this matters in production Because user-land state is rebuilt every time, **request startup cost** is real. A framework application typically re-runs on each request: - autoloader registration and class loading; - configuration parsing and environment reading; - service container construction and route table building. The usual mitigations keep the share-nothing model and make the rebuild cheaper: cache compiled code, cache configuration and routes as generated PHP files, and avoid heavy work at bootstrap. The alternative, runtimes that keep the application in memory across requests, removes the cost but gives up the automatic cleanup that makes share-nothing forgiving, and it is a separate topic. ## Common misconceptions - **"FPM keeps my app warm."** It keeps the *process* warm: extensions and ini are loaded, but the application is rebuilt on every request. - **"MINIT runs per request."** Module startup runs once per process; RINIT is the per-request hook. - **"Leaks pile up in the worker."** Request memory is released wholesale at shutdown; only persistent allocations outlive a request.

  • Why does an ini_set() call in one request not leak into the next one on the same worker?
    During request shutdown the engine restores every ini entry changed at runtime to the value it had after startup. The worker keeps its parsed php.ini, but per-request overrides are rolled back before the next request.
  • Which cost does the share-nothing model add, and how do teams reduce it without leaving it?
    Each request re-runs the application bootstrap: autoloader setup, configuration loading, container and route building, and compilation of every file it touches. Teams cache compiled code, dump configuration and routes into generated PHP files, and keep bootstrap work lazy.
  • Does a small memory leak in a web request matter?
    Usually not for correctness: request memory is released wholesale at shutdown, so it cannot accumulate across requests. It still counts against memory_limit within that request, and it matters in long CLI runs, where the single request lasts until the script exits.

saying these in an interview costs you the question

  • MINIT runs at the start of every request.
  • ini_set() changes persist in the worker for later requests.
  • Objects built during bootstrap are reused by the next request.
  • PHP-FPM keeps the application in memory between requests.
  • Memory a script leaks in one request stays allocated for the next.