Featured image of post A Bug Caused by Static Variables in Laravel Queues

A Bug Caused by Static Variables in Laravel Queues

Pitfalls in Long-running Processes

Environment

PHP_VERSION=7.4
laravel/framework: ^7.0

Static Variables

  • In many programming languages, static variables are defined as variables that:
    • Have the same lifecycle as the program
    • Are initialized only once
  • However, in PHP’s common runtime environment (php-fpm mode), processes get recycled after each request, so static variables don’t persist in memory (only effective during the current request)
  • Official PHP documentation states:

“Another important feature of variable scoping is the static variable. A static variable exists only in a local function scope, but it does not lose its value when program execution leaves this scope.” https://www.php.net/manual/zh/language.variables.scope.php

Preface

  • The project contained the following pseudo-code logic: Since json_data in the database is a JSON string, we used the static modifier to avoid repeated parsing
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class AttributeRequestLog extends Model
{
    public function getJsonData($key)
    {
        static $jsonData;
        if (is_null($jsonData)) {
            $jsonData = json_decode($this->attributes['json_data'], true);
        }

        return $jsonData[$key] ?? null;
    }
}

The code worked fine until we implemented queues for asynchronous tasks. After queue implementation, colleagues reported abnormal data. Log analysis revealed issues in queue task logging, which was ultimately traced back to this static variable usage.

  • Laravel queues run in CLI mode as long-running processes
  • Queue workers load code on startup and keep it in memory until process termination

Source Code Analysis

  • Queue startup command: php artisan queue:work
  • The entry file src\Illuminate\Queue\Console\WorkCommand.php extends Illuminate\Console\Command, running the handle method when executed


  • The worker retrieves queue driver configuration and passes a once parameter to determine if it should process a single job
  • Examining the daemon method in src\Illuminate\Queue\Worker.php:
  • The first three lines handle process signal listening
  • $lastRestart gets a timestamp from cache for graceful shutdown (reset by php artisan queue:restart)
  • Uses an infinite loop to keep the process alive
  • Checks for maintenance mode and other preconditions
  • getNextJob fetches the next task from queue driver (Redis, Database, etc.)
  • registerTimeoutHandler handles task timeout using async signals if supported
  • Processes jobs or sleeps when no tasks available

  • The fire method uses reflection to invoke job handlers
  • stopIfNecessary checks for termination conditions

  • The static variable $jsonData persists across multiple queue jobs
  • Since queue workers are long-running processes:
    • Static variables retain values between different job executions
    • Subsequent jobs may receive cached/stale data from previous executions
  • This caused data contamination between different queue tasks

Solution

  1. Avoid static variables in queue-related classes
  2. Use class properties with proper reset mechanisms
  3. Implement __destruct() to clear cached data
  4. Alternative implementation without static caching:
public function getJsonData($key)
{
    if (!isset($this->jsonDataCache)) {
        $this->jsonDataCache = json_decode($this->attributes['json_data'], true);
    }

    return $this->jsonDataCache[$key] ?? null;
}

public function __destruct()
{
    unset($this->jsonDataCache);
}

Key Takeaways

  • Static variables behave differently in long-running processes vs. request-response cycles
  • Queue workers maintain application state between jobs
  • Always consider process lifecycle when using:
    • Static variables
    • Singleton patterns
    • Class-level caching
  • Implement proper cleanup mechanisms for resource-intensive operations