Laravel

The PHP Runtime: 8.3 to 8.5, OPcache, JIT

OPcache, preloading, and the JIT that PHP ships switched off.

Chapter 11 of 1312 min readOpen access

The PHP Runtime is last in this book on purpose. Runtime settings are the most discussed and the least diagnostic part of Laravel performance, because they apply a flat change to every request and therefore cannot explain why one route is slow and another is not.

Key takeaways

  • Laravel 13, released 17 March 2026, requires PHP 8.3 to 8.5 and receives bug fixes until Q3 2027 and security fixes until 17 March 2028.
  • As of 29 July 2026, php.net records PHP 8.3 as having left active support on 31 December 2025 with security support until 31 December 2027, so Laravel 13's floor is a security-only branch.
  • OPcache defaults documented by php.net include memory_consumption at 128, interned_strings_buffer at 8 and max_accelerated_files at 10000, and opcache.enable cannot be turned on at runtime, only off.
  • Disabling opcache.save_comments discards documentation comments and the manual warns it may break frameworks that rely on comment parsing for annotations.
  • The PHP RFC "JIT Configuration Defaults" changed PHP 8.4's defaults from tracing with a zero buffer to opcache.jit=disable with a 64 megabyte buffer, passing seventeen votes to zero.

Read this after Chapter 8, because a long-lived worker changes what OPcache and preloading are worth, and after Chapter 1, because the only honest reason to touch any of this is a profile that shows flat per-request overhead. Chapter 12 needs opcache.validate_timestamps specifically, since a cold code cache is one of the things that invalidates a first test run.

A team is told the application is slow. Someone finds a blog post listing eleven php.ini settings.

They apply all eleven in one commit, over a weekend, with no baseline. The application is different afterwards and nobody can say which setting did what, or whether the net effect was positive.

Two of the eleven were wrong for this workload. Nobody will find out for eight months.

The floor is already on a security-only branch

Start with the fact that has a deadline attached, because it is the only item in this chapter that gets worse if you ignore it.

Laravel's release notes state that Laravel 13 was released on 17 March 2026, requires PHP 8.3 to 8.5, receives bug fixes until Q3 2027 and security fixes until 17 March 2028. The framework's support policy is 18 months of bug fixes and two years of security fixes, with a major release roughly annually. Laravel 12, released 24 February 2025, has bug fixes ending 13 August 2026, and Laravel 11's security window closed on 12 March 2026.

Now read that beside php.net's own support table, parsed on 29 July 2026. PHP 8.3, released 23 November 2023, left active support on 31 December 2025 and has security support running until 31 December 2027. So Laravel 13 will accept a PHP version that stopped receiving bug fixes seven months ago.

BranchReleasedActive support untilSecurity support until
8.28 December 202231 December 202431 December 2026
8.323 November 202331 December 202531 December 2027
8.421 November 202431 December 202631 December 2028
8.520 November 202531 December 202731 December 2029

A framework minimum is a compatibility statement, not an operational recommendation. Read the table and pick a branch with active support left in it, which on this date means 8.4 or 8.5.

OPcache is already on, and its defaults assume less

The most valuable runtime setting is one you already have. The PHP manual documents opcache.enable as defaulting to 1 and opcache.enable_cli as 0. It notes that opcache.enable cannot be enabled at runtime, only disabled.

The defaults worth checking against your own application are the sizes. memory_consumption defaults to 128, interned_strings_buffer to 8 and max_accelerated_files to 10000. A Laravel application with a full vendor directory can approach or exceed ten thousand files, at which point scripts stop being cached and are recompiled on every request.

You do not have to guess whether that happened. PHP exposes opcache_get_status, which reports cache_full, num_cached_scripts, num_cached_keys against max_cached_keys, opcache_hit_rate, and current_wasted_percentage under memory usage. That is a measurement rather than an opinion, and it takes one request to collect.

Two readings decide the setting. If cache_full is true or cached keys are near the maximum, raise max_accelerated_files. If wasted memory is approaching the documented max_wasted_percentage default of 5, raise memory_consumption.

Check it once per deploy target. Then stop thinking about it. It is a two-minute reading.

Timestamp validation decides whether a deploy is visible

