Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Cloud Tasks Laravel Package

google/cloud-tasks

Official Google Cloud Tasks client library for PHP. Create and manage queues, dispatch asynchronous HTTP and App Engine tasks, configure retries and scheduling, and integrate with Google Cloud IAM and authentication through the Google Cloud PHP SDK.

View on GitHub
Deep Wiki
Context7

Getting Started

What you're actually getting. google/cloud-tasks is a thin, machine-generated GAPIC client over the Cloud Tasks REST/gRPC API. It is not a queue abstraction, worker, or scheduler — there is no dispatch(), no worker loop, and no retry logic in PHP. It gives you CloudTasksClient with typed request objects (Task, HttpRequest, AppEngineHttpRequest, Queue) and you own everything above that line. Mentally model it as "an HTTP request with durable retries, scheduling, and rate limits, produced via an API call."

Install and authenticate.

composer require google/cloud-tasks

Auth is Application Default Credentials only. Locally that means GOOGLE_APPLICATION_CREDENTIALS=/path/sa.json; on Cloud Run/GCE/GKE it's the metadata server and you pass nothing. The service account that creates tasks needs roles/cloudtasks.enqueuer (and roles/cloudtasks.queueAdmin if you manage queues in code). If you use OIDC tokens it also needs roles/iam.serviceAccountUser on the invoking service account.

First use case: push an HTTP job to a worker endpoint.

use Google\Cloud\Tasks\V2\CloudTasksClient;
use Google\Cloud\Tasks\V2\HttpMethod;
use Google\Cloud\Tasks\V2\HttpRequest;
use Google\Cloud\Tasks\V2\Task;

$client = new CloudTasksClient();                      // ADC, close() when done
$queue  = CloudTasksClient::queueName('my-project', 'europe-west1', 'default');

$http = (new HttpRequest())
    ->setHttpMethod(HttpMethod::POST)
    ->setUrl('https://worker.example.com/tasks/rebuild-search-index')
    ->setHeaders(['Content-Type' => 'application/json'])
    ->setBody(json_encode(['siteId' => 42]))
    ->setOidcToken([
        'service_account_email' => '[email protected]',
        // 'audience' => 'https://worker.example.com'  // defaults to the URL
    ]);

$task = (new Task())->setHttpRequest($http);

$created = $client->createTask($queue, $task);
echo $created->getName(); // projects/…/locations/…/queues/…/tasks/…

Where to look first. Not the PHP API docs — they're just proto field lists. Read cloud.google.com/tasks/docs for the semantics (retry config, dispatch deadlines, rate limits, IAM), then use Google\Cloud\Tasks\V2\CloudTasksClient for the method surface (createTask, createTaskAsync, getTask, listTasks, deleteTask, pauseQueue, resumeQueue, purgeQueue, updateQueue). Every RPC has an …Async twin and an $optionalArgs array for retrySettings/timeoutMillis.

Implementation Patterns

Wrap it behind your own interface, always. The client is concrete and hard to fake. Bind a small contract in a Laravel service provider and keep CloudTasksClient out of your domain code:

interface JobDispatcher {
    public function enqueue(string $handler, array $payload, ?int $delaySeconds = null): string;
}

The implementation builds the URL from config (config('tasks.base_url')), serializes the payload, and returns the task name. Tests bind an in-memory implementation that records calls; you get assertions on "was a job enqueued" without network access. This is the single highest-leverage decision in the whole package — everything else is proto plumbing.

Payloads as pointers, not blobs. Task size is small and headers count toward it. Send {"invoiceId": 8812} and let the handler re-read from the database. It also makes retries safe: the handler re-reads current state instead of replaying a stale snapshot.

Delays and scheduling. Two ways: ->setScheduleTime(new Google\Protobuf\Timestamp(['seconds' => time() + 300])) for an absolute time (this is your "run this in 5 minutes / tomorrow 09:00" primitive), or simply don't — Cloud Tasks dispatches immediately. Note scheduleTime respects the queue's taskTtl, so a far-future task can expire before it ever runs.

Idempotency via deterministic task names. Cloud Tasks deduplicates on task name, and a duplicate name returns ALREADY_EXISTS (gRPC code 6 / $e->getStatus() === 'ALREADY_EXISTS'). Derive the name from a business key and you get cheap dedupe:

$task->setName(CloudTasksClient::taskName(
    $project, $location, $queue, 'invoice-' . $invoiceId
));

Catch AlreadyExists and treat it as success. Verify the current name-reuse window in the docs before relying on this as a long-term lock — it's a dedupe window, not a permanent constraint.

Fan-out: use the async methods. There is no batch createTask. For 500 jobs, loop over createTaskAsync() and settle the promises; a sequential loop of 500 RPCs is seconds of wall time and a timeout waiting to happen.

