π Volume III: Advanced Architecture
π Topic 12: Statelessness Doctrine
Your server must be killable at any moment. Design for failure.
"Your server is not special. It will die.
When it does, your users shouldn't notice.
If they do, you failed the Statelessness Doctrine."
β οΈ THE HORIZONTAL SCALING KILLER
The #1 reason Laravel apps can't scale horizontally? State stored on the server. Sessions in files. Images on local disk. Logs on local disk. Config in .env (not cached). When you try to run 10 servers behind a load balancer, each server has different data. Users get logged out randomly. Uploaded images disappear. Statelessness is the foundation of scalability.
π΄ The Problem: State Stored Locally
β
β STATEFUL ARCHITECTURE (CANNOT SCALE)
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β Load Balancer
β β
β ββββββ΄βββββ¬ββββββββββ¬ββββββββββ
β βΌ βΌ βΌ βΌ
β βββββββ βββββββ βββββββ βββββββ
β βServerβ βServerβ βServerβ βServerβ
β β 1 β β 2 β β 3 β β 4 β
β β β β β β β β β
β βSessionβ βSessionβ βSessionβ βSessionβ
β β Files β β Files β β Files β β Files β
β β β β β β β β β
β βUploadsβ βUploadsβ βUploadsβ βUploadsβ
β β(local)β β(local)β β(local)β β(local)β
β βββββββ βββββββ βββββββ βββββββ
β
β PROBLEMS:
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β’ User logs in on Server 1 β Session saved on Server 1 β
β β β’ Next request goes to Server 2 β No session β User logged outβ
β β β’ User uploads avatar to Server 3 β Image on Server 3 β
β β β’ Next request goes to Server 1 β Avatar not found β 404 β
β β β’ Server 2 dies β All sessions on Server 2 are GONE forever β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
THE STATEFUL SINS (What NOT to do)
- Sessions in files (default Laravel) β Each server has its own sessions
- Uploaded files in
storage/app/public β Files live on one server only
- Logs in
storage/logs β Cannot centralize debugging
- Cache in files (default Laravel) β Each server has its own cache
- Environment variables mixed with code β Config changes require redeploy
- Database sessions on master β Better than files, but still not stateless
β
The Solution: Stateless Architecture
β
β STATELESS ARCHITECTURE (SCALES HORIZONTALLY)
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β Load Balancer
β β
β ββββββ΄βββββ¬ββββββββββ¬ββββββββββ
β βΌ βΌ βΌ βΌ
β βββββββ βββββββ βββββββ βββββββ
β βServerβ βServerβ βServerβ βServerβ
β β 1 β β 2 β β 3 β β 4 β
β β β β β β β β β
β β β β β β β β β
β β (No β β (No β β (No β β (No β
β βlocal β βlocal β βlocal β βlocal β
β βstate)β βstate)β βstate)β βstate)β
β βββββ¬βββ βββββ¬βββ βββββ¬βββ βββββ¬βββ
β β β β β
β ββββββββββββΌβββββββββββΌβββββββββββ
β β β
β βββββββββ΄βββββββ β
β β Redis β β
β β (Sessions β β
β β & Cache) β β
β ββββββββββββββββ β
β β
β ββββββββββββββββββββ΄βββββββββββββββββββ
β β S3 / Object Storage β
β β (Uploaded Files) β
β βββββββββββββββββββββββββββββββββββββββ
β
β BENEFITS:
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β’ Any server can handle any request β
β β β’ Add/remove servers anytime (auto-scaling) β
β β β’ Server dies? No problem β other servers take over β
β β β’ Zero-downtime deployments (drain one server at a time) β
β β β’ Global scale (Redis and S3 are shared across regions) β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
THE GOLDEN RULE OF STATELESSNESS
Every server must be able to die at any moment without affecting users. All state must live in external services (Redis, S3, Database).
π The Statelessness Checklist
| Component |
Stateful (BAD) |
Stateless (GOOD) |
Why |
| Sessions |
File driver (default) |
Redis driver |
Sessions must be shared across all servers |
| Cache |
File driver (default) |
Redis or Memcached |
Cache must be consistent across servers |
| Uploaded Files |
storage/app/public |
S3, R2, or GCS |
Files must be accessible from any server |
| Logs |
storage/logs (local) |
CloudWatch, ELK, Papertrail |
Centralized debugging across servers |
| Configuration |
Hardcoded or .env only |
Environment variables + config:cache |
Config changes without redeploy |
| Queue Workers |
Local php artisan queue:work |
Redis + Supervisor on dedicated workers |
Workers can run anywhere, scale independently |
| Scheduled Tasks |
Cron on one server |
php artisan schedule:run on all servers + mutex |
Prevents duplicate task execution |
βοΈ Making Laravel Stateless (Step by Step)
1. Move Sessions to Redis
# .env
SESSION_DRIVER=redis
SESSION_CONNECTION=default
# config/session.php (already configured for Redis)
2. Move Cache to Redis
# .env
CACHE_DRIVER=redis
# config/cache.php
'default' => env('CACHE_DRIVER', 'redis'),
3. Move Files to S3
# .env
FILESYSTEM_DRIVER=s3
AWS_ACCESS_KEY_ID=your-key
AWS_SECRET_ACCESS_KEY=your-secret
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=your-bucket
# In your code
$path = Storage::disk('s3')->put('avatars', $request->file('avatar'));
$url = Storage::disk('s3')->url($path);
4. Move Logs to Centralized Service
# config/logging.php
'channels' => [
'cloudwatch' => [
'driver' => 'custom',
'via' => \App\Logging\CloudWatchLogger::class,
],
],
# .env
LOG_CHANNEL=cloudwatch
5. Ensure Config is Cached
# After every deployment
php artisan config:cache
# Never use env() outside config files
// Wrong: env('APP_NAME')
// Right: config('app.name')
6. Handle Scheduled Tasks with Mutex
// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
// With onOneServer(), tasks run only once across all servers
$schedule->command('reports:generate')
->daily()
->onOneServer(); // β Uses Redis/Cache for mutex
$schedule->command('emails:send')
->hourly()
->onOneServer();
}
// For Laravel 8+, you need to set:
// config/schedule.php
'schedule_cache_driver' => 'redis',
π Horizontal Scaling: Adding More Servers
β
β AUTO-SCALING WITH STATELESS SERVERS
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β Traffic increases (Black Friday)
β β
β βΌ
β Load Balancer detects high CPU
β β
β βΌ
β Auto-scaling group: Add 5 more servers
β β
β βΌ
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β New servers boot, join the pool β
β β β’ No data to sync (stateless!) β
β β β’ Sessions already in Redis β
β β β’ Files already in S3 β
β β β’ Ready in 30 seconds β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β βΌ
β Traffic handled. Users unaffected.
β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β ZERO-DOWNTIME DEPLOYMENT
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β Step 1: Add new servers with new code (in parallel)
β Step 2: Wait for them to be ready
β Step 3: Drain connections from old servers (no new requests)
β Step 4: Remove old servers from load balancer
β Step 5: Terminate old servers
β
β Result: Users never see a "down for maintenance" page.
β
REAL-WORLD EXAMPLE
Laravel Forge + AWS Auto-scaling Group + Load Balancer:
# Forge recipe for stateless servers
- 1 GB swap (no session files)
- Nginx + PHP-FPM
- .env with Redis/S3 credentials
- Deployment script:
git pull
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
sudo systemctl reload php8.2-fpm
β οΈ The Downsides of Statelessness
COST
- Redis β Additional server/service cost (but worth it)
- S3 β Storage cost + API request costs
- Logging services β CloudWatch, Datadog, etc. have costs
- Multiple servers β More servers = more cost
COMPLEXITY
- Debugging is harder β A request may go to any server
- Cache invalidation is distributed β Redis helps, but still complex
- Scheduled tasks need mutex β
onOneServer() must be used carefully
- Local development differs from production β Need Redis/S3 locally too
BUT THE BENEFITS OUTWEIGH THE COSTS
For any application that expects growth, the ability to scale horizontally is worth the added complexity. Start stateless from day one. It's much harder to retrofit later.
π How to Check If Your App Is Stateless
THE ULTIMATE TEST
Kill a random server during peak traffic. If users notice, you have state somewhere.
Automated statelessness audit:
#!/bin/bash
# audit-statelessness.sh
echo "Checking for stateful patterns..."
# Check session driver
grep "SESSION_DRIVER=file" .env && echo "β Sessions in files! Use Redis."
# Check cache driver
grep "CACHE_DRIVER=file" .env && echo "β Cache in files! Use Redis."
# Check filesystem
grep "FILESYSTEM_DRIVER=local" .env && echo "β Files on local disk! Use S3."
# Check for env() in app code (outside config)
grep -r "env(" --include="*.php" --exclude-dir=vendor \
--exclude-dir=config | grep -v "config/" && \
echo "β env() found outside config/! Use config()."
# Check for hardcoded paths
grep -r "storage/app/public" --include="*.php" --exclude-dir=vendor && \
echo "β Hardcoded storage path! Use Storage facade."
echo "Audit complete."
π΄ THE RULE: Run the audit before every deployment. Fix stateful violations immediately. Statelessness is not optional for production.
π Topic 12 Summary: Statelessness Doctrine
| Component |
Stateful (BAD) |
Stateless (GOOD) |
| Sessions |
File database |
Redis |
| Cache |
File database |
Redis Memcached |
| Files |
Local disk (storage/) |
S3 R2 GCS |
| Logs |
Local disk |
CloudWatch ELK Papertrail |
| Config |
.env + env() calls |
config:cache + config() |
| Scheduled Tasks |
Cron on single server |
onOneServer() + Redis mutex |
π THE RULE: Your server must be disposable. If you can't terminate it at any moment without user impact, you are not stateless. Fix sessions, files, logs, and cache first. Then scale horizontally with confidence.
Statelessness is the foundation of cloud-native applications. Start stateless from day one.
NEXT TOPIC PREVIEW
Topic 13: Event-Driven Architecture (EDA) β Stop making users wait. Dispatch events, return 202 Accepted, process in background. How to go from 500ms response time to 20ms.