• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » Scaling Laravel on DigitalOcean to Handle 50,000+ Concurrent Requests

Scaling Laravel on DigitalOcean to Handle 50,000+ Concurrent Requests

Architectural Foundation: Beyond Single-Server Laravel

Achieving 50,000+ concurrent requests for a Laravel application on DigitalOcean necessitates a shift from a monolithic, single-server deployment to a distributed, horizontally scalable architecture. This involves decoupling components, leveraging managed services, and implementing robust caching strategies. We’ll focus on a common, cost-effective setup using DigitalOcean’s Droplets, Load Balancers, Managed Databases, and Object Storage.

Database Scaling: PostgreSQL on DigitalOcean Managed Databases

A single MySQL or PostgreSQL instance will quickly become a bottleneck. For high concurrency, we’ll utilize DigitalOcean’s Managed PostgreSQL. This provides managed replication, automated backups, and failover, allowing us to scale read operations independently.

Configuration Strategy:

  • Primary Instance: A robust instance (e.g., 8 vCPU, 32GB RAM) for write operations.
  • Read Replicas: Multiple smaller instances (e.g., 4 vCPU, 16GB RAM each) to handle read queries. The number of replicas depends on the read/write ratio of your application.
  • Connection Pooling: Implement connection pooling at the application level or via a proxy like PgBouncer. For Laravel, this is often managed within the application’s database configuration.

Laravel Database Configuration (config/database.php):

We’ll define multiple read connections and instruct Laravel to use them for read queries.

<?php

return [
    // ... other configurations

    'connections' => [
        'pgsql' => [
            'driver' => 'pgsql',
            'url' => env('DATABASE_URL'),
            'host' => env('DB_HOST', '127.0.0.1'),
            'port' => env('DB_PORT', '5432'),
            'database' => env('DB_DATABASE', 'forge'),
            'username' => env('DB_USERNAME', 'forge'),
            'password' => env('DB_PASSWORD', ''),
            'charset' => 'utf8mb4',
            'prefix' => '',
            'schema' => 'public',
            'sslmode' => 'prefer',
        ],

        'pgsql_read_1' => [
            'driver' => 'pgsql',
            'url' => env('DATABASE_READ_1_URL'),
            'host' => env('DB_READ_1_HOST', '127.0.0.1'),
            'port' => env('DB_READ_1_PORT', '5432'),
            'database' => env('DB_READ_1_DATABASE', 'forge'),
            'username' => env('DB_READ_1_USERNAME', 'forge'),
            'password' => env('DB_READ_1_PASSWORD', ''),
            'charset' => 'utf8mb4',
            'prefix' => '',
            'schema' => 'public',
            'sslmode' => 'prefer',
        ],

        // Add more read replicas as needed: pgsql_read_2, pgsql_read_3, etc.
        'pgsql_read_2' => [
            'driver' => 'pgsql',
            'url' => env('DATABASE_READ_2_URL'),
            'host' => env('DB_READ_2_HOST', '127.0.0.1'),
            'port' => env('DB_READ_2_PORT', '5432'),
            'database' => env('DB_READ_2_DATABASE', 'forge'),
            'username' => env('DB_READ_2_USERNAME', 'forge'),
            'password' => env('DB_READ_2_PASSWORD', ''),
            'charset' => 'utf8mb4',
            'prefix' => '',
            'schema' => 'public',
            'sslmode' => 'prefer',
        ],
    ],

    'redis' => [
        // ... Redis configuration
    ],
];

Application-level Read/Write Splitting:

Modify your database service provider or use a trait/helper to direct read queries to replicas. A common approach is to use the `read()` method on the database manager.

<?php

namespace App\Services;

use Illuminate\Support\Facades\DB;

class DatabaseRouter
{
    /**
     * Execute a Closure within a read-only database connection.
     *
     * @param  callable  $callback
     * @return mixed
     */
    public static function read(callable $callback)
    {
        // Dynamically select a read replica. A more sophisticated approach
        // might involve a round-robin or load-balancing strategy.
        $readConnection = 'pgsql_read_' . rand(1, 2); // Assuming pgsql_read_1 and pgsql_read_2

        return DB::connection($readConnection)->transaction(function () use ($callback, $readConnection) {
            // Ensure no write operations are attempted on read replicas.
            // This is a basic safeguard; more robust solutions might involve
            // setting transaction isolation levels or using specific database features.
            DB::connection($readConnection)->statement('SET TRANSACTION READ ONLY');

            $result = $callback();

            return $result;
        });
    }

    /**
     * Execute a Closure within the primary database connection.
     *
     * @param  callable  $callback
     * @return mixed
     */
    public static function write(callable $callback)
    {
        return DB::connection('pgsql')->transaction(function () use ($callback) {
            $result = $callback();
            return $result;
        });
    }
}

