How to debug and optimize Laravel apps in production

Última actualización: 04/20/2026
    Use Git or built artifacts for code, migrations for schema and a clean .env to move Laravel safely from development to production.,Fix N+1 queries, index hot columns and cache routes, config, views and query results to remove the most common Laravel bottlenecks.,Tune PHP, OPcache, web server and hosting tier (shared vs VPS vs dedicated) to match your traffic profile and workload.,Rely on Debugbar for local view debugging and Telescope for API/background monitoring, always locked down in production.

Laravel app debugging in production

Deploying and debugging a Laravel application in production is one of those moments where development theory crashes into real-world infrastructure: servers without Composer, shared hostings with cPanel, VPS with Redis and queues, slow queries, 500 errors that only appear under load… and users waiting on the other side of the screen. If you don’t plan this carefully, performance and reliability will suffer fast.

This guide walks through the full lifecycle: from deploying Laravel on shared hosting or VPS, to diagnosing bottlenecks, to squeezing every millisecond out of your app with caching, database tuning and server-level optimizations. We’ll also see when to use tools like Debugbar and Telescope, how to move from development to production safely, and what to watch continuously so your application stays fast and stable as it grows.

From development box to production server: what’s the right way?

It’s perfectly valid (and often recommended) to keep Composer only in your development environment and deploy pre-built code to production. If your dev virtual server has Composer, Laravel, Apache2 and MariaDB installed, and your production VM only runs the stack (PHP + web server + database) without Composer, you’re not doing anything wrong – you just need a clean deployment workflow.

A common and robust approach is to separate code deployment from database deployment: use Git (or another VCS) to move code, and a controlled process (migrations and, when needed, dumps) to move or sync the database. Relying on raw mysqldump for everything can work, but it’s not always the safest or easiest option when the app is already live.

A sane baseline workflow usually looks like this:

  • Code: push to a remote repository and pull/clone on the production machine, or upload a prepared archive (ZIP/TAR) with vendor already built.
  • Dependencies: either install with Composer on prod (if allowed) or ship vendor from your local build machine.
  • Database structure: rely on Laravel migrations to keep schema in sync between dev and prod instead of constantly importing full dumps.
  • Database data: only move seed/demo data when really needed; for a live project, structure should be migrated, but real data will be created by users in production.

On shared hosting with cPanel the process is similar, but you normally package the entire Laravel project locally and upload it via the File Manager or FTP/SFTP. You’ll then adjust paths, configure .env, create the production database through cPanel, and, when you have terminal access, run the Artisan commands directly on the server.

Deploying Laravel on shared hosting (cPanel) safely

Deploy Laravel on shared hosting

On low-cost shared hosting, you usually get cPanel, PHP, MySQL and not much else. That means no full SSH in many cases, no Composer globally, and a strict public web root like public_html. You can still deploy Laravel cleanly – you just have to respect the hosting’s folder layout and harden permissions.

Prepare your project locally before uploading anything:

  • Build frontend assets with NPM (Webpack, Vite, Laravel Mix) so that production JS and CSS are ready: minified, bundled and versioned.
  • Optimize Composer autoload with composer dump-autoload -o and, if you’re building exclusively for production, use composer install --no-dev to avoid dev-only packages on the live server.
  • Clean caches and logs so you don’t drag development garbage into production: clear Laravel caches and remove everything inside storage/logs so that fresh logs are generated in the new environment.

When packaging the project into a ZIP/TAR keep in mind what should not be shipped:

  • Don’t include .git or any VCS-related metadata if you’re not going to use Git directly on the server.
  • Clean old log files from storage/logs to avoid noise and save disk quota.
  • Ensure hidden files like .env and .htaccess are indeed part of the archive; some archivers skip dotfiles by default.

On the hosting side the first step is to upload and extract the archive:

  • Use cPanel’s File Manager to upload the compressed file to your user’s home.
  • Extract it in a folder such as /home/your-user/your-laravel-project.
  • Keep public_html separate; it will act as the web root, while the rest of Laravel will live outside of it for security.

