Back to blog

Performance

Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing

End-to-end performance playbook: queue tuning with Horizon, debug profiling with Telescope, and k6 load testing strategies.

  • Laravel
  • Performance
  • Horizon
  • Telescope
  • Load Testing

Reader map

Key points in Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing

Syntax first, runtime behavior second, migration cleanup last.

Read
16 min
Waypoints
7
Track
Performance
  1. 01
    Start here

    Define p95, p99, error-rate, and queue-wait budgets.

  2. 02
    Waypoint

    Move slow work out of requests before scaling web workers.

  3. 03
    Waypoint

    Isolate queues by business priority.

  4. 04
    Waypoint

    Run Horizon from a real process manager and keep its config in version control.

  5. 05
    Waypoint

    Use Telescope as a short-lived profiler, not as production observability.

  6. 06
    Waypoint

    Load test staging with k6 thresholds that can fail the build.

  7. 07
    Migration check

    Tune one bottleneck at a time and rerun the same test.

SEO Metadata

SEO Title Options

  1. Optimizing Laravel for High Traffic: Horizon, Telescope
  2. Laravel Performance: Practical 2026 Guide
  3. Performance Playbook: Laravel Performance

Meta Description Options

  1. Learn Laravel Performance with a practical Performance framework, expert mistakes, implementation steps, examples, FAQ, and schema-ready guidance.
  2. End-to-end performance playbook: queue tuning with Horizon, debug profiling with Telescope, and k6 load testing strategies.

URL Slug

optimizing-laravel-high-traffic-horizon-telescope-load-testing

Focus Keyword

Laravel Performance

Additional LSI Keywords

  • Performance
  • Laravel
  • Horizon
  • Telescope
  • Load Testing
  • Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing
  • production checklist
  • implementation guide
  • best practices
  • architecture decisions
  • testing strategy
  • performance impact

Table of Contents

Article overview

Laravel Performance is the kind of topic that looks simple until it reaches production. Teams usually discover the real cost late: unclear boundaries, weak defaults, hidden maintenance work, and decisions that seemed harmless when the codebase was small.

The problem gets worse when the article, tutorial, or implementation guide only explains the happy path. This guide closes that gap with a practical framework, a comparison table, common mistakes, and a deep technical section you can use while planning real work.

Keep reading for the non-obvious part: the safest implementation is rarely the most impressive-looking one. It is the one your team can debug, test, document, and evolve without turning every future change into archaeology.

Key Takeaways

  • Laravel Performance should be evaluated as a production decision, not only as a syntax or tooling choice.
  • The best implementation keeps responsibilities visible, with clear ownership, tests, documentation, and rollback paths.
  • Search visibility improves when practical depth, structured answers, and expert examples live on the same page.

[IMAGE: A mobile-first technical article layout showing the main concept, decision table, implementation checklist, and FAQ blocks. Alt: Laravel Performance expert guide for Performance]

What Laravel Performance means

Laravel Performance means applying performance knowledge to a concrete engineering decision, then turning that decision into reliable code, documentation, and operational behavior. In practice, it combines the topic's core concepts with trade-off analysis, implementation boundaries, testing strategy, and maintenance discipline.

This is the definition worth optimizing for featured snippets because it avoids hype. It tells the reader what the topic does and what a professional implementation must include.

Why it matters now

The technical web is more crowded than it was a few years ago. Thin tutorials can still get indexed, but they rarely earn trust from senior developers, buyers, AI answer systems, or teams that need production guidance.

For performance topics, the strongest content now has three layers:

  • a clear answer for fast scanning
  • a practical framework for implementation
  • expert context that explains what breaks later

That same structure helps search engines understand the page. It also helps readers decide whether the advice fits their project.

Implementation framework

Use this framework before adopting the approach described in this article.

  1. Define the user problem and the production risk.
  2. Identify the smallest reliable implementation boundary.
  3. Keep configuration, secrets, and environment-specific behavior outside the article's core logic.
  4. Add tests for the behavior that would hurt if it regressed.
  5. Document the trade-off, not only the final code.
  6. Measure the result with logs, metrics, or user-facing outcomes.
  7. Revisit the decision after real usage exposes edge cases.

The sequence is deliberately conservative. It keeps the work grounded in outcomes instead of novelty.

