AQS
Hand work from one part of your application to another, without keeping a server online.
AQS is a managed message queue. A producer publishes a message; a consumer receives it, does the work, and acknowledges it. Messages are kept until they are acknowledged, delivery is at-least-once, and a message that keeps failing moves to a dead-letter queue instead of being discarded.
Consume from a Worker, a script, or a server. The queue page shows its depth, stored bytes, oldest message, today's published and delivered counts, and the dead-letter queue. The shared address works immediately; a custom hostname adds a branded address when you need one.
Quick start
- Open AQS in the console and choose Create queue. Give it a name; the generated slug is used in the address. Set the visibility timeout, the maximum attempts, and the payload size, or keep the defaults. You can change the storage limit later on the Settings tab.
- The first token is shown once when the queue is created. Create more tokens on the Tokens tab. A token has one or more scopes: send publishes, consume receives and acknowledges, and manage reads message payloads, purges, and handles dead letters. Treat it as a privileged token and keep it server-side: never put it in browser code or source control. Use one token per application so a token can be rotated or revoked on its own.
Send a message with curl. The body field is a
string; this example sends JSON and gives the message an idempotency key so
a retry cannot publish it twice:
curl -X POST "https://anything.solutions/queue/v1/image-jobs/messages" \
-H "Authorization: Bearer acq_your_token" \
-H "Content-Type: application/json" \
-d '{"body":"{\"type\":\"resize\",\"key\":\"avatars/42.png\"}","content_type":"application/json","idempotency_key":"resize-42"}'
Receive a batch. Each message stays hidden for the visibility timeout while you work on it:
curl -X POST "https://anything.solutions/queue/v1/image-jobs/consume" \
-H "Authorization: Bearer acq_your_token" \
-H "Content-Type: application/json" \
-d '{"max_messages":10,"max_wait_seconds":20,"visibility_seconds":30}'
The response carries one receipt per message:
{
"messages": [
{
"id": "9c1f0a2b4d5e4f6a8b7c9d0e1f2a3b4c",
"receipt": "eyJxIjo...",
"body_base64": "eyJ0eXBlIjoicmVzaXplIn0=",
"content_type": "application/json",
"attributes": {},
"attempts": 1,
"group_id": null,
"published_at": "2026-11-03T12:00:00Z"
}
]
}
Decode body_base64 as UTF-8 JSON, then acknowledge the
receipts. An unacknowledged message becomes available again when its
visibility timeout ends:
curl -X POST "https://anything.solutions/queue/v1/image-jobs/ack" \
-H "Authorization: Bearer acq_your_token" \
-H "Content-Type: application/json" \
-d '{"receipts":["eyJxIjo..."]}'
To run the consumer in a Worker instead, add a queue trigger and export a
queue handler. The Worker trigger
section has the code.
API reference
The base address is https://anything.solutions/queue/v1; the queue page shows the
same address for each queue. Every request needs an AQS token, not an
AnythingCloud API key.
| Method | Path | Scope | Purpose |
|---|---|---|---|
| POST | /queue/v1/{slug}/messages | send | Publish one message. |
| POST | /queue/v1/{slug}/messages/batch | send | Publish up to 100 messages in one request. |
| POST | /queue/v1/{slug}/consume | consume | Receive a batch and start a visibility lease on each message. |
| POST | /queue/v1/{slug}/ack | consume | Acknowledge receipts and delete the messages. |
| POST | /queue/v1/{slug}/nack | consume | Return messages for another attempt, with an optional delay. |
| GET | /queue/v1/{slug}/stats | send or consume | Depth, stored bytes, oldest message, and today's counts. |
| GET | /queue/v1/{slug}/messages/peek | manage | Read messages without acknowledging them. |
| POST | /queue/v1/{slug}/purge | manage | Delete waiting (unleased) ready messages. Dead letters require state: "dead". This cannot be undone. |
| GET | /queue/v1/{slug}/dead-letters | manage | List messages that used all their attempts. |
| POST | /queue/v1/{slug}/dead-letters/replay | manage | Move dead-letter messages back to the queue. |
| POST | /queue/v1/{slug}/dead-letters/purge | manage | Delete dead-letter messages. |
| GET | /queue/v1/health | none | Service health; see Health. |
A publish request accepts:
| Field | Does |
|---|---|
body | The message as a UTF-8 string. Use body_base64 instead for binary data. |
content_type | The message type. Defaults to application/json. |
attributes | An optional JSON object stored with the message. At most 8 KiB. |
delay_seconds | Hold the message before a consumer can receive it, up to seven days. |
group_id | An optional label for related messages, such as a user or order id. |
idempotency_key | A unique string per message. Reusing the key with the exact same body, content type, and attributes returns the existing message; a different payload is rejected with idempotency_conflict. |
A batch publish sends {"messages": [ ... ]} with up to 100
entries. The request envelope is limited to 80 MiB and each entry to the
queue's payload size. A consume request accepts max_messages
(1 to 100), max_wait_seconds (0 to 20), and
visibility_seconds. A receipt is bound to that exact delivery
lease and stays valid for the lease plus five minutes (at least one hour).
Consuming again issues a new receipt, and an old receipt cannot acknowledge
a later delivery.
A successful write returns the affected message or a count. A failed
request returns a non-2xx status and a JSON object with an
error string. Retry a failed publish with the same
idempotency_key so the message is not stored twice.
Retries and dead letters
Every delivery increments the message's attempt count and starts a visibility lease. The lease ends when you acknowledge, when you return the message with a negative acknowledgement, or when the visibility timeout runs out. A message whose lease expires is delivered again with a new receipt and a higher attempt count.
A negative acknowledgement returns the message immediately, or after a delay:
curl -X POST "https://anything.solutions/queue/v1/image-jobs/nack" \
-H "Authorization: Bearer acq_your_token" \
-H "Content-Type: application/json" \
-d '{"receipts":["eyJxIjo..."],"delay_seconds":30}'
When a message reaches the queue's maximum attempts (five by default), it moves to the dead-letter queue with its last error. It is not discarded. The Dead letters tab shows the reason, the attempt count, and a preview of the body; from there you can replay a message to the queue or purge it. The same actions are available through the dead-letter API routes.
Delivery is at-least-once, so a consumer can see the same message more
than once: a lease can expire while the work is still running, or a
consumer can crash before it acknowledges. Make handlers idempotent, and
set an idempotency_key when the producer itself may retry.
Worker queue trigger
Open a Worker and choose Triggers, then Queue. Pick the queue, an alias, and the access mode: consume, send, or both. Set Messages per run and Wait (seconds) for each poll.
The handler shape follows Cloudflare Queues: queue(batch, env)
receives decoded message.body values plus
message.ack(), message.retry({ delaySeconds }),
batch.ackAll(), and batch.retryAll().
batch.queue is the queue name, and messages you do not mention
are acknowledged when the handler returns.
The worker exports a queue handler. The agent invokes it with
one batch. Returning normally acknowledges every message in the batch;
throwing returns the whole batch to the queue with a backoff, so a failure
is retried up to the queue's attempt limit before dead-lettering:
export async function queue(batch, env) {
for (const message of batch.messages) {
try {
await handle(message.body, env);
message.ack();
} catch (error) {
message.retry({ delaySeconds: 60 });
}
}
}
Each entry in batch.messages has the decoded
body (an object for JSON, a string for text, bytes otherwise),
id, timestamp, attempts,
content_type, and attributes.
message.ack() acknowledges one message and
message.retry({ delaySeconds }) returns it later.
batch.ackAll() and batch.retryAll({ delaySeconds })
apply to the whole batch. Unmentioned messages are acknowledged when the
handler returns; throwing releases the whole batch.
batch.queue is the queue name.
With a trigger that allows sending, the same worker can publish through
the alias. send publishes one message; sendBatch
accepts up to 100:
export async function fetch(request, env) {
await env.JOBS.send(
{ type: "resize", key: "avatars/42.png" },
{ contentType: "application/json", delaySeconds: 30, idempotencyKey: "resize-42" },
);
await env.JOBS.sendBatch([
{ body: { type: "thumbnail", key: "media/clip-9.mp4" }, contentType: "application/json" },
"plain text message",
]);
return new Response("queued", { status: 202 });
}
Messages from a queue trigger that fail in the worker are returned to the queue and retried until the queue's attempt limit, then dead-lettered. The worker's Metrics tab counts queue runs alongside requests, schedules, and events.
Custom host
The shared address is always available and needs no DNS work. To serve the same API from a hostname you own, open Networking → Hosts and add a host whose target is the queue, under a verified root domain.
Point a DNS-only CNAME at the edge:
| Type | Name | Value |
|---|---|---|
| CNAME | queue.example.com | edge.anything-cloud.com |
Keep the record DNS-only (grey cloud in Cloudflare). The edge issues and renews the certificate automatically; a proxied record can interrupt issuance and buffer long polls. The host shows Setting up while DNS and the certificate are pending, Active once it is routed, and Error with a reason when it cannot be applied.
A custom host is pinned to its queue and rooted at /, so the
paths drop the queue slug:
| Shared address | Custom host |
|---|---|
/queue/v1/image-jobs/messages | /messages |
/queue/v1/image-jobs/consume | /consume |
/queue/v1/image-jobs/ack | /ack |
/queue/v1/image-jobs/nack | /nack |
/queue/v1/image-jobs/stats | /stats |
/queue/v1/image-jobs/messages/peek | /messages/peek |
/queue/v1/image-jobs/purge | /purge |
/queue/v1/image-jobs/dead-letters | /dead-letters |
Every route works on a custom host with the queue slug removed, including peek, purge, and the dead-letter routes. The token is still required, and responses are never cached. Removing a host stops serving the API at that address; the queue, its messages, and its tokens stay in place.
Limits and guarantees
| Limit | Default |
|---|---|
| Queues per member | 10 |
| Storage per queue | 1 GiB |
| Message body | 1 MiB included; 10 MiB and 50 MiB are paid add-ons |
| Batch publish | 100 messages, up to the queue payload size per entry; the request allows eight full-size messages plus 64 KiB, with an 80 MiB ceiling |
| Batch consume | 100 messages per request |
| Visibility timeout | 30 seconds by default, up to 12 hours |
| Attempts before dead letter | 5 by default, up to 100 |
| Long poll | 20 seconds |
| Publish and consume rate | 1,000 requests per second per token |
| Stats rate | 120 requests per minute per token |
| Purge and dead-letter actions | 6 requests per minute per token |
| Message attributes | 8 KiB per message |
| Publish delay | Up to seven days |
A queue keeps messages until a consumer acknowledges them and never deletes old messages to make room. At the storage limit, new publishes are rejected until consumers catch up. Dead letters also consume storage and keep doing so until replayed or purged. Pausing a queue stops publishes and deliveries, and its custom hosts stop serving until it is resumed. The hosts and their routes are kept.
Need a larger message? Choose the 10 MiB or 50 MiB payload add-on on the queue settings page. The add-on is billed monthly per queue, and the queue immediately starts accepting messages up to the new size. The add-on stays billed while the queue is paused and stops when the queue is deleted. Switching back stops the add-on once no stored message is larger than the smaller limit; drain or purge oversized messages first. Messages already stored stay readable.
The optional payload add-on is billed monthly per queue. Message operations and stored bytes are shown in the console as an estimate; they are not separately charged today. See Billing & metrics for account usage and budgets.
Health
GET https://anything.solutions/queue/v1/health needs no token and
reports whether the service is ready to accept requests:
{"ok": true, "service": "aqs"}
Use it for uptime monitoring and load-balancer health checks. A healthy
service returns 200 with that object.