Next, lock down file permissions. Many developers work locally with wide-open permissions (like 777) during development, but those are a huge vulnerability on shared environments. Only the minimal writable directories – usually storage and bootstrap/cache – should be writable by the web server user, and nothing in your codebase should be world-writable.

To finish the split between public and private code, move the contents of Laravel’s public directory into public_html:

  • Everything inside your-laravel-project/public (index.php, assets, etc.) goes into /home/your-user/public_html.
  • The rest of the Laravel app remains in /home/your-user/your-laravel-project, outside public reach.

After that move, public/index.php (now living in public_html) no longer finds vendor/autoload.php and bootstrap/app.php in the expected relative paths. Update the two require lines accordingly, pointing them back to the real project root:

  • Change the autoload include from something like __DIR__.'/../vendor/autoload.php' to a path that steps into your project folder, e.g. __DIR__.'/../your-laravel-project/vendor/autoload.php'.
  • Do the same for the app bootstrap: __DIR__.'/../your-laravel-project/bootstrap/app.php'.

Each time you upload a new project version, avoid blindly overwriting index.php without reapplying these changes. If you do replace it, double-check the require paths before going live.

Configuring database, environment and mail in production

A production-ready Laravel application stands on a correctly wired .env file and a properly created database user. Misconfigured database credentials, wrong APP_URL or a forgotten APP_DEBUG=true are among the most common sources of production headaches.

Start by creating the database in your hosting panel:

  • In cPanel, go to MySQL Databases and create a new database for the project.
  • Create a dedicated DB user with a strong password.
  • Assign that user to the database with all necessary privileges (SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, etc.).

Then adapt your production .env file. If it wasn’t included in the archive, create it in the project root and copy all values from your local version, then adjust:

  • APP_ENV: set it to production so Laravel applies production assumptions for caching, logging and error handling.
  • APP_DEBUG: set it to false to avoid leaking sensitive information in exception pages.
  • APP_URL: set it to your real domain, e.g. https://your-domain.com, so URL generation and links behave correctly.
  • APP_KEY: generate a fresh key locally with php artisan key:generate, then copy the value into the production .env. This key protects encrypted data and sessions, so don’t reuse stale keys carelessly.

Database connection variables are the next critical piece:

  • DB_DATABASE must match the name of the database you created in hosting.
  • DB_USERNAME should be the dedicated DB user, not your panel login.
  • DB_PASSWORD must be the generated password for that MySQL user.

If your app sends email, production MAIL_* settings also need to be updated. Replace any local or sandbox values (like Mailtrap) with the SMTP data provided by your host or third‑party email service:

  • MAIL_HOST, MAIL_PORT and MAIL_MAILER should reflect the real SMTP server.
  • MAIL_USERNAME and MAIL_PASSWORD usually correspond to a real mailbox (e.g., noreply@your-domain.com).
  • MAIL_FROM_ADDRESS and MAIL_FROM_NAME define the sender identity visible to end users.

Once the .env file is in place, it’s a good idea to refresh Laravel’s configuration cache so the framework doesn’t read dozens of config files on every request. With terminal access you’d typically run php artisan config:cache to rebuild bootstrap/cache/config.php using your production values.

For the database schema itself, you have two main strategies depending on whether this is a first deploy or a migration from an existing system:

  • On a brand‑new production instance, running php artisan migrate in the project root will create all tables empty and ready for live data.
  • If you need to replicate an existing local schema without rows, you can reset migrations locally, export the clean DB as SQL via phpMyAdmin, and import that dump into the hosting database.

Understanding and fixing Laravel performance bottlenecks

Laravel puts developer experience first, which means that the default configuration is tailored for flexibility, not raw speed. Out-of-the-box, the framework loads a lot of services, resolves many things dynamically through the container and logs in detail – all of which help in development, but can drag down production if left unchecked.

Modern users expect sub‑second, often sub‑200 ms responses for common pages. If your application consistently takes longer, engagement, conversion and even SEO can start to suffer. When you deploy to production, performance tuning isn’t an optional “nice to have”; it’s part of finishing the job.

