Day 4: State Safety: Analyzing static memory in long-living processes.

Designing and Building a High-Scale PHP Content Management System ~8 min read 60 min hands-on

State Safety: Analyzing static memory in long-living processes

Welcome back, fellow architects and engineers! Yesterday, we laid the groundwork for our high-scale CMS by getting our hands dirty with the Goridge protocol, crafting the heart of our worker loop. We saw how PHP, traditionally a "shared-nothing" language, can transform into a persistent, long-living process, shedding the infamous "bootload tax" that plagues traditional PHP-FPM setups.

Today, we're diving into a topic that, while seemingly innocuous, is a silent killer in high-scale persistent PHP systems: state safety, specifically concerning static memory. This is where the rubber meets the road between understanding how PHP used to work and how it must work in a modern, persistent runtime like RoadRunner. Miss this, and you'll be chasing phantom bugs for weeks, wondering why your carefully designed system is leaking memory or, worse, serving up one user's data to another.

The Elephant in the Room: Persistent PHP and Static Memory

In the traditional PHP-FPM model, every request starts with a fresh PHP process. This is wonderfully simple: global variables, static variables, and the entire memory space are wiped clean. You get a blank slate, every single time. This stateless nature is PHP's superpower for horizontal scaling.

However, when we adopt persistent runtimes like RoadRunner, that paradigm shifts fundamentally. Your PHP worker process, once booted, stays alive across hundreds, thousands, or even millions of requests. This persistence is precisely what eliminates the bootload tax, making 100M RPS feasible.

But here's the catch: static variables and global state within that long-living PHP process are no longer reset between requests. They persist.

Imagine a scenario:

php

In PHP-FPM, $currentCount would always be 1 for every request, because the process restarts. In a persistent RoadRunner worker, $currentCount would increment with every single request handled by that specific worker. This might seem benign for a counter, but what if it was:

  • A static cache of user permissions?

  • A static database connection that gets closed unexpectedly?

  • A static configuration array loaded from the first request?

  • A static variable holding sensitive user data from a previous request?

This isn't theoretical; this is how major outages and insidious data breaches happen in systems not designed with persistent state in mind. It's the kind of subtle bug that only manifests under specific load patterns or after a worker has handled many requests.

Core Concepts: The Perils and Protections of Persistent State

  1. Data Contamination: The most immediate and dangerous risk. If a static variable holds data specific to User A and isn't reset, the next request, potentially from User B, might inadvertently access or modify User A's data. This is a severe security and data integrity nightmare.

  2. Memory Leaks: Objects or large data structures stored in static variables or global arrays accumulate over time. Since the process doesn't restart, this memory is never freed, eventually leading to the worker consuming excessive RAM and crashing (Out of Memory - OOM). At 100M RPS, even a tiny leak per request quickly becomes a catastrophic problem across thousands of workers.

  3. Unexpected Behavior & Race Conditions: Static variables can introduce implicit dependencies and race conditions that are incredibly hard to debug. One request might modify a static variable, unknowingly affecting subsequent requests processed by the same worker, leading to unpredictable outcomes.

The Architect's Toolkit: Taming Persistent State

Keep going with the full lesson

The rest of this lesson β€” plus its implementation guide, runnable scripts and source code β€” is included with the course or an All Access plan. Create a free account to start the free lessons in every course.

Already a member? Sign in