Cache file 'double content' causes PHP error across entire site

Summary

Snippet cache files intermittently get ‘double content’ when MODx recreates them after their expiry time. This causes a PHP parsing error which takes down the whole site until the offending file is manually edited/deleted.

Steps to reproduce

It happens once every couple of weeks organically. Annoyingly, despite repeated attempts (deleting cache files + recreating by visiting site) I have been unable to replicate the issue manually.

Observed behavior

An example of one of the files that was updated with ‘double content’:

<?php if(time() > 1752680198){return null;} return array (
  'remembered' => 1060167,
  'total' => '1060174',
  'figure' => 5.0,
);<?php if(time() > 1752680198){return null;} return array (
  'remembered' => 1060167,
  'total' => '1060174',
  'figure' => 5.0,
);

The PHP error given was:

Parse error: syntax error, unexpected ‘<’, expecting end of file in /var/www/web2/html/core/cache/right_panels/total-tributes.cache.php on line 5

This behaviour started mid-July 2025. We haven’t made changes to the relevant code at all, and MODx version/add-ons/environment etc were last changed long before this issue started occurring.

Environment

MODx 3.1.2-pl, nginx 1.41.1, PHP 7.4.33, MySQL 8.0.43, Chrome Windows (latest).

General Thoughts & Musings

I’ve tried regular (3x weekly) cache clears from the MODx manager with no success.

So far the issue only affects one file at a time, and I have only seen this behaviour on cache files in a specific partition (‘right_panels’).

The snippets involved all make SQL queries via xPDO to grab a large amount of DB data, then attempt to cache it for future usage on the frontend. However, there’s no evidence of those SQL queries failing or timing out. When I run the same SQL queries manually they complete quickly enough every time.

I’ve stopped short of more drastic tests (disabling cache entirely, removing the snippets from site build, etc) due to the performance and functionality issues that would bring.

Due to the intermittent nature of the issue I’ve struggled to nail it down!

Can anybody help shed some light on what might be causing the ‘double content’ in the files?

So the defective cache files all get created by the same custom snippet?

To create the cache files in the snippet code, do you use the normal MODX functions ($modx->cacheManager->get(), $modx->cacheManager->set(), …) or did you implement a custom solution?

The defective cache files are created by a small handful of snippets. The snippets all use the same cache partition.

They all do some form of DB work, ranging from a quick grab of a COUNT(*) to more complex (but still straightforward) queries. As stated previously though, I can’t get any of these queries to fail when tested manually, and there’s no evidence of the DB server going away etc.

We’re using the normal MODx functions throughout the site, including to display cached results on the frontend.

To me it doesn’t look like the snippet failing is the issue here.
It looks more like the same snippet gets somehow executed twice in a row and (maybe because of some caching delay) the result gets written twice.

Does the defective file always contain exactly the same content twice?
Can you maybe share the part of your code that checks if a cache file already exists, and the part that writes the cache file?
Did your hosting provider maybe make some changes to caching settings recently, that could have caused the issue?

Yes, always exactly the same content twice, including the timestamp check against time().

The hosting provision is bespoke so they advise of any change well in advance, we run tests in various environments etc. So unlikely they’ve made a change but I will ask. It’s all behind a load balancer but that hasn’t been an issue at any point previously.

An example of one of the snippets that’s had issues:

<?php
	$options = array(
	  xPDO::OPT_CACHE_KEY => 'right_panels'
	);

	// Test if cached

	$cached = $modx->cacheManager->get('commemorations-left', $options);

	if ($cached) {

		if ($showtotal) {
			$output = $cachedtotal;
		} else {
			$output = $cached;
		}

	} else {

		$statement = $modx->prepare("SELECT COUNT(*) from casualties");
		$statement->execute();
		$remaining = $statement->fetchColumn();

		$statement = $modx->prepare("
			SELECT COUNT(DISTINCT id) 
			FROM casualties cas
			WHERE cas.id NOT IN (SELECT casualty_id FROM commemorations) 
	     	AND cas.id NOT IN (SELECT casualty_id FROM poppies) ");
		$statement->execute();
		$tributesLeft = $statement->fetchColumn();
		
		$output = $tributesLeft;
		$modx->cacheManager->set('commemorations-left', $output, 900, $options);


	}

	return "<span>".number_format($output)."</span>";

While this snippet is a little messy, I can’t see anything obviously ‘doubled’, if that makes sense. We’re certainly not calling set() on cacheManager twice or anything like that.

Most likely the “flock mechanism (locking files for writing) does not work reliably” on your system.

Take a look at these (similar) issues for a solution:

This is quite hacky, but it might improve reliability.

In your else section, pause, then check again for it being cached before writing with:

sleep(1);
$cached = $modx->cacheManager->get('commemorations-left', $options);
if (! $cached) {
    /* write the cache file here */
}

If it works, you could try a shorter sleep time with usleep.

I agree with hth that flock failure is the likely cause, and a custom flock solution is probably a better option if you can get it to work.

A flock issue does sound likely. Appreciate the thoughts and links @halftrainedharry and @bobray, and thanks for your time on this.

I’ll give these things a go and report back in time, hopefully with news of a successful fix!