How I bypassed Vercel's 250MB limit using a Go serverless proxy, compressed vendor, and a bundled PHP-FPM binary
The Problem
I wanted to deploy a Laravel + Filament app on Vercel's free Hobby plan. Two walls hit me immediately:
-
250MB size limit — Vercel serverless functions have a hard 250MB unzipped cap. My
vendor/directory alone was over 200MB with Filament, Dompdf, and other packages. -
HTTP 494 errors — Using
SESSION_DRIVER=cookie(the only stateless option) caused "Request Header Too Large" because Filament stuffs too much data into the session cookie, exceeding Vercel's ~8KB header limit.
vercel-php is a great project, but it couldn't solve the size problem for heavy apps.
The Solution: A Go Proxy + Compressed Vendor
I built vercel-laravel-go — a Go serverless function that wraps PHP-FPM and compresses vendor from ~200MB down to ~30MB.
Here's what happens at a high level:
HTTP Request → Vercel → Go Handler → FastCGI → PHP-FPM → Laravel
The Go function handles the entire PHP lifecycle:
- Bundles a static PHP-FPM binary at build time (no runtime download needed)
-
Extracts
vendor.tar.gzto/tmp/vendoron cold start (~30MB compressed vs 200MB+ raw) - Creates symlinks so Composer's autoloader resolves classes correctly
-
Redirects storage to writable
/tmp/storage(since/var/taskis read-only on Vercel) - Starts PHP-FPM on a Unix socket and proxies every request via FastCGI
Why Go?
Vercel's @vercel/go runtime compiles everything into a single binary. This lets me:
- Embed PHP-FPM and vendor.tar.gz into the function package
- Use
sync.Oncefor one-time cold start initialization - Keep PHP-FPM alive across warm invocations (container reuse)
- Handle FastCGI protocol directly without extra dependencies
The Technical Deep Dive
Vendor Compression
The pack.sh script runs composer install --no-dev --optimize-autoloader, then compresses vendor/ into a tarball, excluding tests and docs:
# Run locally before deploy
bash scripts/pack.sh
This typically gets a 200MB+ vendor directory down to ~30MB.
Build-Time Preparation
During Vercel's build phase, vercel-prepare.sh runs automatically:
- Copies Laravel source files (
app/,config/,routes/, etc.) intoapi/laravel/ - Downloads the static PHP-FPM 8.4 binary from static-php-cli
These get bundled with the Go function — no network calls at runtime.
Cold Start Bootstrap
When the first request hits, the Go handler:
- Extracts the PHP-FPM binary to
/tmp/php-fpm-bin - Extracts
vendor.tar.gzto/tmp/vendor - Creates writable directories under
/tmp/storage/ - Creates symlinks:
/tmp/app → /var/task/laravel/app,/tmp/config → /var/task/laravel/config, etc. - Generates a PHP entry point that overrides
PackageManifest::$vendorPathandstoragePath - Starts PHP-FPM and waits for the socket
Subsequent requests on the same container skip all of this — PHP-FPM stays running.
Why the Symlinks?
This is the trickiest part. Composer's autoloader calculates $baseDir as two directories up from vendor/composer/. When vendor lives at /tmp/vendor/, it looks for app classes at /tmp/app/, /tmp/config/, etc. But our source files are at /var/task/laravel/. Symlinks bridge that gap.
Sessions with Redis
Cookie sessions are out (HTTP 494). File sessions are out (each invocation gets a fresh /tmp). The solution: Upstash Redis (free tier).
SESSION_DRIVER=redis
REDIS_CLIENT=predis
REDIS_URL=rediss://default:password@host:port
Getting Started
It's a one-liner install. Run this in your Laravel project root:
curl -fsSL https://raw.githubusercontent.com/kristiansntsdev/vercel-laravel-go/main/install.sh | bash
Then:
# 1. Compress vendor
bash scripts/pack.sh
# 2. Deploy
vercel deploy --prod
Environment Variables
Set these in the Vercel dashboard:
| Variable | Value |
|---|---|
APP_KEY |
php artisan key:generate --show |
DB_* |
Your database credentials |
REDIS_URL |
Upstash Redis URL |
Everything else (cache paths, session config, log channel) is pre-configured in vercel.json.
CI/CD with GitHub Actions
The installer also sets up .github/workflows/deploy.yml. Add three secrets to your repo:
-
VERCEL_TOKEN— from vercel.com → Settings → Tokens -
VERCEL_ORG_ID— your Vercel user/team ID -
VERCEL_PROJECT_ID— from your project settings
Push to main and it deploys automatically.
Performance
| Metric | Value |
|---|---|
| Cold start | ~2–3 seconds |
| Warm request | Fast (PHP-FPM stays alive) |
| Bundle size | ~50–80MB compressed |
| PHP version | 8.4 (static binary) |
Reducing Cold Starts
Use a free external pinger to keep your container warm:
- cron-job.org — ping every 5 minutes, free
- UptimeRobot — monitor every 5 minutes, free
Limitations
Be aware of the trade-offs:
- No persistent filesystem — use a managed database
- No queue workers — run those on Railway, Fly.io, or similar
- No cron — use an external scheduler or Vercel Pro cron
- 60s max request duration on Hobby plan
- No static IP — use a serverless-friendly database (Neon, Supabase, PlanetScale)
Wrapping Up
If you've been stuck trying to deploy a heavy Laravel app on Vercel, give vercel-laravel-go a try. It works with Laravel 11 and 12, handles Filament apps without breaking a sweat, and deploys on Vercel's free tier.
The key insight: compress vendor, bundle PHP-FPM, and let Go handle the plumbing.
Star the repo if it helps: github.com/kristiansntsdev/vercel-laravel-go
Top comments (0)