Laravel

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:

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 command to contain the data required for your job (in my case, these are the job class constructor parameters).

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!

← Back to all posts