laravel/vapor-core
The core service providers and runtime client that power Laravel applications on AWS Lambda. Part of Laravel Vapor, the auto-scaling serverless deployment platform, handling integration with SQS queues, databases, Redis clusters, networks, and CloudFront CDN.
laravel/vapor-core is not a typical "install and call" package — it is the runtime layer that makes a standard Laravel application bootable inside AWS Lambda. You rarely interact with it through a public API; you interact with it through the constraints it imposes on your app.
Minimal setup:
composer require laravel/vapor-core
The VaporServiceProvider is auto-discovered. There is nothing to publish and no config/vapor.php to tune — configuration lives in vapor.yml (CLI side) and in your application's normal Laravel config. Notably, v2.43.4 made the provider skip registration when the app is detected as running on Laravel Cloud, so the same codebase can deploy to both platforms without a conditional.
Where to look first, in order:
VaporServiceProvider — see what is bound and under what conditions.Kernel/HttpKernel, Kernel/ConsoleKernel, Kernel/JobKernel — the three entry points Lambda will invoke.Runtime/Client — the request/response translation between API Gateway payloads and Symfony's Request/Response.Then verify the environment switch works, because you will use it constantly:
use Laravel\Vapor\Vapor;
if (Vapor::isActive()) {
// configure CloudWatch-friendly logging, SQS queue driver, etc.
}
First real use case: deploy a plain Laravel app, hit a route, then check that the app correctly identifies its environment and that logs land in CloudWatch. If that works, the rest of your work is adapting stateful assumptions (filesystem, sessions, long-lived singletons) to a reused container.
Environment-gated service providers. The single most useful pattern is isolating Vapor-specific configuration behind Vapor::isActive() (added in v2.33.2). Logging is the canonical case — v2.31.0 added a conditional log wrapper so the same code writes to stderr on Lambda and to files under FPM:
public function register(): void
{
if (Vapor::isActive()) {
config(['logging.default' => 'stderr']);
config(['queue.default' => 'sqs']);
}
}
Use VAPOR_ENV (v2.37.1) rather than APP_ENV when you need the non-standard Vapor environment name — this matters for anything keyed on environment, including Sentry, Telescope gating, and feature flags.
Structured logging with Lambda context. v2.42.0 exposes the AWS Lambda execution context for logging. Attach it to your log processor so CloudWatch entries carry function name, version, and request ID — it turns "which invocation broke?" into a filterable query rather than a timestamp hunt:
Log::withContext([
'request_id' => $_SERVER['AWS_LAMBDA_LOG_STREAM_NAME'] ?? null,
'function' => $_SERVER['AWS_LAMBDA_FUNCTION_NAME'] ?? null,
]);
HTTP edge cases you will actually hit.
attachments[]) and nested form fields parse correctly now, but only for payloads that API Gateway passes through. Validate binary uploads early.$request->getQueryString() when recomputing HMACs.response()->stream() over building a large string, given Lambda's payload ceiling.Queues and jobs. The JobKernel handles SQS and custom Lambda events (v2.41.0), and Laravel 13 SQS overflow is supported (v2.45.0). Practical implications:
sync.scoped rather than singleton and they will be discarded between invocations in the same container.Console and scheduling. Artisan commands run via a dynamic CLI handler factory (v2.43.0). Sub-minute schedules are normalized by a scheduler proxy command (v2.34.0) — you can write everyTenSeconds() in routes/console.php and it will be honored, but remember the granularity is bounded by how often the schedule Lambda fires. For anything under a minute, prefer an SQS-driven loop or EventBridge rule.
Database and cache resilience. Two changes reflect real production pain:
DB_*_SSL_CA can point at the packaged certificate rather than a path you must bake into the image.PDOException on "MySQL server has gone away" while resetting wait_timeout. Long-lived containers hold connections across idle periods; you should still set DB_POOL-style reconnect behavior and catch QueryException with isLostConnection() in critical paths.Storage parity. v2.38.0 fixed S3-compatible storage support, which is what lets you run the same filesystem config against MinIO under Herd or Docker locally. Use one disk name (s3) and swap the endpoint, rather than branching on environment.
Container reuse is the #1 source of bugs. Lambda keeps the PHP process alive across invocations. Any static cache, memoized array, or singleton holding request data will leak into the next request. Audit for classes that assign $this->request, accumulate counters, or cache authorization decisions. Mark them scoped, or reset in a terminating callback.
Never call exit or die. A hard exit kills the runtime before the response is written back to API Gateway, producing a 502 with a log that looks like a successful request. This includes dd(), var_dump()-then-exit debugging, and any library that short-circuits.
The filesystem is read-only except /tmp. /tmp is per-container, size-limited, and evaporates on cold start. Anything that must survive belongs on S3. storage_path() writes will fail — move those to the s3 disk explicitly.
Config cache changes the env() story. env() outside config files returns null after config:cache, and Vapor caches config at build time. Read runtime values through config(), and set them in vapor.yml under environment:. This is the most common "works locally, null in prod" failure.
Timeout mismatches silently truncate work. Lambda caps at 15 minutes. Set your vapor.yml timeout slightly above the queue job's $timeout (and retry_after above both) or jobs get killed mid-flight and redelivered without ever being marked failed.
Payload limits bite before PHP limits. API Gateway and Lambda have request/response size ceilings well below what you may be used to on FPM. Large Set-Cookie payloads, base64-inflated binary responses, and big JSON responses are all candidates for truncation. If a response works locally and 502s remotely, check size first.
Debugging follows CloudWatch, not storage/logs. Use vapor tail or the CloudWatch console. Because logging is a conditional wrapper on Lambda (v2.31.0/v2.31.1), a mis-set logging.default will silently swallow output — confirm the channel before you conclude your code isn't running. The health check improvements in v2.36.0 also give you a cheap liveness probe to distinguish "app boots" from "app handles requests."
Extension points, ranked by risk. Prefer, in order: environment-gated providers → custom Log processors → a custom queue job handler → replacing the HttpKernel binding. The last one is the highest-risk because the handler contract is what API Gateway speaks; a mistake there manifests as opaque 502s rather than exceptions.
Guard against double-encoding. If you construct Lambda responses manually, remember the X-Vapor-Base64-Encode header semantics: binary bodies must be base64-encoded once and flagged, not encoded inside an already-encoded payload.
Test at the boundary, not the unit. Request tests with server variables mimicking API Gateway shape catch almost every regression class in this package:
$this->withServerVariables([
'HTTP_X_FORWARDED_FOR' => '203.0.113.1',
'QUERY_STRING' => $rawQueryString,
])->get('/webhook');
Keep register() cheap. Every provider boot is paid on cold start. Deferred providers, lazy service resolution, and avoiding package discovery for dev-only tooling all directly reduce p99 latency on low-traffic functions — which, on Lambda, is most of them.
How can I help you explore Laravel packages today?