Usage in Eloquent Models or Repositories:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use App\Services\DatabaseRouter;

class User extends Model
{
    // ... model definition

    public static function findActiveUsers()
    {
        return DatabaseRouter::read(function () {
            return self::where('status', 'active')->get();
        });
    }

    public function saveUser(array $data)
    {
        return DatabaseRouter::write(function () use ($data) {
            $user = new self();
            $user->fill($data);
            $user->save();
            return $user;
        });
    }
}

Application Server Scaling: Horizontal Scaling with Load Balancers

We’ll deploy multiple Laravel application servers (Droplets) behind a DigitalOcean Load Balancer. This distributes incoming traffic and provides high availability.

Droplet Configuration:

  • Web Servers: Multiple Droplets (e.g., 4 vCPU, 8GB RAM each) running Nginx and PHP-FPM.
  • Statelessness: Ensure your application servers are stateless. Session data, file uploads, and other stateful information must be externalized.
  • Deployment: Use a robust deployment strategy (e.g., CI/CD pipeline with Capistrano, Deployer, or custom scripts) to deploy code to all application servers simultaneously.

Nginx Configuration (Example for one app server):

# /etc/nginx/sites-available/your-app.conf

server {
    listen 80;
    server_name your-domain.com;
    root /var/www/your-app/public;

    index index.php index.html index.htm;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        # Adjust socket path based on your PHP-FPM configuration
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    # Deny access to hidden files
    location ~ /\.ht {
        deny all;
    }

    # Caching headers for static assets (adjust as needed)
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp)$ {
        expires 1y;
        add_header Cache-Control "public";
    }
}

PHP-FPM Configuration (Example – pool.d/www.conf):

; /etc/php/8.1/fpm/pool.d/www.conf

[www]
user = www-data
group = www-data
listen = /var/run/php/php8.1-fpm.sock ; Or a TCP socket if preferred for distributed setups
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 100       ; Adjust based on Droplet RAM and expected load
pm.start_servers = 5
pm.min_spare_servers = 10
pm.max_spare_servers = 20
pm.process_idle_timeout = 10s
pm.max_requests = 500       ; Restart workers after X requests to prevent memory leaks

; Other settings to consider:
; request_terminate_timeout = 60s ; For long-running tasks
; rlimit_files = 1024
; rlimit_nofile = 1024

DigitalOcean Load Balancer Setup:

  • Create a Load Balancer in your DigitalOcean control panel.
  • Frontend: Configure HTTP (port 80) and HTTPS (port 443). Use Let’s Encrypt for SSL certificates.
  • Backend Pools: Add your Nginx application Droplets to a backend pool.
  • Health Checks: Configure health checks to point to a lightweight endpoint on your Laravel app (e.g., `/health`). This endpoint should return a 200 OK status if the application is healthy.
  • Sticky Sessions: Generally, avoid sticky sessions for stateless Laravel apps. If absolutely necessary for specific workflows, configure it on the Load Balancer.

Health Check Endpoint (routes/web.php):

use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Response;

Route::get('/health', function () {
    // Basic check: ensure database connection is available
    try {
        DB::connection()->getPdo();
        return Response::make('OK', 200);
    } catch (\Exception $e) {
        return Response::make('Database Error', 503);
    }
});

Caching Strategies: Redis and Object Caching

Aggressive caching is paramount. We’ll leverage Redis for session storage, queueing, and application-level caching.

DigitalOcean Managed Redis:

  • Provision a Managed Redis cluster. Start with a moderate size and scale as needed.
  • Configure your Laravel application to use this Redis instance for sessions, caching, and queues.

Laravel Configuration (config/session.php):

// config/session.php
'driver' => env('SESSION_DRIVER', 'redis'),
'store' => env('SESSION_STORE', 'default'), // 'default' refers to the Redis store defined in config/database.php

Laravel Configuration (config/cache.php):

// config/cache.php
'default' => env('CACHE_DRIVER', 'redis'),
'stores' => [
    // ... other stores
    'redis' => [
        'driver' => 'redis',
        'connection' => 'cache', // Ensure this matches your Redis connection name in config/database.php
    ],
    // ...
],

Application-Level Caching:

Identify frequently accessed, rarely changing data. Cache query results, configuration values, and computed data.

// Example: Caching a list of active categories
$categories = Cache::remember('active_categories', now()->addMinutes(60), function () {
    return Category::where('is_active', true)->get();
});

// Example: Caching configuration settings loaded from the database
$settings = Cache::remember('app_settings', now()->addHours(24), function () {
    return AppSetting::pluck('value', 'key');
});

