Why sending full models to queues kills your Redis memory.
This is one of the most common production failures in Laravel. Developers dispatch jobs with full models, everything works in development (small data), then in production with 100,000 jobs, Redis memory explodes, workers hang, and the entire queue system collapses.
When you dispatch a job with a Model:
ProcessUserJob::dispatch($user);
Laravel serializes:
If your model has loaded relationships (e.g., $user->load('posts', 'comments')), Laravel serializes every related model recursively. One user with 100 posts and 500 comments = megabytes of serialized data per job.
ProcessUserJob::dispatch($user);
| Scenario | Payload Size |
|---|---|
| User only (no relations) | 2-10 KB |
| User + posts (100 posts) | 50-100 KB |
| User + posts + comments (500 comments) | 500 KB - 2 MB |
ProcessUserJob::dispatch($user->id);
| Scenario | Payload Size |
|---|---|
| Any user | 4-8 bytes (integer) |
| User + any relations | 4-8 bytes |
| Approach | Payload per Job | Total for 100k Jobs | Redis Memory |
|---|---|---|---|
| Full User Model (no relations) | 5 KB | 500 MB | β οΈ Near limit |
| Full User + Posts + Comments | 500 KB | 50 GB | π CRASH |
| ID Only | 8 bytes | 0.8 MB | β Fine (0.1% of memory) |
Sending ID only is 99.99% smaller than sending full models with relations. In production, this difference is the line between a working queue and a crashed server.
Never dispatch a model. Always dispatch the ID.
// Controller
$user = User::find(123);
ProcessUserJob::dispatch($user); // β Serializes everything
// Job
class ProcessUserJob implements ShouldQueue
{
public function __construct(
public User $user // β Receives full model
) {}
public function handle()
{
// Use $this->user directly
$this->user->update(['processed' => true]);
}
}
// Controller
$user = User::find(123);
ProcessUserJob::dispatch($user->id); // β
Serializes integer
// Job
class ProcessUserJob implements ShouldQueue
{
public function __construct(
public int $userId // β
Receives integer
) {}
public function handle()
{
// Fetch fresh model inside job
$user = User::find($this->userId);
$user->update(['processed' => true]);
}
}
Common objection: "What if the user changes between dispatch and job execution?"
Answer: That's exactly what you want! The job should use the current state of the user, not the state from when the job was dispatched. Fetching fresh inside handle() ensures you have the latest data.
If you NEED the state at dispatch time, explicitly serialize only the fields you need:
ProcessUserJob::dispatch($user->only(['id', 'name', 'email']));
This innocent-looking code will kill your queue:
// Controller
$user = User::with(['posts.comments', 'profile', 'settings'])->find(123);
SendNewsletterJob::dispatch($user); // π Serializes EVERYTHING
// Job
class SendNewsletterJob implements ShouldQueue
{
public function __construct(public User $user) {}
public function handle()
{
foreach ($this->user->posts as $post) {
foreach ($post->comments as $comment) {
// Process comment
}
}
}
}
This will serialize the user, all their posts, all comments on those posts, their profile, and their settings. One user could be 10MB+ of serialized data. 10,000 such jobs = 100GB of Redis memory = guaranteed crash.
// Controller
$userId = 123;
SendNewsletterJob::dispatch($userId); // β
Only the ID
// Job
class SendNewsletterJob implements ShouldQueue
{
public function __construct(public int $userId) {}
public function handle()
{
// Fetch WITH relations INSIDE the job
$user = User::with(['posts.comments', 'profile', 'settings'])
->find($this->userId);
// Now process
foreach ($user->posts as $post) {
foreach ($post->comments as $comment) {
// Process comment
}
}
}
}
Now the job fetches fresh data when it runs. No massive serialization in Redis.
| Driver | Default Max Payload | Risk with Full Models |
|---|---|---|
sync (testing) |
No limit (in-process) | Low risk (no serialization) |
database (jobs table) |
~65KB (TEXT column) | β οΈ HIGH RISK β models with relations exceed 65KB |
redis |
~512MB (configurable) | Memory, not size limit |
sqs (AWS) |
256KB | β οΈ HIGH RISK β exceeds limit, jobs fail silently |
AWS SQS has a 256KB hard limit per message. A User model with 50 posts and 200 comments can easily exceed this. The job will fail with no clear error message. Always use ID-only with SQS.
| Approach | Payload Size (100k jobs) | Redis Memory | Safety |
|---|---|---|---|
| Full User Model (no relations) | 500 MB | High | β οΈ Risk |
| Full User + Relations | 50+ GB | π CRASH | π Never do this |
| ID Only | 0.8 MB | Minimal | β Always safe |
Topic 7: Service Container & Reflection Cost β Why auto-wiring is expensive. The difference between singletons and transient services. How explicit binding saves CPU.