Skip to content
Mehedi Hasan Sagor

// blog

Feature Flags, Zero‑Downtime Deploys, and Observability: Advanced Laravel Production Practices

Mehedi Hasan Sagor Nov 10, 2026 5 min read
Feature Flags, Zero‑Downtime Deploys, and Observability: Advanced Laravel Production Practices

Introduction

Running a Laravel application in production is more than writing clean code and pushing it to a server. As a startup scales, the cost of a single outage or a slow release grows dramatically. This post walks through four concrete practices that go beyond the typical checklist: zero‑downtime deployments, feature flags, structured observability, and automated health checks. Each section includes a short code snippet or configuration file you can copy into an existing project.


1. Zero‑Downtime Deploys with Laravel Octane and Blue‑Green Releases

Traditional PHP deployments replace the entire codebase and restart the web server, causing a brief period where incoming requests hit a cold cache or a missing file. Two tools together eliminate that window:

  • Laravel Octane – runs your app on Swoole or RoadRunner, keeping the application in memory between requests.
  • Blue‑Green deployments – keep two identical environments (blue and green) behind a load balancer and switch traffic only after the new version passes health checks.

Step‑by‑step

  1. Add Octane

    composer require laravel/octane
    php artisan octane:install --server=swoole
    

    This creates an octane.php config where you can tune workers, max‑request limits, and graceful shutdown time.

  2. Configure your CI/CD to build a Docker image that runs php artisan octane:start --host=0.0.0.0 --port=8000.

  3. Set up blue‑green in your cloud provider (AWS Elastic Beanstalk, Azure App Service, or GCP Cloud Run). The pipeline should:

    • Deploy the new image to the green environment.
    • Run a health‑check script (see section 4).
    • If healthy, switch the load balancer target group to green.
    • Keep the blue version running for rollback.

Why it matters: Requests never hit a cold PHP-FPM process, and the switch is atomic at the load balancer level, giving you true zero‑downtime releases.


2. Feature Flags for Safer Releases

Feature flags let you ship code to production but keep new functionality hidden until you’re ready. Laravel has a lightweight package called spatie/laravel-feature-flags that stores flags in the database and caches them.

Installation

composer require spatie/laravel-feature-flags
php artisan vendor:publish --provider="Spatie\Feature\FeatureServiceProvider" --tag="migrations"
php artisan migrate

Defining a Flag

use Spatie\Feature\FeatureFacade as Feature;

Feature::define('new‑checkout', false); // default off

Using a Flag in Code

if (Feature::isEnabled('new-checkout')) {
    return view('checkout.new');
}
return view('checkout.legacy');

Runtime Toggle

You can expose a tiny admin UI or use Laravel Nova to toggle flags, or call an Artisan command:

php artisan feature:enable new-checkout
php artisan feature:disable new-checkout

Best practice: Keep flags short‑lived. After a successful rollout, remove the flag and the dead code. Long‑standing flags become technical debt.


3. Structured Logging & Metrics

When an issue surfaces, raw log lines are hard to search. Structured logging (JSON) combined with a metrics collector gives you instant insight.

Logging with Monolog JSON Formatter

Add the formatter in config/logging.php:

'channels' => [
    'stack' => [
        'driver' => 'stack',
        'channels' => ['single', 'json'],
    ],
    'json' => [
        'driver' => 'single',
        'path' => storage_path('logs/laravel-json.log'),
        'level' => env('LOG_LEVEL', 'debug'),
        'formatter' => Monolog\Formatter\JsonFormatter::class,
    ],
],

Now every Log::info('order.created', ['order_id' => $order->id]); produces a JSON line that can be ingested by Loki, Elastic, or CloudWatch.

Exporting Metrics with Prometheus

Install the client library:

composer require superbalist/laravel-prometheus-exporter
php artisan vendor:publish --provider="Superbalist\PrometheusExporter\PrometheusExporterServiceProvider"

Define a counter for request duration:

use Prometheus\CollectorRegistry;
use Prometheus\Histogram;

Route::middleware('web')->group(function () {
    Route::get('/metrics', function (CollectorRegistry $registry) {
        return response($registry->getMetricFamilySamples())->header('Content-Type', Prometheus\RenderTextFormat::MIME_TYPE);
    });
});

Instrument the kernel middleware to observe request latency:

public function handle($request, Closure $next)
{
    $start = microtime(true);
    $response = $next($request);
    $duration = microtime(true) - $start;
    app('prometheus')->getOrRegisterHistogram('http_request_duration_seconds', 'Request duration', ['method', 'uri', 'status'])->observe($duration, [$request->method(), $request->path(), $response->getStatusCode()]);
    return $response;
}

Prometheus scrapes /metrics, giving you per‑endpoint latency charts.


4. Automated Health Checks & Circuit Breakers

A load balancer can only know if a container is up, not if the app is healthy. Implement a /health endpoint that verifies database connectivity, cache, and a critical external service.

Health Endpoint

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

Route::get('/health', function () {
    $checks = [];
    // DB
    try {
        DB::connection()->getPdo();
        $checks['database'] = 'ok';
    } catch (\Exception $e) {
        $checks['database'] = 'error';
    }
    // Cache
    try {
        Cache::store()->ping();
        $checks['cache'] = 'ok';
    } catch (\Exception $e) {
        $checks['cache'] = 'error';
    }
    // External API (example payment gateway)
    try {
        $res = Http::timeout(1)->get('https://api.stripe.com/v1/health');
        $checks['stripe'] = $res->successful() ? 'ok' : 'error';
    } catch (\Exception $e) {
        $checks['stripe'] = 'error';
    }
    $status = in_array('error', $checks) ? 503 : 200;
    return response()->json($checks, $status);
});

Configure your cloud load balancer to hit /health every 10 seconds and mark the instance unhealthy on a 5xx response.

Circuit Breaker with Laravel Resilience

Use spatie/laravel-failure (or a simple wrapper) to prevent cascading failures when an external API is down.

use Spatie\Failure\Failure;

function chargeCustomer(array $payload): Failure|Response
{
    return Failure::run('stripe-charge', fn() => Http::post('https://api.stripe.com/v1/charges', $payload));
}

When the circuit opens, Failure::run returns a predefined fallback, keeping your app responsive while the downstream service recovers.


5. Takeaway Checklist

  • Deploy with Octane + blue‑green to eliminate downtime.
  • Guard new code behind short‑lived feature flags; remove them after rollout.
  • Log in JSON; ship logs to a centralized system.
  • Expose Prometheus metrics for latency, error rates, and queue sizes.
  • Implement a /health endpoint that checks DB, cache, and critical third‑party services.
  • Wrap external calls in a circuit‑breaker pattern.

Applying these practices shifts the focus from “will it work?” to “how quickly can we detect and recover when it doesn’t.” For a SaaS product, that speed translates directly into user trust and revenue continuity.

  • Laravel
  • Devops
  • Performance
  • Monitoring
  • Feature Flags