This is where a performance setting becomes a deployment behaviour. Getting it wrong produces bugs that look like caching bugs somewhere else entirely.

The PHP manual documents opcache.validate_timestamps as defaulting to 1, meaning PHP checks whether a file has changed, and opcache.revalidate_freq as defaulting to 2 seconds, which is how often that check happens. Turning validation off removes the check entirely, which is the aggressive production setting.

The consequence is unambiguous and worth saying plainly. With validation off, new code is not live until the cache is cleared or the process is restarted. Your deploy must do that explicitly.

Under Chapter 8's Octane model this compounds, because the application is already held in memory and octane:reload is already required. Two independent caches now need invalidating on deploy, and a pipeline that handles one and not the other produces a partial deploy.

Decide the pair together. Validation off plus an explicit reload step, or validation on and pay for the stat call. One or the other. Never half of each.

Saved comments are a correctness setting in disguise

One OPcache directive looks like free savings and is not. It appears on more tuning lists than it should.

The PHP manual documents opcache.save_comments as defaulting to 1, and states that disabling it discards all documentation comments from the opcode cache to reduce the size of the optimised code. Then it gives the warning that matters: disabling it may break applications and frameworks that rely on comment parsing for annotations.

In a modern Laravel application the risk has shifted rather than disappeared. PHP attributes are not doc comments, so Laravel 13's own #[Tries] and #[Backoff] attributes from Chapter 7 are unaffected. What is affected is anything in your dependency tree still parsing doc blocks, and you do not have a complete inventory of that.

Leave it on. The saving is memory you can buy. The failure mode is behaviour you cannot predict.

Preloading buys boot time and costs discipline

Preloading has the most genuine upside for a framework application. It also has the strictest operational requirements.

The PHP manual documents opcache.preload as empty by default, with opcache.preload_user alongside it, and notes that preloading is not supported on Windows. A preload script is executed once at server start and the classes it loads stay resident for every request thereafter.

The trade is the same shape as Octane's. You have moved work from per-request to per-start, which means restarts become meaningful events and stale preloaded code becomes possible. A preload list that is out of step with the deployed code is a class of bug that is genuinely hard to diagnose.

There is also an interaction worth naming. If you run Octane, boot is already amortised across hundreds of requests per worker, so preloading is solving a problem you largely removed. Under FPM, where every request boots, it has considerably more to give.

Measure boot cost first, with Chapter 1's profiler on your cheapest route. If boot is small, preloading is effort with nothing behind it.

PHP ships the JIT switched off

This is the most misunderstood item in Laravel performance folklore. The correction comes from PHP itself rather than from an opinion.

The PHP manual documents opcache.jit as accepting disable, off, tracing or on, function, or a four-digit CRTO value. It states that the default is disable as of PHP 8.4.0. The companion setting opcache.jit_buffer_size defaults to 64M as of the same version, and the manual notes that a zero value disables the JIT.

The PHP RFC titled "JIT Configuration Defaults" explains how that came about. It changed PHP 8.4's defaults from opcache.jit=tracing with a buffer size of zero to opcache.jit=disable with a buffer of 64 megabytes, keeping the JIT off by default while moving where the off switch lives. The vote closed in December 2023 with seventeen in favour and none against. The RFC also notes the migration consequence: applications that used to enable the JIT by setting only a buffer size must now set opcache.jit explicitly.

Now the mechanism, which is what actually decides this for you. The JIT compiles PHP to machine code, so the only time it can give back is time the CPU spends executing PHP. A request that spends most of its wall clock waiting on a database, a cache or an HTTP call has very little of that time to give.

That is why I will not print a percentage here, and neither will anybody honest. The answer depends on the ratio of compute to wait in your own workload. Nobody else can supply it.

Artisan optimize caches four things, and one bites

Laravel's deployment documentation gives you the framework-level equivalent of OPcache. One of its four caches has a documented side effect people meet in production.

php artisan optimize caches configuration, events, routes and views, and optimize:clear reverses it. The side effect is on configuration: once configuration is cached, the .env file is not loaded, so any env() call outside a configuration file returns null.

