Summary
On webonyx/graphql-php >= 15.31.0, Lighthouse's lazy schema silently becomes eager on essentially every request. The executor's first built-in scalar lookup (Schema::getType('Boolean') etc. — this happens for virtually any operation, including { __typename }) now triggers scalar-override discovery inside graphql-php, which resolves the schema's types callable. In Lighthouse that callable is TypeRegistry::possibleTypes(), which builds every type in the schema from the AST. Since the Schema is rebuilt per request under PHP-FPM, the documented lazy design is defeated on each request.
Mechanism
SchemaBuilder::build() configures:
// Use lazy type loading to prevent unnecessary work
$config->setTypeLoader(
fn (string $name): ?Type => $this->typeRegistry->search($name),
);
// Enables introspection to list all types in the schema
$config->setTypes(
/** @return array<string, \GraphQL\Type\Definition\Type> */
fn (): array => $this->typeRegistry->possibleTypes(),
);
graphql-php introduced scalar overrides in 15.31.0 (webonyx/graphql-php#1869) and in 15.31.2 moved override discovery exclusively to scanning types (webonyx/graphql-php#1884, see also webonyx/graphql-php#1874). With a lazy types callable, that scan forces full resolution the first time any built-in scalar is looked up.
Measured impact
Production app on Lighthouse 6.66, upgrading graphql-php 15.30.2 → 15.32.3:
- every operation 1.5–3.4× slower wall-clock
- ~6× more PHP work (xdebug cachegrind: 9.1 MB → 57 MB for an authenticated
{ __typename })
- ablation: patching Lighthouse to
setTypes(fn () => []) restores the 15.30.2 baseline
Standalone repro and benchmark table: webonyx/graphql-php#1927.
Affected users currently have only bad options: pin graphql-php to 15.30.2 (re-exposes GHSA-r7cg-qjjm-xhqq, GHSA-fc86-6rv6-2jpm, CVE-2026-40476) or accept the per-request cost.
Proposed change
webonyx/graphql-php#1927 (draft) adds SchemaConfig::setScalarOverrides(?array): null (default) keeps scan-based discovery; an explicit array — including [] — skips the scan entirely.
Lighthouse can determine the overrides cheaply from the AST without building any types: a scalar override is a user-defined type named after a built-in scalar, and $documentAST->types is keyed by name. Sketch (in build(), after setTypes):
// TODO remove the guard when requiring the graphql-php release that ships setScalarOverrides
if (method_exists($config, 'setScalarOverrides')) {
$scalarOverrides = [];
foreach (Type::BUILT_IN_SCALAR_NAMES as $name) {
if (isset($documentAST->types[$name])) {
$type = $this->typeRegistry->get($name);
assert($type instanceof ScalarType);
$scalarOverrides[] = $type;
}
}
$config->setScalarOverrides($scalarOverrides);
}
The method_exists guard keeps the current webonyx/graphql-php: ^15 constraint intact and follows the same pattern as the query-complexity guard added in #2637; it can be dropped whenever the constraint is eventually bumped past the release containing the new API.
For the common case (no built-in scalar names redefined in the SDL) this is five isset() checks and an explicit empty array — the hot path stays fully lazy. The loop is not dead code: TypeRegistry::search() consults $documentAST->types before Type::getStandardTypes(), so a user SDL can redefine e.g. String.
I'm happy to submit a PR with this change plus a regression test asserting that a simple query execution does not invoke possibleTypes(), once the upstream API lands.
Summary
On
webonyx/graphql-php >= 15.31.0, Lighthouse's lazy schema silently becomes eager on essentially every request. The executor's first built-in scalar lookup (Schema::getType('Boolean')etc. — this happens for virtually any operation, including{ __typename }) now triggers scalar-override discovery inside graphql-php, which resolves the schema'stypescallable. In Lighthouse that callable isTypeRegistry::possibleTypes(), which builds every type in the schema from the AST. Since theSchemais rebuilt per request under PHP-FPM, the documented lazy design is defeated on each request.Mechanism
SchemaBuilder::build()configures:graphql-php introduced scalar overrides in 15.31.0 (webonyx/graphql-php#1869) and in 15.31.2 moved override discovery exclusively to scanning
types(webonyx/graphql-php#1884, see also webonyx/graphql-php#1874). With a lazytypescallable, that scan forces full resolution the first time any built-in scalar is looked up.Measured impact
Production app on Lighthouse 6.66, upgrading graphql-php 15.30.2 → 15.32.3:
{ __typename })setTypes(fn () => [])restores the 15.30.2 baselineStandalone repro and benchmark table: webonyx/graphql-php#1927.
Affected users currently have only bad options: pin graphql-php to 15.30.2 (re-exposes GHSA-r7cg-qjjm-xhqq, GHSA-fc86-6rv6-2jpm, CVE-2026-40476) or accept the per-request cost.
Proposed change
webonyx/graphql-php#1927 (draft) adds
SchemaConfig::setScalarOverrides(?array):null(default) keeps scan-based discovery; an explicit array — including[]— skips the scan entirely.Lighthouse can determine the overrides cheaply from the AST without building any types: a scalar override is a user-defined type named after a built-in scalar, and
$documentAST->typesis keyed by name. Sketch (inbuild(), aftersetTypes):The
method_existsguard keeps the currentwebonyx/graphql-php: ^15constraint intact and follows the same pattern as the query-complexity guard added in #2637; it can be dropped whenever the constraint is eventually bumped past the release containing the new API.For the common case (no built-in scalar names redefined in the SDL) this is five
isset()checks and an explicit empty array — the hot path stays fully lazy. The loop is not dead code:TypeRegistry::search()consults$documentAST->typesbeforeType::getStandardTypes(), so a user SDL can redefine e.g.String.I'm happy to submit a PR with this change plus a regression test asserting that a simple query execution does not invoke
possibleTypes(), once the upstream API lands.