Workers
Run JavaScript on a request, a schedule, or an object event.
A worker is an ES module. You deploy it from the console and it runs on AnythingCloud's nodes, not on your servers. Each worker has its own address, versions, environment, bucket bindings, and triggers. A trigger decides when the code runs: an HTTP request, a timer, or a change to an object in a bucket.
The worker page has six tabs: Overview (the default), Metrics, Code, Triggers, Logs, and Settings. Each worker runs in its own process and stays warm between deploys and node restarts. A stopped or daily-recycled worker can return a 503 while it starts. New workers are placed on the least-loaded configured node.
Create and deploy
- Open Workers and choose Create worker.
- Enter a name and a worker id. The id is fixed and is used for the
console URL. The automatic address uses a generated name such as
icy-waterfall-57a4.workers.anything-cloud.com. You can point the worker at a different label under the same suffix later in Settings → Address. - Choose an address: AnythingCloud address, No address, or Your domain for a root domain you have already verified.
- Pick a template, then edit the code. A worker exports
fetch,scheduled, orevent. - Choose Create worker. The first version is stored and the deploy is queued for a node.
The Code tab is the editor, with live syntax highlighting.
Paste code, choose Upload .js, or drop a .js /
.mjs file onto the editor to load it. Press
Ctrl + Enter or Cmd + Enter to open the
deploy dialog, add a changelog line, and save. A deploy of new code stores
an immutable version; deploying code identical to an existing version reuses
that version. Each version is identified by its content hash, not a number.
The last 20 are kept. The Versions page lists every kept
version by hash; roll back to the previous one from the page header, or pick
any older version from its row, without re-uploading code.
A deploy starts the new version beside the one that is serving, waits for it to pass a health check, and then sends new requests to it. Requests that are already running finish on the old version. If the new version does not become healthy, the old version keeps serving.
Addresses and custom domains
Every worker can have one generated address under
workers.anything-cloud.com, for example
icy-waterfall-57a4.workers.anything-cloud.com. Choose
No address when the worker only runs on schedules or
object events; you can add a host later.
To use a hostname you own, follow the Custom domains guide. The host appears in the worker's trigger list, and Settings → Address can change the address the worker advertises.
The guide also covers DNS and certificates. A custom host that shows Preparing is waiting for DNS or the certificate.
When a custom host becomes active it becomes the worker's primary address, and the console shows it instead of the generated address. An existing generated host stays in the trigger list as a fallback. You can remove it once a custom host exists; when you remove the last custom host, the generated address is created again if it is missing.
Requests reach the worker with their method, path, query string, and body.
The edge sets X-Forwarded-For, X-Forwarded-Proto,
X-Forwarded-Host, and X-Real-IP, preserves the
Host header, and strips internal invocation headers. The
worker's status code, headers, and body are returned to the caller.
export default {
async fetch(request) {
const url = new URL(request.url);
const agent = request.headers.get("user-agent") || "unknown";
return Response.json({
path: url.pathname,
query: Object.fromEntries(url.searchParams),
agent,
});
},
};
Triggers
Open Triggers to see everything that invokes the worker in one list. Each row is one of three types: Request host, Schedule, or Object event. Schedules and object events have an on/off switch; request hosts can be copied or removed.
Request hosts
A request host sends HTTP requests to the worker's fetch
handler. The generated address and each custom hostname are request
triggers. A host shows Active once it is routed and
Preparing while DNS or TLS is pending. Copy its URL from
the row, or remove it when you no longer need it.
Schedule triggers
A schedule runs the scheduled handler. Choose an interval
from five minutes to one day, or enter a five-field cron expression
(minute hour day month weekday) evaluated in the time zone you
select.
export default {
async scheduled(event) {
// event.scheduledTime is the run time as an ISO 8601 string.
const response = await fetch("https://example.com/ping");
console.log("ping", response.status);
},
};
Schedules are best-effort and can start a few seconds late under load. Runs do not overlap: the next run starts after the current one finishes, so keep the handler idempotent and short.
Object event triggers
An object event runs the event handler when an object changes
through the console in a bucket you own. Choose the bucket, one or more
event types (created, updated, deleted,
renamed, or restored), and an optional key prefix.
Reads never fire an event. Writes made through the S3 API, a presigned URL,
or env.BUCKET.put do not fire console object events.
The handler receives one event object:
export default {
async event(event, env) {
// event.type "created", "updated", "deleted", "renamed", or "restored"
// event.bucket the bucket name
// event.key the object key
// event.size bytes, currently 0 for console events
// event.etag the object ETag, currently empty for console events
// event.oldKey the previous key, for a rename
// event.eventTime when the change happened, as an ISO 8601 string
console.log(event.type, event.bucket, event.key);
},
};
A prefix such as uploads/ keeps a busy bucket from waking the
worker for every file. The event is delivered after the bucket change
completes, so a slow handler never delays the upload or delete that
produced it.
Environment
Open Settings → Environment and add key/value rows.
Values arrive in the handler as env.KEY:
export default {
async fetch(request, env) {
const key = env.API_KEY;
if (!key) return new Response("Not configured", { status: 500 });
// use the key
},
};
Keys use uppercase letters, digits, and underscores, and start with a letter. Values are encrypted at rest and injected into the worker process; saving a variable applies to the worker without a new code deploy. Secret values are hidden after creation and shown once in the response that creates them. Turn off the secret option for a value you want to see in the list.
For values that change at runtime, create a
KV namespace and bind it in
Settings → KV namespaces. The alias you choose becomes
env.KV or another name.
Bucket helpers
Bind a bucket under Settings → Buckets and it appears
as env.ALIAS (the default alias is BUCKET). The
binding is tied to one bucket, and the runtime signs requests for it, so no
endpoint, access key, or private address is exposed to your code.
| Call | Does |
|---|---|
env.BUCKET.get(key) | Reads an object. Returns null when the key is missing. The result has size, etag, text(), json(), arrayBuffer(), and body. |
env.BUCKET.head(key) | Metadata only: key, size, and etag. Returns null when the key is missing. |
env.BUCKET.put(key, body, options) | Writes an object. Pass a string, stream, or buffer, and set the type with { httpMetadata: { contentType } }. |
env.BUCKET.delete(key) | Deletes an object. Deleting a missing key is not an error. |
env.BUCKET.list({ prefix, limit, cursor }) | Lists objects under a prefix, up to 1,000 per page. Returns { objects, truncated, cursor }, where each object has key, size, and etag. |
env.BUCKET.url(key) | The public URL for a key on the bucket's CDN host. |
Returning a file
For a public bucket, do not stream object bytes through the worker. Redirect to the bucket URL so the download is served from the edge cache, with range requests and caching handled for you. Public bucket reads are edge-cached for up to five minutes, so an overwrite or delete can take that long to appear:
export default {
async fetch(request, env) {
const key = new URL(request.url).pathname.slice(1);
if (!key) return new Response("Missing key", { status: 400 });
const object = await env.BUCKET.head(key);
if (!object) return new Response("Not found", { status: 404 });
return Response.redirect(env.BUCKET.url(key), 302);
},
};
The redirect hands the download to the bucket host; the browser address bar
changes to that host. env.BUCKET.url(key) returns the public URL; it
does not grant access to a private bucket. For a private object, read it
with get and return a Response.
Receiving an upload
A worker can be the upload endpoint for a bucket: accept a multipart form, write the file into the bucket, and answer with the public URL.
export default {
async fetch(request, env) {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const form = await request.formData();
const file = form.get("file");
if (!file || typeof file === "string") {
return new Response("Missing file", { status: 400 });
}
const key = `uploads/${crypto.randomUUID()}-${file.name}`;
await env.BUCKET.put(key, file.stream(), {
httpMetadata: { contentType: file.type || "application/octet-stream" },
});
return Response.json({ key, url: env.BUCKET.url(key) }, { status: 201 });
},
};
Route it to a hostname such as upload.example.com and the form
posts straight to the worker. The bridge caps a single put at
10 MB, and the bucket's public/private setting still applies. Plan quotas
are enforced on console uploads; S3 API and worker writes are not capped by
the plan, so keep them within the plan you are paying for.
Outbound requests
Workers can call public HTTP and HTTPS endpoints with fetch,
which is useful for sending a webhook or calling another API:
export default {
async fetch(request, env) {
const response = await fetch("https://example.com/hooks/orders", {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${env.WEBHOOK_TOKEN}`,
},
body: JSON.stringify({ event: "order.created" }),
});
return Response.json({ delivered: response.ok });
},
};
Public hosts on ports 80 and 443 are reachable. Private, loopback, link-local, and LAN addresses are blocked by the node firewall, including cloud metadata endpoints. Use a bucket binding for object storage; S3 credentials are not available to worker code.
Streaming responses (SSE)
Server-Sent Events are the supported way to push live updates to a
browser. Return a text/event-stream response and keep it open;
the edge and worker node stream each chunk as it is written. SSE is
one-way: send messages from the browser with a normal fetch
POST to the same worker.
export default {
async fetch(request, env) {
if (request.method === "POST") {
const body = (await request.text()).slice(0, 2000);
await env.DB.prepare(
"INSERT INTO messages (room, body, created_at) VALUES (?, ?, ?)"
).bind("general", body, Date.now()).run();
return new Response("ok");
}
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
const send = (data) =>
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`));
const heartbeat = setInterval(() => send({ ping: true }), 15000);
const poll = setInterval(async () => {
const rows = await env.DB.prepare(
"SELECT id, body FROM messages WHERE room = ? ORDER BY id DESC LIMIT 20"
).bind("general").all();
send({ messages: rows.results });
}, 1000);
request.signal.addEventListener("abort", () => {
clearInterval(heartbeat);
clearInterval(poll);
controller.close();
});
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-store",
"Connection": "keep-alive",
},
});
},
};
The browser receives each event with EventSource:
const events = new EventSource("/events?room=general");
events.onmessage = (event) => {
const message = JSON.parse(event.data);
if (message.messages) render(message.messages);
};
- Send a heartbeat at least every 30 seconds; the node closes an idle stream after 5 minutes.
- SSE responses are never cached, even when the worker cache is on.
- Return
Cache-Control: no-storeand do not compress the stream. - Authenticate the stream before returning it.
EventSourcesends same-origin cookies; do not put session tokens in the URL because URLs appear in logs. - If you allow cross-origin streams, validate the
Originheader and never combine a wildcard CORS origin with credentials. - One open stream per browser tab; each connection counts against the worker host's connection limit.
- Use TiniDB or a KV namespace for message history; the worker isolate only sees its own connection.
WebSocket upgrades are not supported. Use SSE for server push and POST for client messages.
Logs and metrics
console.log output and uncaught errors appear on the worker's
Logs tab. Filter messages, select a level, then copy or
download the result. Each worker keeps the newest 2,000 lines for 7 days;
older lines are deleted automatically. Turn off Save logs
to discard new output without clearing what is already stored, or use
Clear to remove the stored lines. An uncaught error in a
request handler returns a 500 to the caller and is recorded in the log.
Metrics
The Metrics tab covers 24 hours, 7 days, or 30 days. It shows invocations, errors and error rate, average duration, and egress, each compared with the previous window of the same length. A source breakdown separates requests, schedules, and object events, including each source's error rate and average duration.
The edge map shows where traffic enters. The edge currently runs in Columbus, US, so entries appear there on the map.
Cache
Settings → Cache can reuse responses from your
fetch handler so repeat requests skip your code. Caching is
off by default and runs at the edge, so a hit does not travel to a worker
node. When it is on, GET and HEAD requests
consult the cache, and GET responses are stored only when
Cache-Control and Expires allow it; a response
with no lifetime uses a safe default based on its status code. Requests
with a Cookie or Authorization header bypass the
edge cache, and responses that set cookies, use Vary, or carry
Cache-Control: no-store or private are never
stored. Cached responses include an Age header.
File-type cache rules are separate from this switch. See
Cache rules in the
Custom domains guide to set a TTL for extensions such as css or
woff2.
Billing
Workers are billed by running time, not per request. The base hourly rate applies to a 128 MB worker, and the price scales with the Memory you choose in Settings:
| Memory | Hourly price |
|---|---|
| 64 MB | 0.5x the base rate |
| 128 MB (default) | The base rate |
| 256 MB | 2x the base rate |
| 512 MB | 4x the base rate |
The default base rate is $0.0025 per hour for 128 MB. A 64 MB worker costs half that, while 256 MB doubles it and 512 MB quadruples it. The operator can change the base rate. Bucket storage and KV namespace storage are billed separately. The Billing page has a monthly budget with an optional hard cap.
Paused workers do not accrue runtime. Pausing an AnythingCloud account stops its workers and triggers until the account is resumed. The Billing page shows running workers, hours this month, and the worker share of the current bill.
Limits
| Limit | Value |
|---|---|
| Code format | ES module exporting fetch, scheduled, and/or event |
| Bundle size | 1 MiB |
| Request body | 10 MB |
| HTTP edge timeout | 30 seconds |
| Environment value | 8 KB per variable |
| Environment variables | 50 per worker |
| Request hosts | 20 per worker |
| Memory | 64, 128, 256, or 512 MB (default 128 MB) |
| Request timeout | 1, 5, 10, or 30 seconds for scheduled and event runs (default 10 seconds); HTTP requests use the 30-second edge timeout |
| Versions kept | 20 |
| Schedules and object events per worker | 20; request hosts are not counted |
| Workers per member | 10 by default |
| Log lines kept | 2,000 per worker |
A run that exceeds its memory cap is stopped and counted as an error.