That is not a bug and it is not avoidable. It is the point. Configuration caching exists precisely so that the environment file is not read per request, and code that calls env() in a service or a controller was always relying on something it should not have.

The same page requires APP_DEBUG to be false in production and documents a health route served at /up. Debug mode in production is a correctness and disclosure problem before it is a performance one, and it also changes what the framework collects per request.

Put all four caches in the deploy. Then grep the application for env( outside config/.

A worked reading, composited and labelled

The details here are composited from ordinary production shapes rather than one system, and the shape is exact.

The reading that justifies runtime work at all is a comparison between two routes, and Chapter 1's profiler gives you both. Profile the slowest route and the cheapest route on the same machine, and separate each call tree into framework construction, application logic and time spent waiting on something external.

If the cheapest route's total is a large share of the slowest route's total, you have a flat overhead problem, and this chapter is where it lives. If the cheapest route is trivial and the slowest route is dominated by waiting, the runtime has nothing for you, and Chapters 3 through 6 have all of it.

Pair that with one opcache_get_status reading for cache_full and the hit rate. Between the two, you can say which of the settings in this chapter is worth changing and why. Without them, you are applying somebody else's php.ini to a workload they never saw.

The objection: just enable the JIT

The folklore says the JIT is free speed that PHP inexplicably leaves off, so turning it on is an obvious win. Three facts make that untenable.

The first is that PHP's own maintainers chose the default, deliberately, through the RFC above and a unanimous vote. That is not an oversight to be corrected by a blog post.

The second is the mechanism already stated. Compiling PHP faster cannot reduce time spent waiting on a query, a lock, a cache or an API, and a typical Laravel request is mostly waiting.

The third is that the numbers circulating on this topic are unusable. Searching it returns posts quoting each other's percentages with no workload description, no hardware, no percentile and no repetition count, which makes them anecdotes with decimal points. I am not going to launder them by repeating them here.

So the decision rule is a measurement rather than a setting. If your profile shows a route dominated by PHP execution rather than waiting, such as heavy in-process transformation or numeric work, test the JIT on that route with a before and an after you can re-run. Otherwise leave it as PHP ships it.

Chapter summary

The runtime is last because its settings apply flatly to every request and therefore cannot explain why one route is slow, and the first thing to read here is a date rather than a directive. Laravel 13 was released 17 March 2026, requires PHP 8.3 to 8.5, and gets bug fixes until Q3 2027 and security fixes until 17 March 2028, under a policy of 18 months and two years. The php.net table, parsed 29 July 2026, records PHP 8.3 as having left active support on 31 December 2025. So the framework's floor is a security-only branch, and a compatibility minimum is not an operational recommendation. OPcache is on by default and its documented sizes assume a smaller application, with memory_consumption at 128, interned_strings_buffer at 8 and max_accelerated_files at 10000, which a full vendor directory can exceed. The function opcache_get_status answers whether that happened, reporting cache_full, cached keys against the maximum, the hit rate and wasted percentage. validate_timestamps defaults to on with a two second revalidation frequency, and turning it off makes an explicit cache clear or restart part of the deploy, which compounds with Octane's own reload requirement. Leave save_comments on, because php.net warns that disabling it may break frameworks relying on comment parsing for annotations. Preloading moves boot cost to server start, is unsupported on Windows, and has much less to offer once Octane already amortises boot. php artisan optimize caches configuration, events, routes and views, and cached configuration stops loading .env, making env() outside config files return null. And PHP has shipped the JIT disabled by default since 8.4, by an RFC that passed seventeen to zero.

Every measurement in this book so far has been taken on production traffic you did not choose. Chapter 12 is Load Testing That Predicts Production, which is how you generate the traffic on purpose and get a result you are entitled to believe.

Sources

  1. Supported VersionsThe PHP Group · 2026-07-29 · Official documentation · verified
  2. OPcache Runtime ConfigurationThe PHP Group · 2026-07-29 · Official documentation · verified
  3. PHP RFC: JIT Configuration DefaultsThe PHP Group · 2023-12 · Standard · verified
  4. Release Notes, Laravel 13Laravel · 2026-03-17 · Official documentation · verified
  5. DeploymentLaravel · 2026-07-29 · Official documentation · verified