Back to blog
FILE 0x41·COLD-START 500S FROM LEFTOVER COMPOSER DEV DEPENDENCIES

Cold-start 500s from leftover Composer dev dependencies

May 1, 2026 · php, lambda, composer

A PHP function on Lambda started returning 500 on cold start after a routine deploy. Warm invocations were fine. The error in CloudWatch was specific and weird:

PHP Fatal error: Failed opening required '.../vendor/myclabs/deep-copy/src/DeepCopy/deep_copy.php'

That file is part of myclabs/deep-copy, which is a transitive dev dependency of PHPUnit. It has no business being required at runtime.

What was happening

The deploy was the standard serverless deploy flow. The serverless.yml excluded dev vendor paths from the package:

package:
  patterns:
    - '!vendor/myclabs/**'
    - '!vendor/phpunit/**'
    - '!vendor/squizlabs/**'

So the file genuinely wasn't in the deployment zip. But the autoload classmap was insisting it was, and PHP was honoring the classmap on cold start.

What I found

Earlier that day I'd run composer update to bump an unrelated dependency. composer update regenerates vendor/composer/autoload_classmap.php based on whatever is in vendor/ at that moment — and at that moment, dev dependencies were installed, so the classmap had entries pointing at myclabs/deep-copy paths.

serverless deploy then bundled the autoload files (correctly — they're required for any PHP runtime) but excluded the actual dev vendor directories (also correctly — you don't want PHPUnit shipping to production). The result was a classmap that referenced files that weren't in the package. On cold start, PHP's autoloader trusts the classmap and tries to require the listed file. It's not there. Fatal error.

The fix

One line, always run before serverless deploy:

composer install --no-dev --optimize-autoloader

--no-dev removes dev packages from vendor/. --optimize-autoloader regenerates the classmap based on the cleaned tree. Now the autoload files match the deployment zip, and there's no phantom path for PHP to chase.

I wrapped both in a make deploy target so I can't forget:

deploy:
    composer install --no-dev --optimize-autoloader
    serverless deploy
    composer install

The trailing composer install puts the dev dependencies back so the next local test run still has PHPUnit.

What I'd do differently

The bug was subtle because warm invocations didn't reproduce it — the autoloader doesn't always walk the classmap on a hot worker. Only cold starts surfaced the missing file, which made it look like an intermittent platform issue at first. Lesson: any deploy artifact whose generation depends on the local state of vendor/, node_modules/, or similar trees needs an explicit "build clean" step. Don't let composer update for a dev convenience leak into your production package.