[IMAGE: A seven-step implementation framework with discovery, boundary design, configuration, tests, documentation, measurement, and iteration. Alt: Laravel Performance implementation framework]

Practical comparison

Decision areaStrong approachWeak approachWhy it matters
ScopeSolve one clear problemMix unrelated concernsFocus improves testing and search intent
ArchitecturePut logic in explicit classes or documented boundariesHide behavior in templates or incidental callbacksFuture changes stay easier to review
Data flowPass prepared data into the view or endpointQuery or compute in presentation codeReduces regressions and performance surprises
TestingCover the risky behavior directlyTest only the happy pathCatches production failures earlier
DocumentationExplain trade-offs and limitsRepeat generic definitionsBuilds E-E-A-T and reader trust
OperationsTrack logs, metrics, and rollback stepsShip without measurementMakes the decision reversible

This table is intentionally practical. It gives a reviewer something to check before the implementation becomes expensive to change.

Expert workflow

Expert tip: "Treat Laravel Performance as a system boundary. If the next developer cannot find where the decision lives, how it is tested, and when it should be avoided, the implementation is not finished."

A useful workflow is simple:

  • Start with the smallest working example.
  • Add the constraints that exist in your real project.
  • Remove anything that only demonstrates cleverness.
  • Write down the failure modes.
  • Add links to related decisions so future readers can navigate the topic cluster.

That last point matters for both humans and search systems. A single article can answer a question; a cluster proves authority.

Common mistakes

Mistake 1: Copying a pattern without its context

A pattern that works in a small demo can fail in a real application. The missing context is usually data volume, team experience, deployment process, security requirements, or observability.

Before copying the pattern, ask what assumption made it safe in the original example.

Mistake 2: Putting business logic in the wrong layer

This is the fastest way to make future debugging expensive. In Laravel, PHP, and server-rendered websites, presentation should receive prepared data, not discover rules on its own.

Keep decision logic in models, actions, services, policies, requests, jobs, or documented helpers where it can be tested directly.

Mistake 3: Optimizing for novelty instead of maintainability

Newer tools and language features can be valuable. They can also hide simple behavior behind unfamiliar syntax.

Use the option that makes the next production incident easier to understand.

Mistake 4: Publishing without a measurement plan

If the article describes a performance, SEO, security, or architecture improvement, define how success will be checked. Logs, tests, crawl diagnostics, analytics, and user behavior are all stronger than assumptions.

[IMAGE: A common-mistakes board with context loss, wrong layer, novelty bias, and missing measurement highlighted. Alt: Laravel Performance common mistakes]

Image placeholders

  • [IMAGE: A concept diagram for Laravel Performance with input, decision boundary, implementation, tests, and production feedback. Alt: Laravel Performance concept diagram]
  • [IMAGE: A mobile screenshot-style checklist for Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing. Alt: Laravel Performance mobile checklist]
  • [IMAGE: A comparison table visualization for strong versus weak implementation choices. Alt: Laravel Performance comparison table]

Video placeholder

[VIDEO: Insert a 5-8 minute YouTube walkthrough that demonstrates the main decision, the implementation boundary, the test strategy, and the production caveats for Laravel Performance.]

Internal linking opportunities

Original Technical Deep Dive

The short version

High traffic Laravel work is not one optimization. It is a contract between HTTP latency, queue latency, database pressure, cache behavior, worker capacity, and deploy discipline.

Use this order:

  • Define p95, p99, error-rate, and queue-wait budgets.
  • Move slow work out of requests before scaling web workers.
  • Isolate queues by business priority.
  • Run Horizon from a real process manager and keep its config in version control.
  • Use Telescope as a short-lived profiler, not as production observability.
  • Load test staging with k6 thresholds that can fail the build.
  • Tune one bottleneck at a time and rerun the same test.

The common mistake is buying more CPU while the application is spending request time on mail, webhooks, exports, N+1 queries, or jobs waiting behind the wrong queue.

Start with budgets

Pick numbers before you touch config.

For a typical Laravel application, a useful first budget is:

