Skip to content
Mehedi Hasan Sagor

// blog

Laravel Production Best Practices: From Code to Cloud

Mehedi Hasan Sagor Oct 29, 2026 6 min read
Laravel Production Best Practices: From Code to Cloud

1. Keep the Core Thin – Service Layer & Repositories

Business logic should live outside controllers. When you put everything in a controller you quickly hit two problems: the controller becomes a monolith that is hard to test, and the same logic is duplicated across routes or jobs.

// Bad – all logic in the controller
public function store(Request $request)
{
    $data = $request->validate([
        'email' => 'required|email|unique:users',
        'name'  => 'required|string',
    ]);

    $user = User::create($data);
    Mail::to($user)->send(new WelcomeMail($user));
    event(new UserRegistered($user));
    return redirect()->route('users.show', $user);
}

Instead, extract the responsibility to a service class and, if you need data‑access abstraction, a repository. The controller now only orchestrates HTTP concerns.

// app/Services/UserRegistrationService.php
class UserRegistrationService
{
    public function __construct(protected UserRepository $users, protected Mailer $mailer) {}

    public function register(array $payload): User
    {
        $user = $this->users->create($payload);
        $this->mailer->to($user)->send(new WelcomeMail($user));
        event(new UserRegistered($user));
        return $user;
    }
}

// app/Repositories/UserRepository.php
class UserRepository
{
    public function create(array $data): User
    {
        return User::create($data);
    }
}

// Controller
public function store(Request $request, UserRegistrationService $service)
{
    $data = $request->validate([
        'email' => 'required|email|unique:users',
        'name'  => 'required|string',
    ]);
    $user = $service->register($data);
    return redirect()->route('users.show', $user);
}

Benefits:

  • Testability – services can be unit‑tested without HTTP.
  • Reusability – the same service can be called from a job, an API endpoint, or a console command.
  • Clear separation – controllers stay thin, focusing on request/response handling.

2. Validation and Error Handling as First‑Class Citizens

Laravel’s FormRequest objects already give you a tidy place for validation, but production‑grade apps need a consistent error format, especially when serving APIs.

// app/Http/Requests/RegisterUserRequest.php
class RegisterUserRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'email' => 'required|email|unique:users',
            'name'  => 'required|string|max:255',
        ];
    }

    public function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(response()->json([
            'status' => 'error',
            'errors' => $validator->errors(),
        ], 422));
    }
}

For unexpected exceptions, register a global handler that logs the context and returns a generic JSON payload. This prevents leaking stack traces while still giving you enough data to debug.

// app/Exceptions/Handler.php
public function render($request, Throwable $e)
{
    if ($request->expectsJson()) {
        Log::error('Unhandled exception', ['exception' => $e]);
        return response()->json([
            'status' => 'error',
            'message' => 'Something went wrong. Our team has been notified.',
        ], 500);
    }
    return parent::render($request, $e);
}

Consistent error shapes simplify front‑end error handling and reduce the need for ad‑hoc parsing.

3. Asynchronous Work: Queues, Jobs, and Rate Limiting

Synchronous execution is the simplest path, but it hurts latency and resilience. Identify any task that:

  • Sends email or SMS
  • Calls an external API
  • Performs heavy image/video processing
  • Generates PDFs or reports

Move each to a queued job. Laravel’s queue system already supports retries, back‑off, and dead‑letter handling.

class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public $tries = 3;          // Max attempts
    public $backoff = [10, 30]; // Seconds before each retry

    public function __construct(public User $user) {}

    public function handle(Mailer $mailer)
    {
        $mailer->to($this->user)->send(new WelcomeMail($this->user));
    }
}

When you have rate‑limited third‑party APIs, guard the job with Laravel’s RateLimiter facade.

public function handle()
{
    RateLimiter::attempt(
        'external-api',
        limit: 100,
        seconds: 60,
        callback: function () {
            // call the API
        }
    );
}