Some of the most frequent performance issues in Laravel apps include:

  • Unoptimized database queries, especially classic N+1 problems.
  • Excessive data loading via Eloquent models instead of selecting only needed columns.
  • Lack of caching for routes, config, queries and views.
  • Non‑optimized PHP setup (no OPcache, slow PHP handler, wrong PHP version).
  • Running heavy jobs during the request instead of pushing them to queues.

To pinpoint what is actually slowing your app down you need both application‑level and infrastructure‑level visibility. At the Laravel level, tools like Debugbar and Telescope provide insight into queries, timings and errors. At the hosting level, dashboards that show CPU, RAM, disk I/O and bandwidth help correlate slow responses with overloaded servers or poor I/O performance.

When you combine both perspectives, you can distinguish between “bad code” and “insufficient or misconfigured infrastructure”. That’s essential if you want to avoid throwing hardware at a problem that should have been fixed in a single Eloquent relationship or index.

Database query optimization with Eloquent

The database layer is often the first and biggest source of performance pain in a real Laravel project. Eloquent is comfortable and expressive, but it’s easy to accidentally write queries that explode under load or with large datasets.

The classic culprit is the N+1 query problem. It happens when your code loads a list of records, and then, for each item in that list, fires an additional query to load related data. Twenty blog posts with author details can suddenly mean 21 queries instead of 2 (one for the posts, one for all authors).

This bug typically shows up in code like this:

  • You fetch posts: $posts = Post::all();
  • In a Blade view, you iterate and call $post->author for each item.
  • Each ->author access triggers another SQL SELECT behind the scenes.

The solution is eager loading via with(). Instead of letting Eloquent lazily hit the DB each time, you explicitly instruct it to load relations in one shot: $posts = Post::with('author')->get();. Debug tools like Laravel Debugbar make N+1 issues obvious by showing a suspiciously high query count on pages that should only need a few.

Once N+1 is under control, the next step is to reduce how much data you fetch and process:

  • Use select() to retrieve only the columns you actually need instead of full table rows.
  • Apply limit() or pagination on listing pages to avoid dragging thousands of rows into memory when only a handful are visible.
  • For complex aggregations or heavy joins, consider dropping down to the Query Builder or even raw SQL where appropriate.

Database indexing is another powerful yet often neglected optimization. Laravel migrations don’t automatically index every foreign key, and unindexed columns used in filters or joins can cause slow full table scans. By explicitly adding single‑column or composite indexes on frequently queried fields, you can drastically reduce query times, especially for read-heavy workloads.

On VPS or dedicated hosting where you control MySQL or MariaDB settings, you can go further by tuning the query cache, InnoDB buffer pool and slow query logs. Once the application code has been optimized, those DB‑level adjustments help the engine scale under sustained load and large datasets.

Using caching effectively in Laravel

Caching is one of the highest‑impact, lowest‑effort techniques you can use to improve Laravel performance. By storing frequently used data, configuration or route definitions in fast storage, you avoid repeating expensive operations on every request.

Laravel ships with a flexible cache abstraction that supports multiple drivers:

  • Redis – an in‑memory data store ideal for sessions, cache and real‑time data, widely used in production.
  • Memcached – another in‑memory cache layer with a simpler data model; good when you want speed without Redis’s additional capabilities.
  • File cache – stores cache entries on disk; slower than memory, but often the only option on cheaper shared hosting.

Drivers are configured in config/cache.php, and you can change the default without rewriting your code. You might start on a shared host using file cache, then migrate transparently to Redis when you move to VPS – the application code that calls the cache API doesn’t need to know what’s underneath.

Several core caching strategies are practically mandatory for production Laravel apps:

  • Route cache: php artisan route:cache compiles routes into a single file. This is especially important for apps with many routes.
  • Config cache: php artisan config:cache merges all config files, minimizing filesystem hits during requests.
  • View cache: php artisan view:cache precompiles Blade templates to plain PHP so runtime parsing is avoided.
  • Query result caching: using Cache::remember() or similar helpers to store DB query results that don’t change on every request.

When Redis or Memcached are available, these strategies become even more powerful because cached data is served from memory. On NVMe SSD‑backed hosting, even file‑based cache sees a boost, as read/write latency to disk is greatly reduced compared to traditional HDDs.