SignalStarting target
HTTP p95Under 500 ms for cached browse pages and simple API reads
HTTP p99Under 1200 ms for normal traffic
HTTP error rateUnder 1 percent during load tests
Critical queue waitUnder 10 seconds
Default queue waitUnder 60 seconds
Heavy queue waitExplicitly documented per job type
Slow SQL queryAnything above 100 ms requires inspection
Failed jobsAlert immediately for repeated failures

These numbers are not universal. They are a starting line. A checkout endpoint and an admin export do not need the same budget. The point is to make performance visible enough that a failed deploy is not judged by vibes.

Remove request work first

Before adding Horizon supervisors, decide what should not run inside a web request.

Move these to queues:

  • Email delivery.
  • Webhook delivery.
  • PDF, CSV, and image generation.
  • Search indexing.
  • Notification fan-out.
  • Slow third-party API synchronization.
  • Post-payment fulfillment that can safely retry.

Do not queue work that must complete before the user can safely continue. Payment authorization, permission checks, stock reservation, and request validation still belong in the request path unless the product flow is explicitly asynchronous.

Use Redis queues for Horizon

Horizon is built for Laravel Redis queues. Start by making Redis the queue connection:

QUEUE_CONNECTION=redis

Then keep queue retry timing explicit:

// config/queue.php
'redis' => [
    'driver' => 'redis',
    'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
    'queue' => env('REDIS_QUEUE', 'default'),
    'retry_after' => 120,
    'block_for' => null,
    'after_commit' => false,
],

[IMAGE: Supporting visual 1 for Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing, showing Laravel Performance decisions, examples, and Laravel, Performance, Horizon. Alt: Laravel Performance optimizing-laravel-high-traffic-horizon-telescope-load-testing visual 1]

[IMAGE: Supporting visual 1 for Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing, showing Laravel Performance decisions, examples, and Laravel, Performance, Horizon. Alt: Laravel Performance optimizing-laravel-high-traffic-horizon-telescope-load-testing visual 1]

The important relationship is:

  • Job timeout must be shorter than retry_after.
  • Horizon supervisor timeout must be shorter than retry_after.
  • Long jobs need a queue with a larger retry_after.

If a timeout is longer than retry_after, the same job can be released and processed again while the first worker is still running it.

Split queues by priority

Do not put everything on default.

A useful production layout:

QueuePurposeRule
criticalPayment fulfillment, account security, user-visible confirmationsSmall jobs, high process ceiling, short wait threshold
defaultNormal application jobsBalanced, steady worker pool
emailsMail and notificationsRetryable, moderate priority
webhooksThird-party deliveryRate-limited and retryable
exportsCSV, PDF, reports, importsLow concurrency, long timeout

Dispatch jobs onto explicit queues:

use App\Jobs\SendReceipt;
use App\Jobs\SyncCrmContact;

SendReceipt::dispatch($order)->onQueue('critical');
SyncCrmContact::dispatch($customer)->onQueue('webhooks');

For jobs that always belong on one queue, set it on the job:

<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Report;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

final class BuildReportExport implements ShouldQueue
{
    use Queueable;

    public string $queue = 'exports';

    public int $tries = 1;

    public int $timeout = 900;

    public function __construct(
        public readonly Report $report,
    ) {}

    public function handle(): void
    {
        // Generate and store the export.
    }
}

Keep exports away from critical. A five-minute report should not delay a password reset email or a post-payment confirmation.

Make jobs retryable on purpose

Retries are capacity planning. A bad retry policy can create a second traffic spike after the first one.

For transient failures, use bounded tries and backoff:

<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Services\BillingGateway;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Throwable;

final class CaptureAuthorizedPayment implements ShouldQueue
{
    use Queueable;

    public string $queue = 'critical';

    public int $tries = 4;

    public int $timeout = 30;

    /**
     * @return array<int, int>
     */
    public function backoff(): array
    {
        return [5, 30, 120];
    }

    public function __construct(
        public readonly string $paymentId,
    ) {}

    public function handle(BillingGateway $billing): void
    {
        $billing->capture($this->paymentId);
    }

    public function failed(Throwable $exception): void
    {
        report($exception);
    }
}

For jobs that call rate-limited providers, add queue middleware instead of hammering the provider:

use Illuminate\Queue\Middleware\RateLimited;

/**
 * @return array<int, object>
 */
