Repository navigation
Test runner sometimes just stops, sometimes gives "Allowed memory size exhausted" error #6840
Description
Activity
PHP 8.3.19I have the same problem after upgrading to version 5.2.1
FATAL ERROR. TESTS NOT FINISHED.
Allowed memory size of 1073741824 bytes exhausted (tried to allocate 1003520 bytes)
An Error occurred while handling another error:
Headers already sent in /var/www/vendor/codeception/codeception/src/Codeception/Subscriber/ErrorHandler.php on line 175Memory consumed(same tests):
Codeception v5.1.2 - 770.34 MB
Codeception v5.2.1 - 1.30 GB
Codeception v5.3.0 - 1.27 GBIf you provide a sample application where this behavior can be replicated I could go commit by commit backwards and find out what change causes it.
In 5.2.0 and 5.2.1 a lot of changes were integrated.
This issue was now gone for a while, however it just re-appeared (
probably introduced by Codeception 5.3.3).My main question is still:
Where is the 1G limit coming from??
(I havememory_limit = 3000Minphp.ini)- added a commit that references this issue
on Dec 19, 2025 Answer: Codeception has a hard
memory_limitof1024M. I'm removing it in #6916Meanwhile, you can override it by adding this to your
codeception.yml:settings: memory_limit: 3G
Codeception has a hard memory_limit of 1024M.
The memory limit is not really "hard" but configurable. The "memory_limit" setting (and its default value of
1024M) is documented in https://codeception.com/docs/reference/Configuration#settings.Root cause: PHPUnit's
EventFacadeis never sealed under CodeceptionI profiled a large suite that OOMs (individual files run fine, full run dies around
~3000 tests) and tracked it down to unbounded accumulation of PHPUnit event objects,
not test/app state.What accumulates
Comparing memory snapshots across the run, the object growth is dominated by PHPUnit's
per-event telemetry:Δ objects (between two snapshots) Class +382k PHPUnit\Event\Telemetry\MemoryUsage+191k PHPUnit\Event\Telemetry\Duration+95k (each) Telemetry\Snapshot/HRTime/Info/GarbageCollectorStatus+81k PHPUnit\Event\Test\AssertionSucceededAssertionSucceededis emitted for every successful assertion, and each event carries a
telemetrySnapshot, so growth is linear in the number of assertions → 2 GB on a big suite.Who retains them
Building a reverse-reachability graph from these objects to a GC root, every instance is held by:
PHPUnit\Event\Facade::instance (static singleton)
└─ DeferringDispatcher
└─ EventCollection → array ← every event ever emitted, for the whole runPHPUnit\Event\DeferringDispatcherstarts withrecording = true; in that statedispatch()
just appends each event to itsEventCollectionand returns. It only stops recording (and clears
the buffer) whenEventFacade::seal()runs.seal()is called in exactly one place —
PHPUnit\TextUI\Application— which Codeception does not use (it wires PHPUnit's components
directly viaTextUI\Configuration). So the facade is never sealed,recordingstaystrue
forever, and every event is retained. Nothing ever consumes them, since Codeception runs its own
(Symfony) event bus.This still reproduces on
main—Suite.phpnever callsseal()— so it's not version-specific.Fix / workaround
Sealing the facade once, early, makes PHPUnit dispatch events directly (to no subscribers) instead
of buffering them. A minimal Codeception extension does the job:use Codeception\Event\SuiteEvent; use Codeception\Events; use Codeception\Extension; use PHPUnit\Event\Facade as EventFacade; final class SealPhpUnitEvents extends Extension { public static array $events = [Events::SUITE_BEFORE => 'seal']; private static bool $done = false; public function seal(SuiteEvent $e): void { if (!self::$done && class_exists(EventFacade::class)) { EventFacade::instance()->seal(); self::$done = true; } } }
This is safe because Codeception registers no subscribers on PHPUnit's EventFacade, so
EventFacadeIsSealedException can't be triggered and no behavior depends on the buffered events.A/B on the same large suite:
┌──────────────┬─────────────────────────────────┬────────────────┐ │ │ Result │ Peak │ ├──────────────┼─────────────────────────────────┼────────────────┤ │ without seal │ OOM mid-run (~2272 tests) │ ~2022 MB │ ├──────────────┼─────────────────────────────────┼────────────────┤ │ with seal │ full suite passes (~3300 tests) │ ~1050 MB, flat │ └──────────────┴─────────────────────────────────┴────────────────┘Memory becomes flat (final ≈ peak) instead of climbing linearly.
Verification methodology (reproducible)
- Install php-meminfo.
- In a Codeception extension, on each TEST_AFTER, log memory_get_peak_usage(true) per test
and dump the live heap with meminfo_dump() whenever peak grows by a threshold. - Run the full suite; peak grows monotonically → accumulation, not a one-off spike.
- Diff object counts per class between an early and a late dump → PHPUnit Event\Telemetry*
and AssertionSucceeded dominate. - Reverse-walk the children graph from those objects to the first is_root node → it's
PHPUnit\Event\Facade::instance → DeferringDispatcher → EventCollection. - Add EventFacade::instance()->seal() on SUITE_BEFORE, re-run → memory stays flat.
Investigated with Claude Code — php-meminfo heap diffing + reverse-reachability analysis.
This write-up was generated by Claude.
Since my latest run (~2 weeks ago), the Codeception runner is suddenly hanging strangely:
Sometimes, in the middle of the suite, it just stops with message "COMMAND DID NOT FINISH PROPERLY."
Sometimes (at another test), it crashes with:
However, the
memory_limitin myphp.inis (for CLI and FPM) is3000M. So where is the 1G limit coming from?This is not caused by the tests themselves, cause when I run just the affected files alone, everything's fine.
Any ideas? Is somebody else experiencing this? Were there recent changes regarding memory management (couldn't find anything)?