Asynchronous Processing: Queues

Offload time-consuming tasks (email sending, image processing, report generation) to background queues. This frees up web workers to handle incoming requests.

Configuration:

  • Use Redis as your queue driver for performance.
  • Run multiple queue worker processes on dedicated Droplets or on your application servers (if resources permit).
  • Monitor queue lengths and worker health.

Laravel Configuration (config/queue.php):

// config/queue.php
'default' => env('QUEUE_CONNECTION', 'redis'),

'connections' => [
    // ...
    'redis' => [
        'driver' => 'redis',
        'connection' => 'default', // Ensure this matches your Redis connection name in config/database.php
        'queue' => env('QUEUE_QUEUE', 'default'),
        'retry_after' => 90,
        'block_for' => 5,
    ],
    // ...
],

Running Queue Workers:

# On each server designated for queue workers
php artisan queue:work --queue=default,high --tries=3 --timeout=120 --memory=256

Process Management (using Supervisor):

; /etc/supervisor/conf.d/laravel-queue.conf

[program:laravel-queue]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/your-app/artisan queue:work --queue=default,high --tries=3 --timeout=120 --memory=256
directory=/var/www/your-app
autostart=true
autorestart=true
user=www-data
numprocs=4 ; Adjust based on Droplet CPU cores and memory
redirect_stderr=true
stdout_logfile=/var/log/supervisor/laravel-queue.log
stderr_logfile=/var/log/supervisor/laravel-queue.err.log

Object Storage: DigitalOcean Spaces

Store user-uploaded files (images, documents) in DigitalOcean Spaces (S3-compatible object storage) instead of the local filesystem. This is crucial for stateless application servers.

Configuration:

  • Create a DigitalOcean Space.
  • Generate API credentials (Key and Secret).
  • Configure Laravel’s Filesystem.

Laravel Configuration (config/filesystems.php):

// config/filesystems.php
'disks' => [
    // ...
    'spaces' => [
        'driver' => 's3',
        'key' => env('SPACES_KEY'),
        'secret' => env('SPACES_SECRET'),
        'endpoint' => env('SPACES_ENDPOINT'), // e.g., 'https://nyc3.digitaloceanspaces.com'
        'region' => env('SPACES_REGION'),   // e.g., 'nyc3'
        'bucket' => env('SPACES_BUCKET'),
        'use_path_style_endpoint' => env('SPACES_USE_PATH_STYLE_ENDPOINT', false),
    ],
    // ...
],

Usage:

// Upload a file
$path = Storage::disk('spaces')->putFile('avatars', $request->file('avatar'));

// Get a file URL
$url = Storage::disk('spaces')->url($path);

Monitoring and Optimization

Continuous monitoring is essential for identifying bottlenecks and optimizing performance.

  • DigitalOcean Monitoring: Utilize Droplet and Managed Service metrics.
  • Application Performance Monitoring (APM): Tools like New Relic, Datadog, or Sentry provide deep insights into application performance, database queries, and errors.
  • Log Aggregation: Centralize logs from all Droplets using tools like ELK stack, Graylog, or LogDNA.
  • Load Testing: Regularly perform load tests (e.g., using k6, JMeter, or Locust) to simulate high traffic and identify breaking points before they impact users.
  • Database Query Optimization: Analyze slow queries using `EXPLAIN` and ensure proper indexing.
  • Code Profiling: Use tools like Xdebug or Blackfire.io to profile your PHP code and identify performance hotspots.

Security Considerations

With a distributed system, security becomes more complex. Ensure:

  • Firewalls: Configure UFW on Droplets and DigitalOcean Cloud Firewalls to restrict access to necessary ports only.
  • SSL/TLS: Enforce HTTPS for all traffic.
  • Secrets Management: Use environment variables securely and consider tools like HashiCorp Vault for more sensitive secrets.
  • Regular Updates: Keep your OS, Nginx, PHP, and Laravel dependencies up-to-date.
  • Rate Limiting: Implement rate limiting at the Load Balancer or application level to prevent abuse.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Step-by-Step: Diagnosing thread pools deadlock during concurrent ActiveRecord transaction processing on Linode Servers
  • Securing Your E-commerce APIs: Preventing SQL Injection (SQLi) in customized checkout queries in WooCommerce Implementations
  • Disaster Recovery 101: Architecting Auto-Failovers for MySQL and Ruby Deployments on Linode
  • High-Throughput Caching Strategies: Scaling MySQL for Perl Application APIs
  • Disaster Recovery 101: Architecting Auto-Failovers for DynamoDB and Laravel Deployments on DigitalOcean

Copyright © 2026 · Vinay Vengala