public function middleware(): array
{
    return [
        (new RateLimited('crm-sync'))->releaseAfter(60),
    ];
}

Rate-limited releases still count as attempts. Give these jobs enough tries or use retryUntil() so they do not fail only because they waited correctly.

Install Horizon

Install and publish Horizon:

composer require laravel/horizon
php artisan horizon:install

Then commit config/horizon.php. Treat it like infrastructure code. Worker counts, queues, timeouts, and wait thresholds should be reviewed like any other production change.

Restrict the dashboard in non-local environments:

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Laravel\Horizon\HorizonApplicationServiceProvider;

final class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
    protected function gate(): void
    {
        Gate::define('viewHorizon', function (User $user): bool {
            return $user->is_admin && str_ends_with($user->email, '@example.com');
        });
    }
}

Dashboard access is operational access. Do not leave it open because the route is hard to guess.

Configure Horizon for priority isolation

This is a practical config/horizon.php production shape:

'waits' => [
    'redis:critical' => 10,
    'redis:default' => 60,
    'redis:emails' => 90,
    'redis:webhooks' => 120,
    'redis:exports' => 600,
],

'trim' => [
    'recent' => 60,
    'pending' => 60,
    'completed' => 60,
    'recent_failed' => 10080,
    'failed' => 10080,
    'monitored' => 10080,
],

'silenced' => [
    App\Jobs\RecordHeartbeat::class,
],

'environments' => [
    'production' => [
        'supervisor-critical' => [
            'connection' => 'redis',
            'queue' => ['critical'],
            'balance' => 'auto',
            'autoScalingStrategy' => 'time',
            'minProcesses' => 2,
            'maxProcesses' => 10,
            'balanceMaxShift' => 1,
            'balanceCooldown' => 3,
            'tries' => 3,
            'timeout' => 45,
            'nice' => 0,
        ],

        'supervisor-default' => [
            'connection' => 'redis',
            'queue' => ['default', 'emails', 'webhooks'],
            'balance' => 'auto',
            'autoScalingStrategy' => 'time',
            'minProcesses' => 2,
            'maxProcesses' => 20,
            'balanceMaxShift' => 1,
            'balanceCooldown' => 3,
            'tries' => 3,
            'timeout' => 90,
            'nice' => 0,
        ],

        'supervisor-heavy' => [
            'connection' => 'redis',
            'queue' => ['exports'],
            'balance' => false,
            'maxProcesses' => 2,
            'tries' => 1,
            'timeout' => 900,
            'nice' => 10,
        ],
    ],

    'local' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue' => ['default'],
            'balance' => 'auto',
            'maxProcesses' => 3,
        ],
    ],
],

Why this works:

  • critical has its own process pool.
  • exports cannot consume all workers.
  • autoScalingStrategy => 'time' lets Horizon scale based on queue wait time.
  • balanceMaxShift and balanceCooldown keep auto-scaling from swinging too aggressively.
  • waits defines which queue latency is worth alerting on.
  • trim keeps Horizon history useful without growing forever.
  • nice lowers CPU priority for heavy jobs on Linux hosts that honor process niceness.

[IMAGE: Supporting visual 2 for Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing, showing Laravel Performance decisions, examples, and Laravel, Performance, Horizon. Alt: Laravel Performance optimizing-laravel-high-traffic-horizon-telescope-load-testing visual 2]

Do not copy worker counts blindly. Start small, run k6, watch queue wait time, Redis, database load, and CPU, then increase limits where the system can actually absorb more work.

[IMAGE: Supporting visual 2 for Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing, showing Laravel Performance decisions, examples, and Laravel, Performance, Horizon. Alt: Laravel Performance optimizing-laravel-high-traffic-horizon-telescope-load-testing visual 2]

Run Horizon under a process manager

In production, Horizon should be supervised by systemd, Supervisor, your container orchestrator, or your platform process model.

A simple Supervisor program:

[program:laravel-horizon]
process_name=%(program_name)s
command=php /var/www/app/artisan horizon
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/supervisor/laravel-horizon.log
stopwaitsecs=3600

Use the real deploy path and user for your server.

During deploys, terminate Horizon gracefully after the new release is ready:

php artisan horizon:terminate

