In PHP, what is the difference between php://memory and php://temp, and how would you use one to buffer a generated report before sending it?
answer
- one stays in RAM, one spills to disk
- 2 MB default threshold
- /maxmemory:NN in bytes
- rewind() before reading back
- fpassthru() or stream_copy_to_stream()
basics
~20 sphp://memory keeps all data in RAM, limited only by memory_limit; php://temp keeps up to 2 MB in memory, then moves to a temporary file. Write the report into php://temp, rewind(), then stream it out with fpassthru() or stream_copy_to_stream().
solid answer
~40 sBoth are read-write streams you open with `fopen('php://memory', 'r+')` or `fopen('php://temp', 'r+')` and use like a file. `php://memory` holds everything in RAM, so a large report counts fully against `memory_limit`. `php://temp` holds data in memory until it passes a threshold, 2 MB by default or `php://temp/maxmemory:NN` in bytes, then continues in a temporary file in the same directory `sys_get_temp_dir()` reports. For a report of unknown size I write rows into `php://temp`, read the size from `fstat($h)['size']` if I need a Content-Length, call `rewind($h)`, and send it with `fpassthru($h)` or `stream_copy_to_stream($h, $out)`. The classic bugs are forgetting `rewind()`, which makes `stream_get_contents()` return an empty string, and turning the whole buffer back into one string, which throws away the memory benefit.
go deeper
Recall that php://memory is RAM only while php://temp spills to a temp file after 2 MB, and that you must rewind() before reading back.
Explain the maxmemory parameter, why stream_get_contents() returns an empty string without rewind(), and why each fopen() creates a new stream.
Show you buffer large exports in php://temp, send them with fpassthru() or stream_copy_to_stream(), and never turn the buffer back into one giant string.
Decide when to buffer at all: complete-before-send guarantees cost memory, disk and latency, while streaming straight out is cheaper but cannot retract a half-sent response.
## Two in-memory stream wrappers PHP's `php://` wrapper includes two streams that behave like a file but have no name on disk: - **`php://memory`** stores its data in memory, always. - **`php://temp`** stores its data in memory until it exceeds a limit, then switches to a temporary file. The default limit is **2 MB**; `php://temp/maxmemory:NN` sets it to `NN` bytes. The file goes in the same directory `sys_get_temp_dir()` returns. Both are opened with `fopen()`, usually in a read-write mode such as `'r+'`, both support `fwrite()`, `fread()`, `fseek()`, `rewind()` and `fstat()`, and both vanish when closed. | | `php://memory` | `php://temp` | |---|---|---| | Where data lives | RAM only | RAM up to the limit, then a temp file | | Memory cost for a 200 MB report | about 200 MB, subject to `memory_limit` | about the limit (2 MB by default) | | Speed | fastest for small data | same as memory until it spills, then disk I/O | | Typical use | small buffers, tests, faking a file handle | output of unknown or large size | ## The report pattern A controller builds an export, needs to know when it is complete (for example to compute a checksum or a size, or to fail cleanly before any byte is sent), and only then sends it: ```php <?php declare(strict_types=1); $buf = fopen('php://temp/maxmemory:' . (4 * 1024 * 1024), 'r+'); foreach ($rows as $row) { fwrite($buf, json_encode($row, JSON_THROW_ON_ERROR) . "\n"); } $size = fstat($buf)['size']; rewind($buf); header('Content-Type: application/x-ndjson'); header('Content-Length: ' . $size); fpassthru($buf); fclose($buf); ``` Step by step: 1. **Open** `php://temp` with a limit that suits the host: small reports never touch disk, large ones cost only the limit in RAM. 2. **Write** rows as they are produced. If generation fails half-way, nothing has been sent yet, so the response can still be an error page. 3. **Measure** with `fstat()`, which both wrappers support. 4. **`rewind()`**: after writing, the pointer is at the end, and reading from there returns nothing. 5. **Send** with `fpassthru($buf)`, which copies the rest of the stream to the output, or `stream_copy_to_stream($buf, fopen('php://output', 'w'))`. `php://output` is a write-only stream that goes through the same output-buffering layer as `echo`. ## Reading it back as a string `stream_get_contents($stream, ?int $length = null, int $offset = -1)` returns the remaining content as one string. With the default offset `-1` it starts at the **current position**, which is why calling it right after writing returns `""`. Pass offset `0` or call `rewind()` first. Keep in mind that turning a 200 MB buffer into a string needs 200 MB, which cancels what `php://temp` saved. ## Pitfalls - **Not reusable.** Each `fopen('php://memory', …)` creates a new, empty stream. `file_put_contents('php://memory', 'x')` followed by `file_get_contents('php://memory')` returns nothing, because the second call opened a different stream. - **`php://memory` is not free.** Its content counts toward `memory_limit`, so it only moves the problem when the data is large. - **The limit is in bytes.** `maxmemory:5` means five bytes, not five megabytes. - **Temp files and disk space.** After spilling, `php://temp` writes to the temp directory, which may be a small `tmpfs`; a huge export can fill it. - **Casting to a C `FILE*`.** The manual notes that some extensions need a standard I/O stream and may fail to cast a memory stream, notably on Windows. ## Choosing the threshold The `maxmemory` value is a trade between RAM and disk per concurrent request: - Multiply it by the number of workers that can build reports at the same time; that is the worst-case memory the buffers add on top of normal usage. - Keep it well below `memory_limit`, because the rows, the encoder and the framework also need memory. - A few megabytes is usually enough to keep typical reports in RAM and push only the rare large export to disk. - If the temp directory is a RAM-backed `tmpfs`, spilling saves PHP memory but not machine memory. ## When to skip the buffer If the report does not need a size, checksum or all-or-nothing behaviour, write rows straight to `php://output` as they are generated. The buffer is worth it when the response must be complete and valid before its first byte leaves.
- Why does stream_get_contents($buf) return an empty string right after the rows were written?Writing leaves the pointer at the end of the stream, and `stream_get_contents()` with its default offset of `-1` reads from the current position. There is nothing after the end, so it returns `""`. Call `rewind($buf)` first, or pass an offset: `stream_get_contents($buf, null, 0)`.
- When would you still pick php://memory over php://temp?When the data is small and bounded and you want no chance of touching disk: a unit test that needs a real stream handle, building a small attachment, or feeding a function that expects a resource. For output whose size depends on user data, `php://temp` caps the memory cost at its threshold.
- How do you check that a php://temp buffer really spilled to disk?Write past the threshold and compare `memory_get_usage()` before and after; with `php://temp` it stays roughly flat beyond the limit, while `php://memory` grows with the data. The temporary file is created in the directory `sys_get_temp_dir()` reports, so watching that directory during a large export also shows it.
php://memory is a whiteboard: fast, but when it is full you are out of space. php://temp is a whiteboard with a filing cabinet next to it: once the board fills, the rest goes into the cabinet, and you read it back the same way.
saying these in an interview costs you the question
- php://memory writes to disk automatically once memory_limit is near.
- php://temp/maxmemory:5 keeps five megabytes in memory.
- Opening php://memory twice returns the same shared buffer.
- stream_get_contents() always reads from the start of the stream.
- php://temp never uses the disk, it is just a faster php://memory.