Skip to main content
Article

A Green Deploy Still Serving the Previous Release

5 min read 58 views

Deployer finishes, every task is green, and the fix you just shipped is not on the site. You refresh, it is still broken, and you start looking for what went wrong with the fix. Often nothing did. The server is still serving the previous release, and it will stop on its own in a minute or two. We have reached the wrong conclusion from that window more than once, on more than one of our own Yii2 sites, and this post explains where it comes from and how we now close it.

How a symlink deploy goes live

Deployer builds each release in its own directory and then switches a symlink, current, to point at it. The web server's document root is current/web. The switch is atomic, which is why this style of deploy has no downtime: one moment requests resolve to the old release, the next to the new one.

That is true for the filesystem. It is not true for PHP, which keeps two caches of its own in every worker process:

  • The realpath cache remembers what a path resolved to. With realpath_cache_ttl=120 on our server, a worker can keep resolving current to the old release directory for up to two minutes after the symlink has moved.
  • OPcache keeps compiled bytecode in memory. With opcache.revalidate_freq=60 it checks a file's timestamp at most once a minute.

Together they can keep serving the previous release for about two minutes after a deploy that reported success.

What it cost us

On one of our sites, in one session in August 2026, this produced two confident and wrong conclusions: an HTTPS redirect reported as still broken, and a disabled OAuth login reported as still on offer. Both fixes had in fact shipped; we were checking inside the window.

The step that looks like it should help does not. Our deploys run ./yii cache/flush-all, and that clears the application's data cache, not PHP's realpath cache or OPcache.

It can also make things worse. On pinuno.com.gh the cache flush ran before the symlink switch, so for the length of the window the old release was still serving traffic and filling the freshly emptied cache with old pages. One crawler request for /sitemap.xml in that gap cached the retired sitemap's output for its full 24-hour lifetime, and the new code served the old bytes until someone flushed the cache by hand. The fix was a second flush after the switch.

Reload the process that is actually serving

The direct way to end the window is to make the PHP workers start again. Our language-learning app added a deploy task in June 2026 that reloads php8.3-fpm after the symlink switch, so stale bytecode is discarded straight away.

pinuno.com.gh had a task with the same name and the same reload, and it did nothing useful, because that site does not run under PHP-FPM at all. It runs under mod_php in Apache, so the processes holding the old caches were Apache children, and reloading FPM left every one of them untouched. The task also swallowed errors, so it always looked successful. After one release the course listing kept serving the old view for about a minute although the "reload" had run. Since 30 September 2026 the task runs apache2ctl configtest and then a graceful Apache reload, with no || true to hide a failure. A graceful reload keeps the listening sockets open and lets each child finish the request it is serving, so no visitor gets an error.

Reloading is not free on a shared server. When we looked at doing the same on that site, it would have cleared OPcache for every site on the host, a cold start for all of them on each deploy of one. We decided that was not one project's call to make. That site documents the window instead, and its verification step reads the deployed file on the server before trusting a request to the live site.

Prove the new release is serving

A reload that runs is still not proof. What we wanted was a deploy that fails if the old release is still answering, so pinuno.com.gh now ends with a revision check:

  • A small endpoint returns the contents of the release's REVISION file as plain text, uncached, and only with a health-check token (without a configured token it returns 404).
  • After the reload, the deploy makes 12 requests to it per round and requires all 12 to return the revision it has just deployed.
  • It tries up to 10 rounds, two seconds apart. If the workers still disagree, the deploy fails and prints the recovery commands.

Twelve requests rather than one, because each request may be served by a different worker, and a single worker holding the old release is enough to show some visitors the old site.

ApproachEnds the windowCost
Wait about two minutes before checkingEventuallyEvery check in the window can mislead
Reload the process that serves PHPImmediatelyCold caches; on a shared host, for every site
Revision check after the reloadProves it endedA small token-guarded endpoint

Lessons

  • A green deploy proves the tasks ran, not that the new code is answering requests.
  • Know which process serves your PHP before you reload it. A reload of the wrong service succeeds and changes nothing.
  • Flush application caches after the switch, or old code will refill them.
  • When you must verify by hand, read the file on the server first; a request to the live site is the slower signal.
  • Better still, let the deploy verify itself, and fail when it cannot.

If you want to learn to build and deploy PHP and Yii2 applications like these, have a look at our PHP Application Developer course.

K

Kwame Asante

Related Articles

Article Info

Type
Article
Category
Web Development
Published
11 Oct 2026
Reading time
5 min

Search

Put this into practice

Our structured courses take these topics from article to hands-on skill, taught by the practitioners who write them.

Browse Courses