The process manager should restart Horizon with the fresh code. Without this step, old workers can keep running old classes, old config, and old container state.

Record Horizon metrics

Schedule Horizon snapshots:

<?php

use Illuminate\Support\Facades\Schedule;

Schedule::command('horizon:snapshot')->everyFiveMinutes();

Then watch:

  • Jobs per minute.
  • Queue wait time.
  • Runtime per job class.
  • Failed jobs.
  • Long waits by queue.
  • Throughput before and after deploys.

Queue wait time is usually more actionable than total pending jobs. Ten thousand low-priority report rows may be acceptable. Twenty critical jobs waiting behind exports is not.

Use Telescope for diagnosis, not permanent observability

Telescope is excellent when you need to inspect requests, queries, jobs, cache, mail, notifications, exceptions, dumps, logs, and scheduled tasks. It is not a replacement for metrics, logs, traces, and alerting.

For local and staging:

composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate

Keep it disabled unless you are actively using it:

TELESCOPE_ENABLED=false
TELESCOPE_QUERY_WATCHER=true
TELESCOPE_REQUEST_WATCHER=true
TELESCOPE_JOB_WATCHER=true
TELESCOPE_CACHE_WATCHER=false

If you intentionally ship Telescope to a controlled production environment, use a strict gate, short retention, and a filter. Do not record every request on a busy site.

<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Support\Facades\Gate;
use Laravel\Telescope\IncomingEntry;
use Laravel\Telescope\Telescope;
use Laravel\Telescope\TelescopeApplicationServiceProvider;

final class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
{
    public function register(): void
    {
        $this->hideSensitiveRequestDetails();

        Telescope::filter(function (IncomingEntry $entry): bool {
            if ($this->app->environment('local')) {
                return true;
            }

            return $entry->isReportableException()
                || $entry->isFailedRequest()
                || $entry->isFailedJob()
                || $entry->isScheduledTask()
                || $entry->hasMonitoredTag();
        });
    }

    protected function gate(): void
    {
        Gate::define('viewTelescope', function ($user): bool {
            return $user !== null
                && $user->is_admin
                && str_ends_with($user->email, '@example.com');
        });
    }
}

Prune Telescope data:

use Illuminate\Support\Facades\Schedule;

Schedule::command('telescope:prune --hours=48')->daily();

On high-traffic systems, retention is a performance setting. Recording too much diagnostic data can become the bottleneck you are trying to find.

Tune Telescope watchers

Start with queries and requests:

use Laravel\Telescope\Watchers;

'watchers' => [
    Watchers\RequestWatcher::class => [
        'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
        'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),
    ],

    Watchers\QueryWatcher::class => [
        'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
        'slow' => env('TELESCOPE_SLOW_QUERY_MS', 100),
    ],

    Watchers\JobWatcher::class => [
        'enabled' => env('TELESCOPE_JOB_WATCHER', true),
    ],

    Watchers\CacheWatcher::class => [
        'enabled' => env('TELESCOPE_CACHE_WATCHER', false),
    ],

    Watchers\DumpWatcher::class => [
        'enabled' => env('TELESCOPE_DUMP_WATCHER', false),
    ],
],

For performance investigations, the Query Watcher usually gives the fastest return:

  • Repeated SQL means N+1 loading.
  • Slow SQL means missing indexes, bad joins, or too much data.
  • Many fast queries still hurt p95 when they happen in every request.
  • Request entries connect SQL, logs, exceptions, and jobs to one user action.

Disable noisy watchers unless they answer the current question.

[IMAGE: Supporting visual 3 for Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing, showing Laravel Performance decisions, examples, and Laravel, Performance, Horizon. Alt: Laravel Performance optimizing-laravel-high-traffic-horizon-telescope-load-testing visual 3]

Add lightweight database budgets

Telescope is interactive. Add runtime budgets for signals you want to alert on:

<?php

declare(strict_types=1);

namespace App\Providers;

use Illuminate\Database\Connection;
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;

final class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        DB::whenQueryingForLongerThan(
            500,
            function (Connection $connection, QueryExecuted $event): void {
                report(new SlowDatabaseRequest(
                    connection: $connection->getName(),
                    sql: $event->sql,
                    timeMs: $event->time,
                ));
            },
        );
    }
}