For more advanced setups, combining application cache with a CDN for static assets (CSS, JS, images) offloads a lot of work from your origin server. Edge caching of static resources reduces latency for users across different regions and helps your Laravel app focus on dynamic responses.

Server-level tuning: PHP, web server and hosting tiers

No matter how polished your Laravel code is, a poorly configured server can sabotage performance and stability. PHP settings, the chosen handler (like PHP-FPM), OPcache and the underlying hardware (SSD vs NVMe vs HDD) all directly influence response times and concurrency.

First, choose a suitable PHP version that is officially supported by your Laravel version and still maintained upstream. Newer PHP 8.x releases generally deliver sizable performance gains over old 7.x builds, so upgrading PHP alone often cuts request time dramatically, even before you touch application code.

Next, ensure that required PHP extensions (mbstring, openssl, PDO, etc.) are enabled and that OPcache is active. OPcache stores precompiled PHP bytecode in shared memory, preventing PHP from reloading and recompiling scripts on every request. On VPS and dedicated plans you can fine‑tune OPcache’s memory size and validation policies to fit your app’s footprint.

Using PHP-FPM with properly sized process pools can also improve concurrency and isolation. Instead of using older, slower handlers, PHP-FPM manages worker processes more efficiently and lets you tune pool settings (max children, idle timeouts) according to your traffic profile.

On the web server side, configure URL rewriting so Laravel’s router receives clean URLs. In Apache, that means ensuring mod_rewrite is enabled and the rules from public/.htaccess are honored. Enabling HTTP/2 and TLS (HTTPS) at the panel level also brings both performance and security benefits.

Choosing the right hosting tier is just as important as tuning the software stack:

  • Shared hosting works for small, low‑traffic Laravel apps, but you share CPU and memory with other customers. Expect limits and inconsistent performance under spikes.
  • VPS gives you root access and dedicated resources, enabling custom PHP-FPM pools, Redis, queue workers, DB tuning and better overall control.
  • Dedicated servers are best for mission‑critical or high‑traffic apps that require predictable performance, custom stacks and advanced scaling patterns.

Across all these tiers, NVMe SSD storage is a major advantage because it accelerates everything involving disk I/O: file‑based caching, session storage, log writes, even database access. That extra headroom is especially useful once your app begins to scale.

Artisan and Composer optimizations for production

Before flipping the switch on a production release, you should always run a specific set of Artisan and Composer commands to prepare the codebase for speed. Skipping these steps is one of the easiest ways to leave performance on the table.

Core Artisan commands to include in your deployment pipeline are:

  • php artisan optimize: runs a bundle of optimizations in one go for many common scenarios.
  • php artisan config:cache: compiles config files into a single file.
  • php artisan route:cache: speeds up route registration (avoid if you still use closures in routes; refactor them to controllers first).
  • php artisan view:cache: compiles Blade templates ahead of time.

Be aware that configuration and route caches can break things if your app relies on environment‑specific config or closures in routes. Always test these commands in a staging environment before running them on production, and ensure your deployment process can clear caches if something goes wrong.

On the Composer side, two flags are fundamental for production builds:

  • --no-dev when installing to avoid shipping dev‑only packages, which reduces the attack surface and package overhead.
  • dump-autoload -o to generate an optimized class map, speeding up autoloading.

If your production server doesn’t have Composer installed, run these steps in your local or CI environment and upload the resulting vendor directory along with the app code. That way, production never needs to touch Composer yet still benefits from optimized autoloading.

Structuring the application, assets, sessions and queues

Beyond DB and caching, the way your Laravel project is structured internally also affects runtime performance and maintainability. A leaner, more intentional architecture typically translates to fewer unnecessary operations on each request.

Pay particular attention to how you use the service container and middleware:

  • Avoid registering or resolving services that are rarely or never used for the current request; use deferred loading when possible.
  • Attach middleware only to the routes or groups that really need them instead of piling everything onto web or api globally.
  • Organize routes into groups with prefixes and shared middleware to make them both easier to manage and more cache‑friendly.

