Skip to main content
RefoundRefound
All articles
migrationphplegacy systemsarchitecture

How to Migrate from PHP 5.6 Without Downtime

Niclas Kusenbach

PHP 5.6 reached end-of-life in December 2018. That was over seven years ago. If your production system still runs on it, every unpatched CVE since then is an open door. Every deployment is a gamble. And every month you wait, the migration gets harder — because the gap between PHP 5.6 and PHP 8.3 is not incremental. It is architectural.

But you already know this. The question isn't whether to migrate. It's how to do it without taking the system offline.

Why "just update PHP" doesn't work

The gap between PHP 5.6 and modern PHP (8.x) isn't a simple version bump. It's a language evolution that breaks backward compatibility in fundamental ways:

  • Removed functions. mysql_* functions were removed in PHP 7.0. If your codebase uses them — and most pre-2015 PHP applications do — every database call will fail on upgrade.
  • Strict typing. PHP 7+ enforces stricter type handling. Code that relied on implicit type coercion will produce different results or throw errors.
  • Error handling. PHP 7 changed fatal errors to Error exceptions. Your error handling code may catch nothing.
  • Namespace and autoloading changes. PSR-4 autoloading replaced the manual require_once chains that older applications depend on.

Running apt-get install php8.3 on a PHP 5.6 codebase will produce a wall of fatal errors. That's not a migration — it's a crash.

The incremental approach: strangler fig migration

The only approach that consistently works for production systems is incremental migration. You don't rewrite the application. You replace it, piece by piece, while it stays live.

Step 1: Set up dual runtime

Run PHP 5.6 and PHP 8.3 side by side on the same server or in parallel containers. Use your web server (nginx or Apache) to route requests to one version or the other based on URL path.

# Route new modules to PHP 8.3, everything else to PHP 5.6
location /api/v2/ {
    fastcgi_pass php83-fpm:9000;
    include fastcgi_params;
}

location / {
    fastcgi_pass php56-fpm:9000;
    include fastcgi_params;
}

This is the foundation. Both runtimes hit the same database, the same session store, the same file system. The user doesn't know which PHP version served their response.

Step 2: Pick your first seam

A seam is a piece of functionality you can extract from the old codebase and reimplement in the new one without changing anything else. Good first seams:

  • A read-only API endpoint. No writes means no risk of data corruption during the transition.
  • A reporting module. Usually self-contained, high read volume, low write volume.
  • Authentication. If you're planning to move to a framework like Laravel, start with auth — it touches everything, and modernizing it early makes every subsequent migration easier.

Step 3: Rewrite the seam in modern PHP

Build the replacement using a modern framework (Laravel, Symfony, or even plain PHP 8 with Composer dependencies). Write tests. Set up CI. This is your chance to do it right.

// Old: PHP 5.6, no framework, raw SQL
$result = mysql_query("SELECT * FROM orders WHERE customer_id = " . $_GET['id']);

// New: PHP 8.3, Laravel, Eloquent ORM
$orders = Order::where('customer_id', $request->input('id'))->get();

Step 4: Route traffic gradually

Start with 0% of traffic going to the new implementation. Use feature flags or percentage-based routing to shift traffic slowly: 1%, 5%, 25%, 50%, 100%.

Monitor error rates, response times, and data consistency at each step. If something breaks, roll back by routing 100% back to the old path.

Step 5: Delete the old code

Once the new implementation has handled 100% of traffic with zero issues for an agreed stability period (we typically use two weeks), delete the old code. Don't comment it out. Don't move it to a legacy/ directory. Delete it.

Handling the database

Database migrations are the hardest part of any PHP migration. The old and new code need to share data during the transition period.

Option 1: Shared database. Both PHP versions read and write to the same database. The new code uses the existing schema. This is simplest but means your new code inherits the old schema's problems.

Option 2: Schema evolution. Add new columns or tables alongside old ones. The new code writes to both old and new columns during transition. Once migration is complete, drop the old columns.

-- Add new column alongside old one
ALTER TABLE users ADD COLUMN email_verified_at TIMESTAMP NULL;

-- New code writes to both during transition
UPDATE users SET email_verified = 1, email_verified_at = NOW() WHERE id = ?;

-- After migration: drop old column
ALTER TABLE users DROP COLUMN email_verified;

Option 3: Database proxy. Use a tool like ProxySQL or PgBouncer to abstract the database layer. Both PHP versions connect through the proxy, and you can manage connection pooling and failover centrally.

What this looks like in practice

We migrated a logistics company's order management system from PHP 5.6 to PHP 8.3 over 14 weeks. The system processed 3,000 orders per day and could not take downtime.

The sequence:

  1. Weeks 1–2: Set up dual runtime. Deployed PHP 8.3 container alongside PHP 5.6.
  2. Weeks 3–5: Migrated the tracking API (read-heavy, well-defined boundary).
  3. Weeks 6–8: Migrated the order creation workflow (write-heavy, required database schema evolution).
  4. Weeks 9–11: Migrated the reporting module and admin interface.
  5. Weeks 12–14: Migrated authentication and session management. Retired PHP 5.6 container.

Zero minutes of downtime. The system was live and processing orders every day of those 14 weeks.

Common mistakes to avoid

1. Trying to migrate everything at once. This is a big-bang rewrite in disguise. It will take longer, cost more, and risk a failed cutover.

2. Skipping tests. If you're migrating without writing tests for the new code, you're just moving technical debt to a new runtime. Write tests alongside every migrated module.

3. Keeping the old code "just in case." Dead code is not insurance. It's maintenance burden. Delete it when the new code is proven.

4. Ignoring the session layer. PHP 5.6 and PHP 8.3 serialize sessions differently. If both runtimes share sessions (which they should during migration), you need a session store that both can read — Redis or database-backed sessions work well.


If your business is running on PHP 5.6 or 7.x and you need a migration plan that doesn't risk your production uptime, a system audit is where we start — mapping the codebase, identifying the seams, and designing a migration sequence that keeps your system live throughout.