Use a real exception or notification class in your application. The point is to make slow database time visible outside Telescope.

Also prevent lazy loading outside production:

use Illuminate\Database\Eloquent\Model;

Model::preventLazyLoading(! app()->isProduction());

This catches N+1 regressions before they reach a load test.

Build a k6 smoke test first

[IMAGE: Supporting visual 3 for Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing, showing Laravel Performance decisions, examples, and Laravel, Performance, Horizon. Alt: Laravel Performance optimizing-laravel-high-traffic-horizon-telescope-load-testing visual 3]

Before load testing, write a smoke test that proves the endpoint works.

import { check } from 'k6';
import http from 'k6/http';

export const options = {
  vus: 1,
  duration: '30s',
  thresholds: {
    http_req_failed: ['rate<0.01'],
    checks: ['rate>0.99'],
  },
};

export default function () {
  const baseUrl = __ENV.BASE_URL;
  const response = http.get(`${baseUrl}/products`);

  check(response, {
    'status is 200': (r) => r.status === 200,
    'body is not empty': (r) => r.body.length > 1000,
  });
}

Run it against staging:

BASE_URL=https://staging.example.com k6 run tests/load/smoke.js

If the smoke test fails, a bigger load test is noise.

Use arrival-rate tests for traffic targets

For capacity planning, test the rate you expect the site to handle. ramping-arrival-rate starts iterations at a configured rate, independent of how slowly the system responds.

import { check, sleep } from 'k6';
import http from 'k6/http';

