A real-world debugging story about Memcached key collisions, caching layers, and why the same bug can hide for months — until it doesn’t.
March 2026 · OJS-Services.com
If you’re managing multiple OJS journals on the same server, here’s a scenario that might sound familiar: you update the current issue on one journal, and a few minutes later, a completely different journal’s homepage is showing that same issue. Or worse — users are seeing a journal they’ve never heard of, with a ‘View All Issues’ link that goes nowhere.
We ran into exactly this. Multiple OJS 3.3.x installations on a shared hosting environment, all seemingly independent, but quietly contaminating each other’s data through a shared Memcached instance. Here’s what happened, how we tracked it down, and what actually fixed it.
The Setup
We manage a portfolio of academic journals running on Open Journal Systems (OJS). Several of them are hosted on the same server — each with its own subdomain, its own database, its own OJS installation. Completely separate, or so we thought.
The server runs SiteGround’s shared hosting stack. SiteGround offers several caching layers out of the box:
- NGINX Direct Delivery — static file serving for images, CSS, JS
- Dynamic Cache — full-page HTML caching
- Memcached — object-level caching, shared across all sites on the account
That last one is the key detail. Memcached runs as a single shared service at 127.0.0.1:11211 — and every OJS installation on the account was configured to use it.
What We Saw
The first symptom was a PHP Fatal Error appearing on the homepage of one journal:
Fatal error: Call to a member function getPublished() on null
in classes/journal/SectionDAO.inc.php on line 334
The error trace pointed to getByIssueId(18). Except — issue_id 18 didn’t exist in that journal’s database. The highest issue_id was 11.
So where was 18 coming from?
We checked everything: the issues table, publication_settings, journal_settings, url_path fields. Nothing. The database was perfectly correct. The right current issue was set. Yet OJS was trying to load issue 18 on every homepage request.
This is one of those bugs that PHP 8.0 exposed rather than created. On PHP 7.x, calling a method on null returns null and moves on. On PHP 8.0, it throws a Fatal Error. The underlying problem existed before — it just wasn’t visible.
The Cache Trail
After ruling out database issues, we turned to caching. We deleted the entire /cache/ directory. The error came back immediately on the next request. We cleared SiteGround’s Dynamic Cache. Same result — gone for a moment, then back.
Then we tried something almost accidental: switching the journal’s theme in the OJS admin panel. The error disappeared. We switched back to the original theme. It stayed fixed — for a while.
The reason? Switching themes triggers OJS’s internal cache flush, which includes Memcached. The stale data was cleared. But the next time OJS populated the cache, it pulled the wrong data again.
This pointed clearly at Memcached. The object cache was storing the wrong issue_id, and no amount of file cache clearing would touch it.
The Real Problem: Key Collisions
Here’s what was actually happening. OJS builds Memcached cache keys like this (from MemcacheCache.inc.php):
$this->getContext() . ‘:’ . $this->getCacheId() . ‘:’ . $id
For the current issue cache, this produces a key like:
issues:current:1
The 1 at the end is the journal_id. And here’s the problem: every fresh OJS installation assigns journal_id = 1 to the first journal. All three of our affected installations had journal_id = 1.
So Journal A, Journal B, and Journal C were all reading and writing to the exact same Memcached key. When Journal C updated its current issue, that value was written to issues:current:1. When Journal A loaded its homepage, it read that same key — and got Journal C’s data.
There is no site-specific prefix in OJS 3.3.x’s Memcached implementation. If two installations share the same Memcached instance and both have journal_id = 1 (the default), they will silently share cached data.
The Caching Layer Stack
Before we get to the fix, it’s worth stepping back and looking at the full picture. Modern hosting environments — especially managed ones like SiteGround — can have three or four independent caching layers operating simultaneously:
1. OJS Internal Cache (File-based)
OJS writes cache files to its own /cache/ directory. Clearing this is the first thing most people try. It has no effect on Memcached-stored data.
2. OJS Object Cache (Memcached)
Configured in config.inc.php under [cache]. This is where things like current issue data are stored. If you’re using Memcached on a shared hosting account, this cache is shared at the server level across all sites. This was the source of our problem.
3. Server-Level Dynamic Cache (SiteGround SuperCacher)
SiteGround’s Dynamic Cache stores the complete HTML output of pages. It’s fast, but it can serve stale content even after the database or OJS cache is updated. For OJS journals, we recommend disabling this entirely. OJS has its own caching logic; layering a full-page cache on top creates unpredictable behavior.
4. CDN Cache (Cloudflare)
If your journals are proxied through Cloudflare, you have yet another caching layer in front. Depending on your page rules, Cloudflare can cache HTML pages too. In our setup, some domains were behind Cloudflare — which added another variable to the debugging process when things didn’t clear as expected after a fix.
The tricky part with multiple caching layers is that clearing one doesn’t clear the others. You might flush OJS’s file cache, confirm the database is correct, but Cloudflare is still serving a page from three hours ago. Or SiteGround’s Dynamic Cache is returning a stored response generated before your fix. Always clear in the right order: OJS cache first, then server cache, then CDN.
When something looks wrong on an OJS site and you’ve already confirmed the database is correct, the question becomes: which cache layer am I actually looking at right now?
What Didn’t Work
For the record — things we tried that had no effect on the actual collision:
- Deleting all /cache/ files: Only affects file-based cache, not Memcached
- Adding memcache_key_prefix to config.inc.php: This option exists in the config template but is not read by MemcacheCache.inc.php in OJS 3.3.x. It does nothing.
- Clearing Cloudflare cache: Not the source
- Clearing SiteGround Dynamic Cache: Temporarily removed stale HTML, but Memcached repopulated on the next request
- Updating the current issue via database: Correct in DB, but immediately overridden by the stale Memcached value
The Fix
The solution was a one-line addition to MemcacheCache.inc.php in each affected installation. In the constructor, before adding the server, we set a site-specific prefix using Memcached’s OPT_PREFIX_KEY option:
$this->connection = new Memcached;
// Add this line:
$this->connection->setOption(Memcached::OPT_PREFIX_KEY, md5(__FILE__) . ‘_’);
$this->connection->addServer($hostname, $port);
The md5(__FILE__) generates a unique hash from the absolute filesystem path of MemcacheCache.inc.php. Since each OJS installation lives in a different directory, the hash is automatically unique per installation — no manual configuration needed.
After applying this to all three affected installations and flushing Memcached from the SiteGround panel, the cross-site contamination stopped completely.
We also added a null guard to SectionDAO.inc.php to prevent the PHP 8.0 Fatal Error in case a stale cache entry ever surfaces again:
$issue = Services::get(‘issue’)->get($issueId);
if (!$issue) return []; // Don’t crash on invalid/stale issue_id
Why OJS 3.4.x Wasn’t Affected — What the Code Tells Us
The following is based on direct inspection of OJS 3.4.x source files. These are observations and reasoned conclusions, not claims we can fully verify without PKP team confirmation.
We had OJS 3.4.x installations running on the same server, with the same journal_id = 1, using the same Memcached instance — and they weren’t experiencing the collision. We wanted to understand why before writing this up.
Looking at the OJS 3.4.x source, the answer appears to be that the Memcached cache path for issue data has been intentionally disabled. In classes/issue/Repository.php, the getCurrent() function contains this comment:
// TODO: Caching as currently setup never properly caches objects
// and always fires a _cacheMiss()
// if ($useCache) {
// $cache = $this->dao->_getCache(‘current’);
// return $cache->get($contextId);
// }
The Memcached code is commented out. In classes/issue/DAO.php, cache flush calls are similarly disabled with TODO notes. This suggests the PKP team was aware of caching problems during the 3.4.x rewrite and chose to disable this code path temporarily, pending a proper fix.
The result is that OJS 3.4.x reads the current issue directly from the database on every request — no Memcached involved, no collision possible. It’s not that the underlying key collision problem was architecturally solved; it’s that the affected cache path simply isn’t executing.
If Memcached is re-enabled in a future OJS 3.4.x version without adding site isolation, the same collision would likely reappear. The OPT_PREFIX_KEY fix should be applied proactively.
Recommendations
If you’re running multiple OJS 3.3.x installations on the same server with Memcached:
- Apply the MemcacheCache.inc.php prefix fix to every installation sharing the Memcached instance
- After applying the fix, flush Memcached from your hosting panel to clear any stale cross-site data
- Disable SiteGround Dynamic Cache for all OJS subdomains — it conflicts with OJS’s own caching
- If using Cloudflare, bypass cache for dynamic paths (/index.php/*) and only cache static assets
- When troubleshooting: clear OJS cache first, then server cache, then CDN — in that order
For CMS and publishing platforms with their own internal caches, server-level full-page caching (Dynamic Cache, Varnish, etc.) often causes more problems than it solves. Leave object caching to the application; use the CDN for static assets.
Wrapping Up
Cache bugs are among the most disorienting to debug because the evidence keeps disappearing. The database looks right, the admin panel looks right, but the site is showing something wrong. When you’re dealing with a multi-layer cache stack and multiple installations sharing infrastructure, the number of possible interactions multiplies fast.
In our case, the bug had probably been present for months, quietly hiding behind PHP 7.x’s silent null handling. The PHP 8.0 upgrade made it impossible to ignore. Sometimes an upgrade doesn’t introduce bugs — it just makes existing ones visible.
We’ve submitted a detailed bug report to the PKP/OJS team with the full analysis and reproduction steps. If you’re running a similar multi-site OJS setup and seeing strange homepage behavior, Memcached key collisions should be high on your list of suspects.
OJS-Services.com
We manage 500+ academic journals on Open Journal Systems. When we run into edge cases like this, we write them up — because someone else is probably dealing with the same thing.
The post When Your OJS Journals Start Showing Each Other’s Content first appeared on OPEN JOURNAL SYSTEM SERVICES.