Skip to content
Latchkey
Published June 2026

The State of PHP CI 2026

What it really costs to test PHP at scale: Composer caches, PHPUnit suites, and the version matrix that quietly doubles your bill.

74%
of PHP CI wall-clock spent in install plus test, not build
Latchkey analysis (modeled)
3.4x
pipeline minutes when testing a 4-version PHP matrix vs a single version
Latchkey analysis (modeled)
69%
cheaper to run the same PHP suite on managed runners vs hosted
Latchkey analysis (modeled)

Executive summary

PHP testing is dominated by two costs that have nothing to do with your application code: pulling dependencies with Composer and fanning the suite across a version matrix. A modern Laravel or Symfony pipeline spends the bulk of its billed minutes installing packages and waiting on PHPUnit, and most of that is repeated unchanged on every push. The framework code that the team actually wrote is a small fraction of what CI spends its time on.

With CI adoption at 76% among professional developers, PHP teams are no longer asking whether to automate tests. They are asking why the bill grows faster than the codebase, and the answer is almost always mechanical. A one-line change to a controller still triggers a full Composer resolve, a full PHPUnit run, and a full sweep across every supported PHP version, so the cost tracks the size of the repository and the width of the matrix rather than the size of the diff.

This report quantifies where PHP CI minutes go, how much an uncached Composer install costs over a month, why the version matrix is the single most expensive line item most teams never audit, and what PHPUnit becomes once caching is solved. We look at the install curve from cold to warm to managed cache, the near-linear minute cost of widening the version matrix, and the per-minute runner economics that decide the bill once the work itself is right-sized.

The recurring theme is that PHP CI is overwhelmingly Linux CPU work with a deterministic, cacheable dependency step in front of it. That is the most favorable possible shape for optimization, because almost none of the cost is intrinsic. Cold Composer installs, redundant matrix legs, and transient failures that pass on retry are all removable without touching a single test, which is exactly where a managed, self-healing runner layer changes the math.

For engineering leaders the practical message is that the PHP pipeline rewards hygiene over heroics. A persistent Composer cache, a matrix that is honest about what runs on every push, sharded PHPUnit on right-sized Linux runners, and automatic recovery of transient failures turn the bill from a growing tax into a flat, predictable cost. None of it requires rewriting the suite, and all of it compounds on every push for the life of the codebase.

Where a PHP CI minute goes
PHPUnit suite 41%
Composer install (uncached) 26%
Static analysis (PHPStan) 18%
Setup, lint, queue 15%

Modeled split of billed minutes for a typical Laravel pipeline. · Source: Latchkey analysis (modeled)

Composer install time, cold vs warm cache
Cold, no cache78 sGitHub cache31 sWarm local cache14 sManaged cache (Latchk…6 s

Dependency resolution and download time per run. · Source: Latchkey analysis (modeled)

Email me the report

The full report is right here on this page, free. Want the link in your inbox to read later or share, plus new Latchkey reports as they drop? Drop your email and we will send it over.

Sent! Check your inbox for the report link.

No spam. Unsubscribe anytime.

Composer is the most repeated wasted work in PHP CI

A cold Composer install resolves the full dependency graph and downloads every package on every run. For a mid-size Laravel app that is well over a minute of identical work repeated on each push, producing a vendor directory that is bit-for-bit the same as the one from the previous run unless the lockfile actually changed. It is the clearest example in the PHP pipeline of paying repeatedly for a result that never varies.

Warm caching collapses that minute to seconds. A GitHub-hosted cache helps but is itself billed restore time and frequently misses on the first run of a branch; a warm local cache does better; and a managed cache that survives across runners removes the variance entirely, because the dependency store is already present when the job starts. The chart below shows the full curve from a cold install to a managed warm cache, and the gap between the ends is the recoverable minute.

The reason this is the easiest minute most teams reclaim is that Composer's inputs are perfectly deterministic. The lockfile pins exact versions, so a cache keyed on the lockfile hash is correct by construction: it can never serve the wrong dependencies, and it hits on every run where the lockfile is unchanged, which is the overwhelming majority. The only thing standing between a cold resolve and a warm restore is a cache layer that persists across jobs.

  • A cold Composer install is over a minute of identical work repeated on every push.
  • Keying the cache on the lockfile hash is correct by construction and hits on nearly every run.
  • A managed cache that persists across runners removes the cold-install variance entirely.

The PHP version matrix is your hidden multiplier

Testing across 8.1 through 8.4 means running the entire suite four times, and the cost is near-linear in the number of versions: a nine-minute single-version pipeline becomes roughly thirty-one minutes across four. Each leg installs dependencies, runs PHPUnit, and runs static analysis independently, so the matrix multiplies the whole pipeline rather than just the test step.

The matrix tends to widen silently. Most teams add a PHP version when it releases and never remove the old one, partly out of caution and partly because nobody owns the decision to drop it. The result is a matrix that grows monotonically, so a one-line change to a controller eventually pays for a quadruple validation that no individual change ever needed.

The fix is not dropping coverage but right-sizing it: run the full matrix on main and on pre-release, and give pull requests single-version fast feedback on the primary supported PHP version. Run the legs in parallel on cheap Linux runners rather than serialized on a single box, so the matrix costs wall-clock once rather than four times. The chart below shows the near-linear minute curve that makes this the highest-value audit most PHP teams never run.

  • Each added PHP version is a near-linear multiplier on total pipeline minutes.
  • Matrices widen silently because teams add versions and rarely remove old ones.
  • Full matrix on main and pre-release, single-version on pull requests, parallel legs on Linux.
Pipeline minutes by PHP version matrix width
1 version (8.4)9 min2 versions17 min3 versions24 min4 versions (8.1-8.4)31 min

Total billed minutes as the supported PHP version count grows. · Source: Latchkey analysis (modeled)

PHPUnit dominates the clock once caching is solved

Once Composer is cached, the PHPUnit suite becomes the long pole, and the chart of where a PHP minute goes shows it as the single largest slice. The usual culprits are database-backed feature tests that hit a real database, and uncached fixtures that rebuild the same seed data for every test class. These are the tests worth keeping, which is exactly why making them faster matters more than deleting them.

Paratest sharding plus transactional fixtures typically cut suite wall-clock by half. Running each test in a transaction that rolls back instead of truncating and reseeding removes the per-test database churn, and splitting the suite across parallel processes uses the cores that a single-threaded PHPUnit run leaves idle. The two techniques compound: faster individual tests across more parallel workers.

Pairing that with right-sized runners avoids paying for cores the suite cannot use, and it avoids paying macOS or oversized-instance rates for work that is pure Linux CPU. PHPUnit has no reason to run anywhere but Linux, so the runner-economics lever and the parallelism lever sit right next to each other, and a team that pulls both gets a faster suite on cheaper compute at the same time.

Static analysis is a fixed cost that caches well

PHPStan and similar static-analysis tools are a meaningful slice of the PHP pipeline, and on a large codebase a full analysis pass is far from free. Because most teams run it at a high level on the whole tree on every push, it behaves like a fixed tax that grows with the codebase rather than with the change being validated.

Static analysis is highly cacheable because its result is a deterministic function of the source and the configured level. PHPStan's result cache stores per-file analysis keyed on content, so an unchanged file is not re-analyzed when the cache is present. When that cache persists across runs, the stage drops from a full-tree analysis on every push to an analysis proportional to what actually changed.

The pattern is identical to the Composer story and the same managed-cache layer serves it: deterministic inputs, a known cache key, and the only obstacle being persistence across jobs. A team that already caches Composer gets static-analysis caching almost for free, and the two together pull the pipeline's two largest non-test costs down to the size of the diff.

Transient failures retry clean and tax every merge

Packagist timeouts, flaky database connections during heavy feature tests, and OOM kills on undersized runners fail builds that pass on a clean retry. None of these are real test failures: the suite is fine, the environment hiccuped while installing a package or holding a database connection. But the red check is indistinguishable from a real failure until someone investigates and re-runs it.

At even a two percent flake rate the cost compounds for a busy repository. The re-run minutes are real billed compute, and the engineer context switch, stopping work to investigate a red change that turns out to be nothing, is the larger and entirely invisible cost. On a repo merging dozens of times a day, a handful of spurious reds a week is a steady drain on both the bill and the team's attention.

Self-healing runners absorb the retry automatically on a fresh environment, without anyone touching the test suite. The transient failure is detected, retried, and resolved before it reaches the pull request, so the minutes spent are recovery minutes rather than wasted ones and the developer never loses focus. The test code does not change, because the test code was never the problem.

Managed runners change the PHP CI break-even

Self-hosting PHP runners looks cheap per-minute on a spreadsheet, because you compare a raw instance price to a hosted per-minute rate and conclude you can run your own fleet for less. That comparison omits the two costs that actually decide the question: idle compute between pushes, and the engineering time spent patching, scaling, and cleaning up the fleet.

Managed runners capture roughly 69% of the hosted cost with zero ops, which is the relevant comparison rather than instance price versus hosted price. And because PHP CI is overwhelmingly Linux CPU work with predictable, burst-shaped demand, it benefits the most from an elastic managed layer that scales with pushes rather than from oversized self-hosted boxes sitting idle between merges.

The chart below shows why the per-minute rate matters so much once the work is right-sized. With Composer cached, the matrix slimmed, and PHPUnit sharded, the remaining cost is mostly a function of the per-minute price of the compute, and a managed Linux runner priced well below hosted rates is the lever that turns a right-sized pipeline into a cheap one. The optimizations make the pipeline efficient; the runner price makes it inexpensive.

Hosted runner cost per minute by platform
Linux 2-core$0.008Windows 2-core$0.016macOS$0.08Managed (Latchkey)$0.0025

Published GitHub-hosted rates vs a managed alternative. · Source: GitHub Actions pricing + Latchkey rates

Framework upgrades are the real reason the matrix exists

The PHP version matrix is not arbitrary caution: it exists because the ecosystem moves, and a library or application that claims support for a range of PHP versions has to prove it. Framework major releases, language deprecations, and the steady cadence of PHP itself mean the supported range is a real contract with users, and CI is where that contract is enforced.

This is why the answer to matrix cost is right-sizing rather than elimination. Dropping a version to save minutes is a coverage decision with downstream consequences, not just a cost decision, and it should be made deliberately when a version reaches end of life rather than implicitly to shrink a bill. The matrix is expensive because it is doing real work.

The reconciliation is to make the matrix cheap rather than narrow. Parallel legs on cached, right-sized Linux runners let a team keep the full supported range green on main without paying for it on every pull request, so the coverage contract holds and the hot-path feedback loop stays fast. The matrix earns its place; it just does not need to run on every push to earn it.

Recommendations

Cache Composer on the lockfile hash in a layer that persists

Key the Composer cache on the lockfile hash so it is correct by construction and hits on nearly every run, and store it in a layer that survives across jobs rather than one rebuilt and re-uploaded each time. This collapses an install that runs over a minute cold down to a few seconds and reclaims the largest non-test block in the pipeline.

Right-size the version matrix instead of dropping coverage

Run the full PHP version matrix on main and on pre-release, and give pull requests single-version fast feedback on the primary supported version. Run the legs in parallel on cheap Linux runners so the matrix costs wall-clock once, not four times, keeping the coverage contract while capping hot-path cost.

Shard PHPUnit with transactional fixtures

Use Paratest to split the suite across parallel workers and wrap database-backed tests in transactions that roll back instead of truncating and reseeding. Together these typically halve suite wall-clock without deleting a single test, and they use cores a single-threaded run leaves idle.

Cache static analysis alongside dependencies

Persist the PHPStan result cache so unchanged files are not re-analyzed on every push. It uses the same deterministic-input, persistent-cache pattern as Composer, so a team that already caches dependencies gets static-analysis caching almost for free and pulls another fixed cost down to the size of the diff.

Move to a managed Linux runner once the work is right-sized

PHP CI is burst-shaped Linux CPU work, the ideal case for an elastic managed runner that scales with pushes and avoids idle self-hosted capacity. After caching and matrix right-sizing, the remaining cost is mostly the per-minute rate, so a managed runner priced well below hosted rates is what turns an efficient pipeline into a cheap one.

Outlook

Expect the gap between PHP teams that treat CI as managed infrastructure and those still paying cold-install and full-matrix prices to widen through 2026. The optimizations in this report compound: a team with a persistent Composer cache, a slim hot-path matrix, sharded PHPUnit, and automated recovery does not just spend less, it merges faster, which lets it ship smaller changes more often, which makes every one of those optimizations matter more on a busier pipeline.

The ecosystem direction reinforces the case for caching and right-sizing. PHP continues its steady release cadence and the major frameworks continue to move, so the supported version range, and therefore the matrix, is not going to shrink on its own. The teams that make the matrix cheap rather than narrow will keep their coverage contract intact while their bills stay flat, and the teams that respond to cost by dropping versions will quietly erode the support they promised users.

For most PHP teams the practical takeaway is that the pipeline does not need a rewrite to get fast and cheap, because almost none of its cost is intrinsic. It needs a Composer cache that persists, a matrix that is honest about what runs on every push, a sharded suite on right-sized Linux runners, and a runner layer that recovers from transient failures on its own. The teams that internalize that will spend the next two years treating PHP CI as cheap, fast feedback while their peers keep paying for work they could have cached away.

Methodology

This report combines public PHP ecosystem signals (Packagist usage, framework release cadence, published runner pricing) with Latchkey runner analysis of Composer, PHPUnit, and matrix-driven pipelines. Modeled figures reflect a representative mid-size Laravel or Symfony repository with a database-backed feature-test suite, a four-version supported PHP range, and a PHPStan static-analysis stage. Figures labeled "modeled" are illustrative estimates derived from public pricing and typical pipeline shapes, not a primary survey; figures attributed to a named source reflect that source. Pricing reflects published rates at time of writing and should be verified against current provider pricing.

Sources

More Latchkey reports

See what you would save with Latchkey managed runners and self-healing. Start free →