export const options = {
  scenarios: {
    browse_products: {
      executor: 'ramping-arrival-rate',
      startRate: 10,
      timeUnit: '1s',
      preAllocatedVUs: 50,
      maxVUs: 300,
      stages: [
        { target: 25, duration: '2m' },
        { target: 50, duration: '5m' },
        { target: 75, duration: '2m' },
        { target: 0, duration: '1m' },
      ],
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<500', 'p(99)<1200'],
    checks: ['rate>0.99'],
  },
};

export default function () {
  const baseUrl = __ENV.BASE_URL;

  const response = http.get(`${baseUrl}/products`, {
    tags: { page: 'products_index' },
  });

  check(response, {
    'status is 200': (r) => r.status === 200,
    'contains product data': (r) => r.body.includes('Products'),
  });

  sleep(1);
}

Run:

BASE_URL=https://staging.example.com k6 run tests/load/products.js

If p95 fails while CPU is low, look at database, Redis, external HTTP, locks, and queue waits. If CPU is pinned, tune worker counts, OPcache, hot code paths, or add capacity.

Load test authenticated APIs

Use setup() for login or token creation:

import { check } from 'k6';
import http from 'k6/http';

const headers = {
  'Content-Type': 'application/json',
  Accept: 'application/json',
};

export const options = {
  scenarios: {
    account_api: {
      executor: 'ramping-arrival-rate',
      startRate: 5,
      timeUnit: '1s',
      preAllocatedVUs: 25,
      maxVUs: 150,
      stages: [
        { target: 20, duration: '2m' },
        { target: 20, duration: '5m' },
        { target: 0, duration: '1m' },
      ],
    },
  },
  thresholds: {
    http_req_failed: ['rate<0.01'],
    http_req_duration: ['p(95)<400'],
    checks: ['rate>0.99'],
  },
};

export function setup() {
  const response = http.post(
    `${__ENV.BASE_URL}/api/login`,
    JSON.stringify({
      email: __ENV.LOAD_TEST_EMAIL,
      password: __ENV.LOAD_TEST_PASSWORD,
    }),
    { headers },
  );

  check(response, {
    'login succeeded': (r) => r.status === 200,
    'token exists': (r) => Boolean(r.json('token')),
  });

  return {
    token: response.json('token'),
  };
}

export default function (data) {
  const response = http.get(`${__ENV.BASE_URL}/api/account`, {
    headers: {
      Accept: 'application/json',
      Authorization: `Bearer ${data.token}`,
    },
  });

  check(response, {
    'account status is 200': (r) => r.status === 200,
  });
}

Use dedicated test users and test data. Do not point this at production customer accounts.

Correlate k6 with Horizon and Telescope

Run each load test with a written observation sheet:

During the testWhat to watch
k6p95, p99, failed requests, failed checks
Horizonqueue wait, job runtime, failed jobs, process scaling
Telescopeslow queries, repeated queries, failed requests, failed jobs
RedisCPU, memory, evictions, connection count
DatabaseCPU, locks, slow query log, connection count
Web tierPHP-FPM or Octane worker saturation, memory, restarts

Interpret common patterns:

SymptomLikely causeNext move
HTTP p95 rises, queue wait is flatRequest path is slowInspect Telescope requests and queries
Queue wait rises, HTTP stays stableWorkers cannot drain jobsAdd worker capacity or split heavy jobs
Queue wait rises and database CPU spikesJobs overload the databaseReduce job concurrency or batch writes
Failed jobs spike after rate limitsRetry policy is too aggressiveAdd RateLimited, backoff, or provider-specific queues
k6 failures start at the same arrival rate every runCapacity ceiling foundTune the bottleneck and rerun the same scenario
k6 passes but real traffic failsTest data or workflow is unrealisticAdd authenticated paths, writes, cache misses, and background jobs

[IMAGE: Supporting visual 4 for Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing, showing Laravel Performance decisions, examples, and Laravel, Performance, Horizon. Alt: Laravel Performance optimizing-laravel-high-traffic-horizon-telescope-load-testing visual 4]

One load test should answer one question. "Can this endpoint handle 50 requests per second for five minutes under production-like data?" is useful. "How fast is the app?" is not.

Deployment checklist

Before a high-traffic release:

  • QUEUE_CONNECTION=redis is set.
  • Horizon runs under a process manager.
  • php artisan horizon:status returns active.
  • php artisan horizon:snapshot is scheduled every five minutes.
  • Dashboard gates protect /horizon and /telescope.
  • Telescope is disabled by default outside local/staging.
  • Telescope prune is scheduled if Telescope is installed.
  • Queue names match Horizon supervisors.
  • Job $timeout values are shorter than queue retry_after.
  • Long-running jobs are isolated from critical jobs.
  • Failed job alerts route to the team that can fix them.
  • k6 smoke tests pass.
  • k6 load tests have thresholds, not just output summaries.
  • Deploy runs php artisan horizon:terminate after new code is ready.

[IMAGE: Supporting visual 4 for Optimizing Laravel for High Traffic: Horizon, Telescope & Load Testing, showing Laravel Performance decisions, examples, and Laravel, Performance, Horizon. Alt: Laravel Performance optimizing-laravel-high-traffic-horizon-telescope-load-testing visual 4]

The practical loop

Use this loop until the budget passes:

  1. Run the smoke test.
  2. Run the load test.
  3. Read k6 p95, p99, failures, and checks.
  4. Read Horizon queue wait and job runtime.
  5. Inspect Telescope entries for the slowest request or failed job.
  6. Fix the highest-confidence bottleneck.
  7. Rerun the same test before changing anything else.

Do not tune Horizon, SQL, cache, PHP-FPM, and frontend assets in the same pass. You will not know which change mattered.

FAQ

What is Laravel Performance?

Laravel Performance is a practical performance topic that should be evaluated through implementation scope, production risk, testing, documentation, and long-term maintainability.

When should a team use Laravel Performance?

Use Laravel Performance when it solves a real project constraint, improves clarity, or reduces operational risk. Avoid it when it only adds novelty or hides behavior from future maintainers.

What is the biggest risk with Laravel Performance?

The biggest risk is copying a pattern without its context. Production systems need clear boundaries, rollback options, tests, and observability before a technique becomes dependable.

How do you test Laravel Performance?

Test the smallest unit that owns the behavior, then add integration coverage for the path users or systems actually rely on. Include failure cases, configuration differences, and regression checks.

How does Laravel Performance affect SEO and AI search visibility?

It improves visibility when the article gives a direct answer, expert context, structured headings, internal links, trustworthy references, and FAQ content that matches the visible page.

Conclusion

Laravel Performance is worth doing when the implementation improves clarity, reliability, or delivery speed. It is not worth doing when it hides ownership, increases operational risk, or makes the system harder to explain.

Use the framework above as a review checklist. Then connect this topic to the rest of the project documentation so readers can move from concept to implementation without losing context.

Top