The result is a system that can survive spikes without blocking the user experience.

4. Caching Strategically, Not Everywhere

Cache is a performance lever, but over‑caching leads to stale data and complex invalidation logic. Follow a two‑step rule:

  1. Cache only what is expensive to compute – e.g., aggregated statistics, heavy DB queries, or third‑party look‑ups.
  2. Invalidate explicitly – never rely on time‑based expiration alone for critical data.

Example: caching a product catalog query.

class ProductRepository
{
    public function all(): Collection
    {
        return Cache::remember('catalog:all', now()->addMinutes(30), function () {
            return Product::with('category')->orderBy('name')->get();
        });
    }

    public function refreshCache(): void
    {
        Cache::forget('catalog:all');
    }
}

Whenever a product is created, updated, or deleted, call refreshCache() from the model observer. This keeps the cache coherent without guessing expiration windows.

5. Observability: Logging, Metrics, and Health Checks

A production app is invisible until something breaks. Instrumentation should be baked in from day one.

Logging

  • Use structured logs (JSON) so that log aggregators can query fields.
  • Include a request ID ($request->header('X-Request-ID')) in every log line. Laravel’s Log::withContext() makes this trivial.
Log::withContext(['request_id' => $request->header('X-Request-ID')]);
Log::info('User registered', ['user_id' => $user->id]);

Metrics

  • Export key counters (e.g., jobs.dispatched, http.requests) to Prometheus via the spatie/laravel-metrics package or a custom middleware.
  • Track latency per route with a simple middleware that records start/end timestamps.

Health Checks

  • Expose /health that returns 200 only when the database, cache, and queue connections are healthy.
  • Use Laravel’s built‑in php artisan schedule:work to verify the scheduler is alive.
Route::get('/health', function () {
    try {
        DB::connection()->getPdo();
        Cache::store()->get('health-check');
        return response()->json(['status' => 'ok']);
    } catch (Throwable $e) {
        return response()->json(['status' => 'error'], 503);
    }
});

Observability reduces mean time to detection (MTTD) and mean time to recovery (MTTR).

6. Deploy with Confidence – Zero‑Downtime and Rollbacks

Even the best‑written code can fail in the wild. A robust deployment pipeline mitigates risk.

Zero‑Downtime Deployments

  • Use Laravel Octane (Swoole or RoadRunner) for long‑running workers; it supports graceful reloads.
  • When using a traditional PHP‑FPM stack, configure your load balancer to drain old containers before terminating them.
# Example GitHub Actions step for a rolling deploy on ECS
- name: Deploy rolling update
  run: |
    aws ecs update-service \
      --cluster my-cluster \
      --service my-service \
      --force-new-deployment

Database Migrations

  • Write non‑blocking migrations. Add new columns with nullable first, back‑fill data in a background job, then make the column non‑nullable in a later migration.
  • Keep migrations idempotent; if a migration fails, you should be able to roll back safely.

Rollbacks

  • Tag each release in Git. Store the previous image tag in the deployment manifest. If health checks fail after a deploy, trigger an automated rollback to the last known good version.
  • Keep a database backup before running migrations (e.g., pg_dump for PostgreSQL) and automate verification of backup integrity.

Takeaway

Production‑ready Laravel isn’t a checklist; it’s a mindset that treats code, infrastructure, and operations as a single system. By:

  • isolating business logic in services/repositories,
  • standardising validation and error responses,
  • off‑loading work to queues with proper rate limiting,
  • caching only what truly needs it and invalidating explicitly,
  • instrumenting logs, metrics, and health endpoints, and
  • deploying with zero‑downtime strategies and reliable rollbacks,

you give founders and technical leads the confidence that their app will scale, stay secure, and recover quickly when the unexpected happens.

Implement one of these pillars this sprint, measure the impact, and iterate. The cumulative effect is a Laravel product that feels as solid in production as it does in development.

  • Laravel
  • Best Practices
  • Php
  • Devops