Cloud Tasks
Lightweight, dependency-free fake of Google Cloud Tasks that speaks the real Cloud Tasks v2 REST API (https://cloudtasks.googleapis.com/v2), so application code using @google-cloud/tasks can run against it with zero cost and zero side effects.
| Key | Value |
|---|---|
| Port | 4584 |
| Protocol | Cloud Tasks v2 REST API (HTTP + JSON) |
| Compatible client | @google-cloud/tasks (v6, google-gax v5) |
| Size | ~60 KB |
| Startup | < 100ms |
| State | In-memory, ephemeral, resettable |
Quick Start
Start the server:
import { CloudtasksServer } from "./services/cloudtasks/src/server.js";
const server = new CloudtasksServer(4584);
await server.start();
// ... use it ...
await server.stop();
Connect with the real @google-cloud/tasks client. The fake speaks the
HTTP/1.1 REST transport (the google-gax fallback mode), so the low-level
gapic CloudTasksClient must be constructed with fallback: true,
protocol: "http", and an explicit apiEndpoint + port pointing at the fake:
import { v2 } from "@google-cloud/tasks";
const client = new v2.CloudTasksClient({
projectId: "parlel",
fallback: true, // use the HTTP/1.1 REST transport instead of gRPC
protocol: "http", // talk plain HTTP to the local fake
apiEndpoint: "127.0.0.1",
port: 4584,
// Any credentials work — the fake never verifies them.
});
const parent = client.locationPath("parlel", "us-central1");
const queueName = client.queuePath("parlel", "us-central1", "emails");
// Create a queue.
await client.createQueue({ parent, queue: { name: queueName } });
// Enqueue an HTTP target task.
const [task] = await client.createTask({
parent: queueName,
task: {
httpRequest: {
url: "https://example.com/worker",
httpMethod: "POST",
headers: { "Content-Type": "application/json" },
body: Buffer.from(JSON.stringify({ id: 42 })),
},
},
});
// Force an immediate run (records an attempt; no real dispatch happens).
await client.runTask({ name: task.name });
Error decoding note (google-gax v5)
@google-cloud/tasks ships google-gax v5, whose REST fallback only transcodes
a google.rpc.Status error body back into a canonical gRPC status code (e.g.
NOT_FOUND → 5) when the transport's fetch() resolves on non-2xx
responses instead of throwing. The default GoogleAuth/gaxios transporter
throws, surfacing the raw HTTP status as error.code. In tests we therefore
supply a tiny authClient whose fetch() always resolves the response (see
tests/cloudtasks.test.ts), which makes the client surface real gRPC codes.
The fake itself always returns the correct HTTP status + google.rpc.Status
body — this is purely a client transport detail.
Implemented operations
The fake transcodes the Cloud Tasks v2 google.api.http annotations. Every RPC
the real @google-cloud/tasks v2 client exposes is implemented.
Queues (CloudTasks service)
| RPC | Method + path |
|---|---|
ListQueues | GET /v2/{parent=projects/*/locations/*}/queues |
GetQueue | GET /v2/{name=projects/*/locations/*/queues/*} |
CreateQueue | POST /v2/{parent=projects/*/locations/*}/queues |
UpdateQueue | PATCH /v2/{queue.name=projects/*/locations/*/queues/*} |
DeleteQueue | DELETE /v2/{name=projects/*/locations/*/queues/*} |
PurgeQueue | POST /v2/{name=...}:purge |
PauseQueue | POST /v2/{name=...}:pause |
ResumeQueue | POST /v2/{name=...}:resume |
Tasks
| RPC | Method + path |
|---|---|
ListTasks | GET /v2/{parent=.../queues/*}/tasks |
GetTask | GET /v2/{name=.../queues/*/tasks/*} |
CreateTask | POST /v2/{parent=.../queues/*}/tasks |
DeleteTask | DELETE /v2/{name=.../queues/*/tasks/*} |
RunTask | POST /v2/{name=.../queues/*/tasks/*}:run |
IAM (google.iam.v1)
| RPC | Method + path |
|---|---|
GetIamPolicy | POST /v2/{resource=.../queues/*}:getIamPolicy |
SetIamPolicy | POST /v2/{resource=.../queues/*}:setIamPolicy |
TestIamPermissions | POST /v2/{resource=.../queues/*}:testIamPermissions |
Locations mixin (google.cloud.location)
Served at the /v1/ prefix (the mixin is versioned independently of the
service), matching the real client.
| RPC | Method + path |
|---|---|
ListLocations | GET /v1/{name=projects/*}/locations |
GetLocation | GET /v1/{name=projects/*/locations/*} |
Internal parlel endpoints
These are not part of the Cloud Tasks API; they are conveniences for tests and tooling.
| Endpoint | Purpose |
|---|---|
GET /_parlel/health | Liveness + queue/task counts |
POST /_parlel/reset | Wipe all in-memory state |
GET /_parlel/dump | Dump queues + tasks as JSON |
Behavior notes
- HTTP target tasks (
httpRequest) and App Engine target tasks (appEngineHttpRequest) are both accepted; exactly one must be supplied. runTaskrecords a synthetic successful attempt (incrementingdispatchCount/responseCountand settingfirstAttempt/lastAttempt) but performs no real HTTP dispatch — zero side effects.- Task views:
FULLincludes the request body;BASIC(the default) omitshttpRequest.body/appEngineHttpRequest.body. - Queue defaults: newly created queues start
RUNNINGwith defaultrateLimits(500 dps / 100 burst / 1000 concurrent) andretryConfig(100 attempts, exponential backoff). updateQueuesupportsupdateMaskpaths in snake_case, including nested leaves such asrate_limits.max_dispatches_per_second. With no mask, every supplied field is patched. It upserts if the queue does not yet exist.pauseQueue/resumeQueue/purgeQueueflip queue state / clear tasks.runTaskon a paused or disabled queue returnsFAILED_PRECONDITION.- Pagination:
pageSize+pageTokenare honored for allList*RPCs (the token is an opaque base64 offset), so*Async/*Streamiterators work.
Surface coverage
This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.
Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.
| Feature | Status |
|---|---|
| Queue CRUD + pause/resume/purge | ✅ Supported |
| Task create/get/list/delete/run | ✅ Supported |
| HTTP target & App Engine target tasks | ✅ Supported |
Scheduled tasks (scheduleTime) | ✅ Stored & returned |
Task name dedup (ALREADY_EXISTS) | ✅ Supported |
BASIC / FULL response views | ✅ Supported |
| IAM get/set/test policy | ✅ Supported (test grants all permissions) |
| Locations list/get | ✅ Supported |
List pagination (pageSize/pageToken) | ✅ Supported |
| Actual HTTP dispatch of tasks to a target | ✓ By design — Intentional for a local, zero-cost test emulator |
| Automatic scheduled delivery / retries | ⟳ Roadmap — Not simulated (use runTask to force) |
| Rate-limit / concurrency enforcement | ✓ By design — Config stored but not enforced |
| Task name de-dup retention window | ⟳ Roadmap — Not simulated (dedup only on live tasks) |
v2beta2 / v2beta3 surfaces (e.g. BufferTask) | ⟳ Roadmap — v2 only |
| gRPC transport | ⟳ Roadmap — REST (fallback) only |
Error codes & shapes
Errors are returned as a google.rpc.Status-shaped JSON body with the matching
HTTP status code:
{
"error": {
"code": 404,
"message": "Queue does not exist: projects/parlel/locations/us-central1/queues/missing",
"status": "NOT_FOUND"
}
}
| Condition | gRPC status | HTTP |
|---|---|---|
| Missing queue / task / location | NOT_FOUND (5) | 404 |
| Duplicate queue / task name | ALREADY_EXISTS (6) | 409 |
| Invalid name / missing payload / bad argument | INVALID_ARGUMENT (3) | 400 |
runTask on paused/disabled queue | FAILED_PRECONDITION (9) | 400 |
| Unknown verb / path | UNIMPLEMENTED (12) / NOT_FOUND (5) | 501 / 404 |
Configuration — test.env
Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.
CLOUD_TASKS_EMULATOR_HOST=parlel-bridge:4584
CLOUDTASKS_EMULATOR_HOST=parlel-bridge:4584
GOOGLE_CLOUD_PROJECT=parlel
GCLOUD_PROJECT=parlel
<!-- parlel:testenv:end -->