$promises = [];
foreach ($items as $i => $item) {
    try {
        $promises[$i] = $client->createTaskAsync($queue, $this->buildTask($item));
    } catch (\Throwable $e) {
        report($e);
    }
}
foreach ($promises as $i => $p) {
    try {
        $p->wait();
    } catch (ApiException $e) {
        if ($e->getStatus() !== 'ALREADY_EXISTS') {
            Log::warning('enqueue failed', ['item' => $i, 'status' => $e->getStatus()]);
        }
    }
}

Configure the queue, not the caller. Retries, backoff, and throughput live on the Queue resource, so tune them once with updateQueue rather than inventing your own retry layer:

$queue = (new Queue())->setName($queueName)
    ->setRateLimits(new RateLimits([
        'max_dispatches_per_second' => 20,
        'max_concurrent_dispatches' => 10,
    ]))
    ->setRetryConfig(new RetryConfig([
        'max_attempts'   => 5,
        'min_backoff'    => new Duration(['seconds' => 1]),
        'max_backoff'    => new Duration(['seconds' => 300]),
        'max_doublings'  => 4,
    ]));
$client->updateQueue($queue, ['updateMask' => ['rate_limits', 'retry_config']]);

updateMask is required — omit it and you'll wipe fields you didn't set. This is the one place people lose queue config in production.

Receiver side: verify the token, then acknowledge fast. Your endpoint must validate the OIDC JWT (iss: https://accounts.google.com, aud = your URL, email = expected SA) — Google authenticates the call, it does not authorize your handler. Return 2xx to ack; return 4xx/5xx to trigger a retry. Because a 5xx retries, never return 5xx for "validation failed, will never succeed" — return 200 and log, or the task burns its attempts.

Pair with a TaskStatus lookup for debugging. getTask($taskName) still works after the task runs and returns lastAttempt.responseStatus / dispatchCount, which is the only way to see why a task kept failing after the fact (no response body is stored).

Gotchas and Tips

This is not a Laravel queue driver, and that's the point. Laravel's queue already does retries, backoff, and workers. Reach for Cloud Tasks when the consumer is an independent HTTP service (Cloud Run, App Engine, a partner endpoint), when you need per-queue rate limiting to protect a fragile downstream, or when you need server-side scheduling that survives your app being down. Don't build a Laravel job worker out of it — you'd be reimplementing queue:work with worse ergonomics.

Your handler must be publicly reachable. Cloud Tasks calls a URL over the internet. http://localhost:8000 and private VPC IPs are not targets. In development, tunnel (ngrok/cloudflared) or point at a staging deployment. There is no reliable local emulator for Cloud Tasks — don't plan your test suite around one.

A slow handler is a double-processed handler. dispatchDeadline (default 10 min, max 30 min for HTTP) expiring means the task is retried while your first run may still be executing. Combined with at-least-once delivery, every handler must be idempotent — unique constraints, upserts, "if already processed, return 200". This is the number one production incident with this package.

ALREADY_EXISTS is not an error. Wrap it. The same applies to PERMISSION_DENIED (usually the missing iam.serviceAccountUser on the OIDC account — cryptic message, easy to misdiagnose as queue permission).

Set the oneof, once. A Task needs exactly one of httpRequest / appEngineHttpRequest. Setting both (e.g. reusing a Task object across calls) throws at serialization time, not at set… time — the stack trace points at the transport, not your code. Build a fresh Task per enqueue.

Content-Type is your job. Cloud Tasks forwards headers verbatim; if you omit Content-Type: application/json, Laravel's $request->json() silently returns nothing and you'll debug the wrong layer.

Resource names are full paths, and the helpers exist. CloudTasksClient::queueName($project, $location, $queue) and taskName(...) save you from string-concatenation typos. The $parent argument is the queue name, not the project — a common mix-up.

Location matters for App Engine targets. The queue's location must match the App Engine app's region; HTTP targets are cross-region tolerant. Pick the region closest to your consumers at queue-creation time — it is not mutable.

purgeQueue is instant and irreversible. It deletes all tasks in the queue and is itself rate-limited. Never call it from a code path a human can hit casually; gate it behind an explicit admin command.

Duration and Timestamp are protobuf types. They come from google/protobuf, use seconds/nanos array constructors, and are not \DateInterval. Set them wrong and updateQueue fails with a validation error that names neither field.

Debugging checklist, in order: getTask() for dispatchCount and responseStatus → queue state (PAUSED/DISABLED queues don't dispatch) → IAM on the OIDC service account → dispatchDeadline vs handler runtime → receiver logs. Nine times out of ten it's the IAM binding or a 5xx from a non-idempotent handler.

Extension points are limited by design. The generated client supports $optionalArgs['retrySettings'], timeoutMillis, and a custom transport/credentials — that's about it. Anything resembling middleware belongs in your wrapper class, which is another reason to build one on day one.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource