# Laravel Tests Fail When Two Runs Overlap, Even With --parallel

**Author:** Mozex | **Published:** 2026-09-30 | **Tags:** Laravel, PHP, Testing, Open Source, Database | **Package:** mozex/laravel-test-lanes | **URL:** https://mozex.dev/blog/29-laravel-tests-fail-when-two-runs-overlap-even-with-parallel

---


Whenever two test commands ran inside one of my projects at the same time, they failed, because one run would clear the database under the other. It happened especially when I asked a Claude Code session to hand work to sub-agents: each sub-agent ran the tests for its own change, the runs failed, and it became a loop of discovering the problem and running it again.

For this post I reproduced it on a fresh Laravel app whose `phpunit.xml` points at MySQL, with 1,201 tests, 1,200 of them using `RefreshDatabase`. Run alone, before any fix, the suite passes in under a minute. I started `php artisan test` twice at the same moment, three times. All six runs failed 1,167 to 1,200 of their 1,201 tests, and each took 3.5 to 4.4 minutes.

The fix is my package [mozex/laravel-test-lanes](https://mozex.dev/docs/laravel-test-lanes/v1). Install it with `composer require mozex/laravel-test-lanes --dev` and there's nothing else to set up. With it, those runs pass. Everything here ran on Laravel 13.33, PHP 8.5, PHPUnit 12.5 with ParaTest 7.20, and MySQL 8.4 on Windows 11, with version 1.1.1 of the package (Laravel 11 to 13, PHP 8.2+, MySQL, MariaDB and PostgreSQL).

<!--more-->

## Serial runs share one database, and RefreshDatabase keeps retrying

Without `--parallel`, Laravel never switches databases. Every serial run of the suite uses the database `phpunit.xml` names, here `lanesapp_testing`. `RefreshDatabase` runs `migrate:fresh` before a process's first test, then [wraps each test in a transaction](https://mozex.dev/blog/16-the-laravel-bug-your-tests-will-never-catch#why-your-tests-are-blind-to-this). Two runs at once drop and create the same tables under each other. Two of the messages, cut before the SQL:

```text
SQLSTATE[42S01]: Base table or view already exists: 1050 Table 'users' already exists
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'lanesapp_testing.migrations' doesn't exist
```

Almost every test failed, not just the first few, because `RefreshDatabase` marks the migration as done only after `migrate:fresh` succeeds. When it throws, the next test runs `migrate:fresh` again, which drops the other run's tables, which makes the other run's migration throw. All 7,158 failures in those six runs came from migration statements, and none from a test's own queries.

## --parallel numbers its databases from 1 in every run

[Laravel's docs](https://laravel.com/docs/testing#parallel-testing-and-databases) say parallel test databases are "suffixed with a process token which is unique per process", as in `your_db_test_1` and `your_db_test_2`. Unique inside one run. The token is ParaTest's worker number, `TEST_TOKEN`. ParaTest counts from 1 every time it starts. With 24 processes (the default on this machine, which has 24 logical processors), two runs both use `lanesapp_testing_test_1` to `_test_24`.

I started a second `--parallel` run 8 seconds after the first, three times. Both runs failed in every pair, 1,085 to 1,176 of 1,201 tests each. When I started both at the same moment, three times, one run of each pair stopped before its first test with `mkdir(): File exists`, a different collision (below), and the other passed because it had the databases to itself.

ParaTest also sets `UNIQUE_TEST_TOKEN`, "unique both per run and per process" according to [its README](https://github.com/paratestphp/paratest). Laravel 13.33 never reads it, and a name that changes every run would leave a new database behind every run anyway.

A serial run next to a `--parallel` run didn't collide: one uses the base database, the other the `_test_N` ones. And on the skeleton's default in-memory SQLite there's nothing to collide on, because every process has its own database.

## Lanes give every test process a database no other run is using

Install the package as a dev dependency:

```bash
composer require mozex/laravel-test-lanes --dev
```

The service provider registers itself when tests run under `APP_ENV=testing`, which the skeleton's `phpunit.xml` sets. In the test app, the install changed `composer.json` and `composer.lock` and nothing else.

Each test process then claims a lane: the lowest number from 1 to 256 whose advisory lock it can take (`GET_LOCK` on MySQL and MariaDB, `pg_try_advisory_lock` on PostgreSQL), on its own connection that Laravel doesn't manage. The lane becomes the token, so the process works in `lanesapp_testing_test_lane1`, `_test_lane2` and so on, and Laravel creates each one on first use. Serial runs are routed the same way, so a plain `php artisan test` gets a lane instead of the shared base database.

The lock lasts as long as that connection. [MySQL's docs](https://dev.mysql.com/doc/refman/8.4/en/locking-functions.html) say it is released "implicitly when your session terminates (either normally or abnormally)". I killed a `--parallel` run with `taskkill /F /T` ten seconds in, while it held lanes 1 to 24. A second later, none were held. There is no lock file to clean up after a crash, unlike a Redis queue lock, which [stayed behind after I killed its worker with `taskkill /F`](https://mozex.dev/blog/25-shouldbeunique-vs-withoutoverlapping-neither-sets-a-lock-expiry-for-you#a-job-that-times-out-leaves-the-queue-lock-behind).

I repeated the pairs with the package installed. All 24 runs passed all 1,201 tests: three serial pairs started at the same moment, and nine `--parallel` pairs started 1, 3 or 8 seconds apart. Two runs at once used lanes 1 to 48, and after all the runs for this post the server held only those 48 lane databases, because a process always takes the lowest free lane. `--parallel` pairs started at the same moment are the exception (below).

## Lanes cost no measurable time with --parallel, and about 15 ms per test serially

I switched the package off with `TEST_LANES_ENABLED=false` and on again, alternating runs. With `--parallel` there was no measurable difference: a median of 21.9 seconds off and 22.1 on, five runs each. Two `--parallel` runs at once took 31 to 47 seconds each, against 22 alone, because they share one machine.

Serial runs did pay. Over three runs each, the median was 54.8 seconds off and 73.1 on, a difference of about 15 milliseconds per test. The package sends serial runs through Laravel's parallel-testing setup, and that setup switches the connection to the test database before every test. The server's connection counter rose by 2,403 during one serial run with the package on and by 1,202 with it switched off. A `--parallel` run goes through that setup anyway. With 24 processes, `--parallel` also ran this suite in less than half the serial time.

Each test process holds two connections, one to its lane database and one for the lock. One `--parallel` run peaked at 49 open connections on the server with the package on and 25 with it switched off, my sampling connection included. Two runs at once peaked between 97 and 106 in the four pairs I measured, up to 10 above twice their 48 processes. This MySQL server allowed 151. [PostgreSQL's default](https://www.postgresql.org/docs/current/runtime-config-connection.html) is "typically 100", so set `max_connections` to at least twice the number of test processes in all the runs you expect at once, plus headroom.

## What lanes don't isolate

One part of Laravel's parallel runner still uses the worker number: it creates and deletes the compiled-view folders `storage/framework/views/test_1` to `test_24` from the parent process. With the package installed, I started three more `--parallel` pairs at the same moment. In two of them, one run stopped with `mkdir(): File exists`; in the third, one run passed every test and still exited with code 1, because the other run deleted one of those folders while this run was deleting it. Starting the second run a second or more later avoided the collision in all nine pairs I tried.

The same goes for anything else your suite keys on a fixed path or on ParaTest's raw `TEST_TOKEN`. A SQLite database file is refused with an exception that names `TEST_LANES_ENABLED=false` as the way out, and [the package's docs](https://mozex.dev/docs/laravel-test-lanes/v1#what-is-not-isolated) cover the other setups it refuses or can't serve, such as a `DB_URL`-style connection and PgBouncer in transaction-pooling mode.

Lane databases stay between runs for reuse. `php artisan test-lanes:cleanup` drops every one whose lock is free, but it takes the database name from `.env`, not `phpunit.xml`. In my test app it dropped nothing until I ran it as `DB_DATABASE=lanesapp_testing php artisan test-lanes:cleanup`, which dropped all 48.

What else collides when your agents run tests side by side?