On the frontend side, a proper asset pipeline is a big performance win. Using Laravel Mix, Vite or a similar tool, you should:

  • Compile SCSS/LESS and modern JS into minified, versioned bundles.
  • Enable minification and tree‑shaking to reduce file size.
  • Serve static assets via a CDN when possible, letting the CDN handle geo‑distributed delivery.

Session and queue configuration has a direct impact under load. Small apps can survive with file or database session drivers, but as concurrency grows, these options quickly turn into bottlenecks. Redis is typically the go‑to choice for fast, concurrent session storage and queue backends in production.

Offloading heavy work to queues is one of the keys to keeping response times low. Sending emails, generating PDFs, processing large reports or images – these tasks should run in the background via jobs and workers (php artisan queue:work), not block the main HTTP response. On VPS or dedicated machines, you can run these workers persistently via system services or process managers.

A well‑structured Laravel app, combined with the right hosting features (Redis support, NVMe storage, SSH access), can stay fast and stable even as traffic and data grow. The goal is always the same: keep your requests doing the minimum synchronous work possible, and push the rest into caches or background tasks.

Debugging and monitoring Laravel in production: Debugbar vs Telescope

At some point you’ll need more than logs and error pages to figure out what’s going on inside your Laravel app. That’s where tools like Laravel Debugbar and Laravel Telescope come into play, each with its own sweet spot.

Laravel Debugbar is a browser toolbar that injects itself at the bottom of your HTML pages during development. It shows SQL queries (with counts and timings), timeline breakdowns, routes, views, logs and more, right inside the page you’re testing. It’s ideal for fast, visual debugging when working with Blade views locally.

A typical installation flow for Debugbar uses Composer with the --dev flag, ensuring the package is only present in development:

  • Install it with Composer in dev mode.
  • Publish its config if needed and let it auto‑enable when APP_DEBUG=true.
  • Tweak settings via debugbar.php or env flags if you want to toggle it per environment.

Debugbar focuses on the current request. It’s not meant to store lots of historical data or be accessible to end users. In fact, its own documentation warns against enabling it on publicly accessible sites because it reveals sensitive request information by design.

Laravel Telescope, in contrast, is a full‑fledged monitoring panel for your Laravel app. It runs on its own URI (typically /telescope) and records requests, queries, exceptions, jobs, commands, mail, notifications and more into your database. It’s especially useful for APIs, SPAs or systems with heavy background activity where there is no HTML UI to inject a toolbar into.

Installing Telescope involves adding the package, publishing assets and running migrations to create its tables. Because data accumulates over time, you also want to schedule regular pruning, for example via a daily telescope:prune command in your console kernel, to keep the tables from growing indefinitely.

When using Telescope in production you must secure it with authorization gates. Only admins or specific roles should access /telescope, and that protection should live in the Telescope service provider. Done right, Telescope can safely run even in live environments and becomes a powerful profiler and audit log for slow queries and errors.

Feature‑wise, Debugbar and Telescope overlap in some areas but with different trade‑offs. Debugbar delivers immediate, in‑page feedback (perfect for spotting N+1s visually in views), while Telescope keeps a long‑running history that teams can inspect asynchronously and collaboratively. Telescope tends to handle big volumes of data better because it loads information asynchronously and stores it in the DB, at the cost of extra maintenance and storage.

You don’t have to treat them as mutually exclusive. Many developers run Debugbar locally for view‑level debugging and use Telescope in staging or production to track background jobs, API requests and long‑running processes. If you’re already using Telescope and worried about overhead, Telescope Toolbar is an option that gives you a Debugbar‑like interface backed by Telescope’s data store.

Regardless of the toolset, the key is to disable Debugbar in production, restrict Telescope with proper authorization, and periodically prune stored data. Combined with server logs and metrics from your host’s dashboard, they form a robust debugging and monitoring stack for serious Laravel applications.

Bringing all of these pieces together – a clean deployment flow, careful environment configuration, aggressive query and cache optimization, tuned PHP and web server settings, and the right debugging tools – turns Laravel from “fast in theory” into “fast under real traffic”. With a solid checklist for pre‑production, an explicit production deploy routine and continuous monitoring, you can confidently push new releases, track down issues and keep your Laravel app responsive even as your user base grows.

Related posts: