Rather than coordinating who recomputes, let each reader decide independently with a probability that rises as the expiry approaches — so one request refreshes early and the rest keep using the value that is still there.
$payload = json_decode($redis->get($key), true);
$expiresAt = $payload['expires_at'];
$delta = $payload['compute_ms'] / 1000;
$beta = 1.0;
// XFetch: recompute early, with rising probability
if (time() - $delta * $beta * log(mt_rand() / mt_getrandmax()) >= $expiresAt) {
$value = $this->recompute();
}
It needs the value to carry its own expiry and how long it took to compute, so the key holds a small envelope rather than the bare value. The appeal is that there is no lock, no coordination and no waiting — the more expensive the computation, the earlier a refresh becomes likely. The trade is that occasionally two requests recompute, which is far cheaper than two hundred.