Problem Statement
When PHP hits memory_limit and the Sentry breadcrumb buffer contains large entries (e.g. SQL queries with bindings), the OOM event is silently discarded. The SDK's 5 MiB headroom (sentry-php#1633) is insufficient when serializing the event requires more memory than is available after OOM recovery.
Measured: serializing 100 breadcrumbs × ~100 KB each requires ~15 MB of working memory. With only 5 MB headroom, the secondary OOM during json_encode is caught by ErrorHandler::invokeListeners() (catch (\Throwable) {}) and silently discarded.
This makes the failure intermittent — events are captured when breadcrumbs happen to be small at the time of OOM, but lost when they've accumulated large entries.
Root Cause
With max_breadcrumbs=100 (default) and SQL bindings enabled (common in Laravel), the breadcrumb buffer accumulates serializable data proportional to query complexity. When OOM occurs:
ErrorHandler::handleFatalError() frees $reservedMemory (16 KiB) and raises memory_limit by 5 MiB
FatalErrorListenerIntegration attempts to capture the exception
PayloadSerializer::serialize() → json_encode() needs to allocate a contiguous buffer for the output — this requires ~15 MB for 100 breadcrumbs with large metadata
- The allocation fails (only ~5 MiB available), triggering a secondary OOM
- The secondary OOM is caught by
ErrorHandler::invokeListeners() at ErrorHandler.php line 496 and silently discarded
- The original OOM event is lost with no indication
Reproduction
Tested with sentry/sentry 4.29.0, PHP 8.3. Run php reproduction.php — output is EVENT LOST.
reproduction.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use Sentry\Breadcrumb;
use Sentry\Event;
use Sentry\Integration\FatalErrorListenerIntegration;
use Sentry\SentrySdk;
use Sentry\State\Scope;
use Sentry\Transport\Result;
use Sentry\Transport\ResultStatus;
use Sentry\Transport\TransportInterface;
final class RecordingTransport implements TransportInterface
{
public function send(Event $event): Result
{
file_put_contents(__DIR__ . '/oom_result.txt', 'EVENT CAPTURED');
return new Result(ResultStatus::success(), $event);
}
public function close(?int $timeout = null): Result
{
return new Result(ResultStatus::success());
}
}
final class AppState
{
/** @var array<string> */
public static array $data = [];
}
@unlink(__DIR__ . '/oom_result.txt');
\Sentry\init([
'dsn' => 'http://key@localhost/1',
'max_breadcrumbs' => 100,
'transport' => new RecordingTransport(),
'integrations' => [new FatalErrorListenerIntegration()],
]);
// Fill 100 breadcrumbs with ~100KB each (simulates SQL queries with bindings).
// Serializing the full event requires ~15MB of working memory.
SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope): void {
for ($i = 0; $i < 100; $i++) {
$bindings = [];
for ($j = 0; $j < 200; $j++) {
$bindings["param_{$j}"] = str_repeat('x', 512);
}
$scope->addBreadcrumb(new Breadcrumb(
Breadcrumb::LEVEL_INFO,
Breadcrumb::TYPE_DEFAULT,
'db.sql.query',
'SELECT * FROM t WHERE id IN (' . implode(', ', array_fill(0, 50, '?')) . ')',
$bindings,
));
}
});
// Fill persistent memory to near the limit (simulates DI container, models, caches).
// These survive the fatal error — they are NOT freed.
ini_set('memory_limit', '24M');
$limit = 24 * 1024 * 1024;
$sizes = [64, 128, 256, 512, 1024, 2048, 4096, 8192];
$counter = 0;
while (memory_get_usage(true) < $limit - 128 * 1024) {
AppState::$data[] = str_repeat('P', $sizes[$counter % count($sizes)]);
$counter++;
}
register_shutdown_function(static function (): void {
if (file_exists(__DIR__ . '/oom_result.txt')) {
echo "RESULT: EVENT CAPTURED\n";
} else {
echo "RESULT: EVENT LOST — OOM error silently swallowed\n";
}
});
// Trigger OOM. Top-level allocation fails, freeing nothing.
// After fatal: ~5MB headroom. Needed for serialization: ~15MB. → secondary OOM, discarded.
$trigger = str_repeat('Z', 256 * 1024);
Suggested Fix
When handleFatalError() detects an OOM (by checking the error message), clear or heavily truncate the breadcrumb buffer on the current scope before attempting capture. This reduces the serialization payload to well under 5 MiB regardless of what accumulated during normal execution.
Alternatively, the serialization path could catch OOM specifically and retry with a stripped-down event (no breadcrumbs, minimal context) — a degraded report is better than no report.
Workaround
Set max_breadcrumbs to a low value (e.g. 20) in your Sentry config. This keeps useful debugging context while keeping the serialization payload within the headroom budget.
Environment
sentry/sentry: 4.29.0 (also confirmed on 4.19.1)
- PHP: 8.3
memory_limit: 512M (production; reproduction uses 24M for speed)
max_breadcrumbs: 100 (default)
sql_bindings: true (Laravel default via sentry-laravel)
Problem Statement
When PHP hits
memory_limitand the Sentry breadcrumb buffer contains large entries (e.g. SQL queries with bindings), the OOM event is silently discarded. The SDK's 5 MiB headroom (sentry-php#1633) is insufficient when serializing the event requires more memory than is available after OOM recovery.Measured: serializing 100 breadcrumbs × ~100 KB each requires ~15 MB of working memory. With only 5 MB headroom, the secondary OOM during
json_encodeis caught byErrorHandler::invokeListeners()(catch (\Throwable) {}) and silently discarded.This makes the failure intermittent — events are captured when breadcrumbs happen to be small at the time of OOM, but lost when they've accumulated large entries.
Root Cause
With
max_breadcrumbs=100(default) and SQL bindings enabled (common in Laravel), the breadcrumb buffer accumulates serializable data proportional to query complexity. When OOM occurs:ErrorHandler::handleFatalError()frees$reservedMemory(16 KiB) and raisesmemory_limitby 5 MiBFatalErrorListenerIntegrationattempts to capture the exceptionPayloadSerializer::serialize()→json_encode()needs to allocate a contiguous buffer for the output — this requires ~15 MB for 100 breadcrumbs with large metadataErrorHandler::invokeListeners()at ErrorHandler.php line 496 and silently discardedReproduction
Tested with
sentry/sentry4.29.0, PHP 8.3. Runphp reproduction.php— output isEVENT LOST.reproduction.php
Suggested Fix
When
handleFatalError()detects an OOM (by checking the error message), clear or heavily truncate the breadcrumb buffer on the current scope before attempting capture. This reduces the serialization payload to well under 5 MiB regardless of what accumulated during normal execution.Alternatively, the serialization path could catch OOM specifically and retry with a stripped-down event (no breadcrumbs, minimal context) — a degraded report is better than no report.
Workaround
Set
max_breadcrumbsto a low value (e.g. 20) in your Sentry config. This keeps useful debugging context while keeping the serialization payload within the headroom budget.Environment
sentry/sentry: 4.29.0 (also confirmed on 4.19.1)memory_limit: 512M (production; reproduction uses 24M for speed)max_breadcrumbs: 100 (default)sql_bindings: true (Laravel default via sentry-laravel)