The pitch for Laravel Vapor is the easiest sell in PHP infrastructure: vapor deploy production and your Laravel app is running on AWS Lambda, behind API Gateway, talking to RDS and Redis, with queues on SQS and uploads going straight to S3. No Nginx, no PHP-FPM, no servers to keep patched. The first time it works, it feels like cheating.
Then you ship a long-running export, watch a single endpoint burn a thousand dollars of NAT gateway traffic, or wait twenty seconds for a cold start at 3 AM on a low-traffic page. Vapor is genuinely good — but it is not magic, and the trade-offs are specific. This is what's actually under the hood and where it bites.
What Vapor Actually Is
Vapor is two things glued together:
- A managed deployment service at vapor.laravel.com — the dashboard, the build pipeline, the rollback button.
- A runtime layer — the
laravel/vapor-coreComposer package plus thelaravel/vapor-clitool, which together package your app as a Lambda function. The actual PHP runtime layer is built on top of Bref, the open-source PHP-on-Lambda project.
When you run vapor deploy, the CLI bundles your app, uploads it to S3, and provisions or updates a CloudFormation stack that wires together:
- API Gateway (REST API by default, HTTP API opt-in via
gateway-versioninvapor.yml) — the public entrypoint, routes to Lambda. - Lambda functions — one for HTTP, one or more for queues, one for the scheduler.
- RDS — MySQL or Postgres, optionally fronted by RDS Proxy for connection pooling.
- ElastiCache (Redis) — for cache and session.
- SQS — every queued job becomes a message; a Lambda consumer drains it.
- S3 — uploads, plus the place your assets and
vapor-cliartifacts live. - CloudFront — fronts S3 for assets and optionally fronts the API for caching.
Your Laravel app is broadly the same. routes/web.php still works. php artisan queue:work becomes "Vapor's queue consumer Lambda invokes the same job class." The job classes don't change.
Cold Starts: Real, Manageable, Not Always
A cold start happens when AWS spins up a new Lambda execution environment. PHP on Lambda (via Bref) is fast as runtimes go — a typical cold start lands somewhere between 200 and 700 ms, depending on package size, region, and whether you're inside a VPC. The first Eloquent query that has to negotiate a TLS handshake and a DB connection pushes that toward a second.
Three things keep cold starts from showing up in user-facing latency:
- Provisioned concurrency. AWS keeps N execution environments warm. Vapor exposes this as
warminvapor.yml. Costs a flat rate per hour per warm slot, but eliminates user-visible cold starts on critical endpoints. - Keeping the app out of the VPC when possible. Lambdas inside a VPC need an ENI, which adds 100–300 ms to cold start. If you can talk to RDS via RDS Proxy with the public endpoint enabled and IAM auth, you can avoid VPC mode for most paths — though many teams keep VPC mode for security and accept the cost.
- Slimming the deployment artifact. Every megabyte of
vendor/andnode_modules/is a megabyte that has to load on cold start. Runcomposer install --no-dev --optimize-autoloader. Don't bundle your tests. Vapor has aruntime: php-8.3:al2(or whatever you target) — pick the Amazon Linux 2 / AL2023 runtime your packages actually need.
The honest expectation: 95% of requests served from warm Lambdas at FPM-class latency, with occasional sub-second cold starts on cold paths. If your SLO is "every request under 300 ms p99 with no warm-up," Vapor isn't your tool — go to Forge or ECS.
The 15-Minute Limit Is A Hard Wall
Lambda has a maximum execution time of 15 minutes. There is no extending this. Anything that runs longer must be split:
- Long imports / exports / report generation — chunk into queued jobs that each finish in under 15 minutes (give yourself a 12-minute target with margin).
- Large file processing — use S3 multipart uploads from the browser straight to S3, then process in a queued job after the upload completes.
- Scheduled work — the scheduler Lambda dispatches jobs; the work runs in queue Lambdas.
Bus::batch([...]) is the right primitive for "this used to be one long script." Each job in the batch has its own 15-minute envelope, and the batch tracks progress.
The other limits worth knowing: 6 MB request/response payload (use S3 for anything bigger), 512 MB to 10 GB ephemeral disk at /tmp (configurable), no persistent disk. Any code that writes to storage/app/local needs to be moved to S3 — the local filesystem dies with the function.
Database Connections Are The Silent Killer
A Lambda that runs once per request and opens a DB connection on each invocation will exhaust your RDS connection limit on a small spike. The fix is RDS Proxy — a managed connection pooler that sits between Lambda and the database.
Vapor handles the wiring; you opt in per environment in vapor.yml:
environments:
production:
database: my-prod-db
database-proxy: my-prod-db-proxy
cache: my-prod-cache
memory: 1024
cli-memory: 1024
runtime: php-8.3:al2
warm: 5
queue-warm: 1
timeout: 30
queue-timeout: 600
build:
- 'composer install --no-dev --optimize-autoloader'
- 'php artisan event:cache'
- 'php artisan view:cache'
Without the proxy, a moderate spike (a few hundred concurrent invocations) opens a few hundred connections to RDS, which on a db.t4g.medium (default of ~85 connections) means errors. With the proxy, Lambda holds one connection per execution environment, the proxy multiplexes them, and RDS sees a healthy pool.
The catch: RDS Proxy adds ~5 ms of latency per query and costs roughly $0.015 per vCPU-hour. For most apps it pays for itself the first time you avoid an outage.
The NAT Gateway Surprise
The single most expensive accidental fact about Lambda-in-VPC is the NAT gateway. Lambdas inside a private subnet that need to reach anything outside the VPC — Stripe, SendGrid, S3, even AWS APIs without a VPC endpoint — route through a NAT gateway. NAT gateway pricing is ~$0.045/hour plus $0.045 per GB of processed traffic.
A Vapor app calling Stripe on every request, downloading 500 MB videos from S3 inside a job, or pulling external APIs at scale will rack up four-figure NAT bills before anyone notices. The fixes:
- VPC endpoints for S3, DynamoDB, SQS, Secrets Manager — they bypass NAT entirely. S3 in particular is a free Gateway Endpoint; turn it on and never look back.
- Route external API calls through CloudFront if cacheable, or accept the NAT cost as the price of doing business.
- Move the Lambda out of the VPC for paths that don't need RDS access, if your security model allows it.
Watch the NAT gateway metric in CloudWatch from day one. The first time you see a vertical line on the data-processed graph, find which job did it.
Queues, Storage, And The Stateless Mindset
Three pieces of Laravel that change shape on Vapor:
Queues become SQS. The default messages-per-batch and visibility timeouts mean Vapor's queue Lambda pulls up to 10 messages at once, gives each one your queue timeout, and re-queues failures. Idempotency matters more here than on a traditional worker — the same job can run twice if a Lambda dies mid-way. Use unique job IDs and check-and-set semantics on writes.
Storage becomes S3. Set FILESYSTEM_DISK=s3 and never touch storage/app/local. For private files, use signed URLs (Storage::temporaryUrl()). For large uploads, use the vapor-js pre-signed multipart upload helper directly from the browser to S3 — never proxy uploads through Lambda, you'll hit the 6 MB payload limit instantly.
Sessions and cache become Redis. ElastiCache is reachable from inside the VPC. Set SESSION_DRIVER=redis and CACHE_STORE=redis. The file driver doesn't survive between invocations, and the database driver works but is slower than Redis.
When Vapor Is The Right Tool
Vapor earns its keep on apps with the right shape:
- Spiky traffic. A SaaS that's quiet at night and busy at 9 AM. Lambda scales the workers to zero between spikes; you pay nothing for idle capacity.
- Background-heavy work that fits in 15-minute jobs. Webhooks, notifications, async processing.
- Teams without dedicated DevOps. No Nginx, no PHP-FPM, no patching. Vapor is more expensive per request than Forge but cheaper per engineer-hour.
Vapor is the wrong tool when:
- Traffic is steady and high. A constantly busy app pays Lambda's per-invocation premium for no benefit. Two t3.medium ECS tasks behind an ALB cost a fraction of the equivalent Lambda hours at sustained load.
- You need long-running processes. WebSockets, long polling, anything over 15 minutes — Vapor is wrong. Reverb on Forge or ECS is right.
- You need GPU, persistent disk, or filesystem-heavy workloads. Lambda doesn't.
Pricing, Honestly
Vapor itself is $39 per month for the basic team plan. The AWS bill is the real number. A small production app — a few hundred thousand requests a month, a db.t4g.small RDS, a cache.t4g.micro ElastiCache, RDS Proxy on, modest queue volume — typically lands around $80–150 a month on AWS. The breakdown shifts dramatically:
- RDS and ElastiCache are the floor — they run 24/7.
- Lambda invocations are usually under $20 a month at this scale.
- NAT gateway is the wildcard — $30–500+ depending on what you call externally.
- Data transfer out (CloudFront, API Gateway) scales with payload sizes.
Watch Cost Explorer weekly for the first month. The numbers settle quickly once you've turned on VPC endpoints and right-sized RDS.
A One-Sentence Mental Model
Laravel Vapor packages your app as a Bref-based Lambda behind API Gateway with RDS Proxy, ElastiCache, SQS, and S3 wired in — a great fit for spiky CRUD apps and async-heavy workloads if you're willing to design around the 15-minute limit, manage cold starts, and watch the NAT gateway bill from day one.




