Get CV
Back to business cases

Queue

Horizon jobs that carried the whole model

Redis filled with serialized Eloquent. The queue was slow because the payload was the catalog.

Horizon showed a healthy throughput until it did not. Failed jobs sat in Redis, retries piled up, and the queue container grew past 1 GB RSS. The jobs were small on paper: rebuild a catalog page. The payload was not.

Each job stored a serialized Product collection. Relations, casts, and three locales travelled with it. A retry wrote the same blob again. Redis maxmemory hit, then Horizon started losing the lock keys that the web workers needed.

The problem was a ticket that carried the catalog. Shop listings stalled while Redis evicted locks. I needed an id and a locale on the wire, not 1.4 MB of Eloquent on every retry.

Jobs belong in Redis. Catalog HTML does not. The payload should be an id list.
Jobs belong in Redis. Catalog HTML does not. The payload should be an id list.

What went on the wire

I dumped one reserved job. 1.4 MB of PHP serialize. Forty products, each with title JSON for en, ru, and de. The worker unserialized that graph, rendered HTML, wrote Memcached, and died. The next retry did it again from the same blob.

The job class had a public $products collection. Laravel serializes constructor arguments into Redis. A failed worker left that blob reserved. Redis kept copies for the retry window. That is how a rebuild queue ate maxmemory.

Ids, not models

The job receives page id and locale. It loads what it needs from MySQL, or from the compact DTO already in Memcached. Redis stores a few integers and a string.

  • Job timeout shorter than the Redis reservation.
  • Three tries, then the failed_jobs table, not an endless retry with a 1.4 MB payload.
  • Horizon --memory=128, so a fat unserialize kills the worker instead of the host.

Queue RSS dropped to the size of PHP plus Redis client buffers. Catalog rebuilds still run after writes. They just stopped shipping the catalog inside the ticket.

What I took from this

A queue payload is a ticket, not a cache. If the worker can load the row, the job should not carry the row.

Retries multiply size. A 1.4 MB serialize that fails three times is four copies in Redis plus the failed_jobs dump.

--memory=128 is a fence for a fat unserialize. The design is still an id list.

Back to business cases