How to Dispatch Laravel Jobs from external service
Laravel's queue system is incredibly powerful. You can use message batching, unique jobs, rate limits, overlap prevention, and delayed dispatching right out of the box even if your queue engine misses these features. I think many developers love this simplicity - until they try to use Laravel queues to communicate with external services.
In this post, I will explain how to push jobs to a Laravel queue from an external service (such as Go) without doubling the connection count, struggling with PHP serialization, or exposing internal Laravel classes - all while retaining the full advantages of the framework.
To demonstrate this, we will use Laravel, Redis, and Go.
Let's cook
First, we need to prepare Laravel to handle payloads published by external services. By default, Laravel uses a specific format that is quite "heavy" because the external service must be aware of Laravel's internal classes, php serialization and numerous parameters.
The standard Laravel payload looks like this:
[
'uuid' => (string) Str::uuid(),
'displayName' => $this->getDisplayName($job),
'job' => 'Illuminate\Queue\CallQueuedHandler@call',
'maxTries' => $this->getJobTries($job),
'maxExceptions' => $job->maxExceptions ?? null,
'failOnTimeout' => $job->failOnTimeout ?? false,
'backoff' => $this->getJobBackoff($job),
'timeout' => $job->timeout ?? null,
'retryUntil' => $this->getJobExpiration($job),
'data' => [
'commandName' => get_class($job),
'command' => serialize($job),
'batchId' => $job->batchId ?? null,
],
'createdAt' => Carbon::now()->getTimestamp(),
]
Fortunately, we aren't required to pass all of these if we don't need features like backoff or max tries. The essential parameters are:
- uuid: A unique identifier for the queued message.
- job: The job handler; defines which class processes the message.
- data: The actual payload passed from the external service.
- attemts: How many times to attempt the job before it fails.
Additionally, displayName and
createdAt are used for logging. I recommend
including them to ensure your logs remain accurate and
readable.
The Implementation Steps
Since we know the expected structure, we can prepare Laravel to handle it. We need to create a custom job handler to determine the correct job class. This is necessary because the default CallQueuedHandler only works with serialized PHP classes.
1. Let’s create our own handler
CallExternalQueuedHandler:
<?php
namespace App\Queue;
use Illuminate\Queue\CallQueuedHandler;
use RuntimeException;
use Throwable;
class CallExternalQueuedHandler extends CallQueuedHandler
{
protected function getCommand(array $data)
{
if (empty($data['commandName'])) {
throw new RuntimeException('Unable to extract job.');
}
$handler = app()->getAlias($data['commandName']);
$params = $data['command'] ?? [];
try {
return new $handler(...$params);
} catch (Throwable $e) {
throw new RuntimeException('Unable to extract job payload.', previous: $e);
}
}
}
In this setup, $data contains everything passed
from the external service. We expect
commandName to be a Laravel class alias,
and
2. Create a simple Laravel job for testing purposes.
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class TestJob implements ShouldQueue
{
use Queueable, InteractsWithQueue;
public function __construct(private string $id) {}
public function handle(): void
{
Log::info('Test job successfully executed', ['id' => $this->id]);
}
}
3. Register an alias for the job class in your
AppServiceProvider
public function register(): void
{
$this->app->alias(TestJob::class, 'test-job');
}
4. Write the Go code to publish messages into the Laravel queue.
The New Wave of Tools
package main
import (
"context"
"fmt"
"log"
"time"
"encoding/json"
"github.com/redis/go-redis/v9"
"github.com/google/uuid"
)
type CommandData struct {
ID string `json:"id"`
}
type JobData struct {
CommandName string `json:"commandName"`
Command CommandData `json:"command"`
}
type Payload struct {
UUID string `json:"uuid"`
Job string `json:"job"`
DisplayName string `json:"displayName"`
Data JobData `json:"data"`
Attempts int `json:"attempts"`
CreatedAt int64 `json:"createdAt"`
}
func main() {
ctx := context.Background()
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
})
now := time.Now().Unix()
payload := Payload{
UUID: uuid.New().String(),
Job: "App\\Queue\\CallExternalQueuedHandler@call",
DisplayName: "Test job",
Attempts: 0,
CreatedAt: now,
Data: JobData{
CommandName: "test-job",
Command: CommandData{ID: "45180d91-b763-47a0-9e17-1377f8811f1e"},
},
}
jsonPayload, err := json.Marshal(payload)
redisPrefix := "laravel"
queueName := "default"
queueKey := redisPrefix + "-database-queues:" + queueName
err = rdb.RPush(ctx, queueKey, jsonPayload).Err()
if err != nil {
log.Fatalf("Failed to push to Redis: %v", err)
}
fmt.Println("Successfully pushed job to Redis!")
5. Start the Laravel queue worker.
php artisan queue:work
6. Run the Go code to push a message.
go run main.go
And that’s it! The job is successfully executed without the overhead of PHP serialization.
[2026-02-18 23:22:00] local.INFO: Test job successfully executed {"id":"45180d91-b763-47a0-9e17-1377f8811f1e"}
I hope you enjoyed the process!