Using workers

A worker is backend code that lives in your HTML file. You write it in a <script type=worker> element, as a fetch() handler that answers requests. In the playground it runs in the service worker of the output, so a page and its API develop together. With the edge attribute, the same code deploys to RTEdge.net as the worker of your route.

Note: Workers run in the playground and, with edge, on RTEdge.net. The two runtimes differ in a few places. This article marks them, and Playground and edge differences lists them all.

Writing a worker

Add a <script type=worker> element and export a fetch() handler. The handler receives a Request and returns a Response, as in a service worker or a Cloudflare Worker.

A first worker

html
<iframe src=:hello></iframe>
<script type=worker edge>
  export default {
    fetch: request => new Response(`Hello from ${new URL(request.url).pathname}`)
  }
</script>

You should see Hello from /hello in the iframe. The : in front of hello addresses the worker, which sees the path /hello. The edge attribute marks the script for deployment; without it, the worker runs only in the playground.

Three forms of the handler work: fetch(request, env, ctx) { … }, async fetch(request, env, ctx) { … } and fetch: (request, env, ctx) => …. Keep fetch as the only property of export default. Handler forms lists what else the transform accepts and what it rejects.

Calling the worker

Prefix a path with : to reach the worker. Everything after the first : becomes the pathname that the worker sees, and the query string is kept:

You requestThe worker sees
/:/
/:1.json/1.json
/:/x/x
/:api/users?id=7/api/users?id=7
/app/:/
/a/:b/c/b/c

On RTEdge.net, the text before the first : also selects the route, so /app/: calls the worker of the route name/app.

In HTML and JavaScript, write the colon first: <iframe src=:>, fetch(':api') or new EventSource(':sse'). A reference such as api:x is read as a URL scheme. ./a:b works, and the worker sees /b. Fragments never reach the worker.

A JSON endpoint called from the page

Requests that the page makes with fetch() keep their method, headers and query.

html
<output id=out></output>
<script type=module>
  const { greeting } = await fetch(':api/greet?name=Ada').then(r => r.json())
  out.value = greeting
</script>
<script type=worker edge>
  export default {
    fetch: request => {
      const { pathname, searchParams } = new URL(request.url)
      if (pathname !== '/api/greet') return new Response('Not found', { status: 404 })
      return Response.json({ greeting: `Hello, ${searchParams.get('name') ?? 'world'}!` })
    }
  }
</script>

You should see Hello, Ada!.

Other paths in the playground

  • GET / always returns your page, even when a worker exists, so the worker never sees it. A non-GET request to / goes to the worker. To let the worker render the root, request :.
  • Any other path of the output reaches the worker without a :, unless a file with that name is exported with o:=. A path whose last segment contains a . is answered from your exported files, for any method and ignoring the query, before the worker is asked. Siblings of your route can answer such names too; see Sibling routes.
  • A : in front of a path skips the file lookup: /:app.js reaches the worker as /app.js.

Other paths on RTEdge.net

  • A path with a : goes straight to the worker of the route. No file lookup happens, and edge:user is not applied, so the worker must check request.rt.user itself.
  • Without a :, the router serves the page or an exported file first. The worker answers when the page is empty once the worker is removed, which makes an API-only route; for a dotted name that no o: export matches; and for a non-GET request to the root. In these calls, the worker sees the full original pathname, route prefix included.
  • A clean path such as /api/users is redirected with 307 to /api/users/, which is a different route, and ends in Code does not exist. Use /:api/users.
  • On a path route, always write /docs/:x. docs:x without the slash belongs to the parent route, and a URL that ends in docs: is redirected to docs/:.
  • The sequences ::s, ::i, ::e, ::c, ::u, ::t and ::v, and /::edge, ::qr and ::clear at the end, are claimed by the platform and never reach the worker. Do not use them in worker paths. See Reserved paths and the /: URL suffix.

What a worker receives

The handler is called with three arguments:

Several worker scripts

A page may have more than one worker script. The playground joins the text of all of them, in document order, into one program, and runs it. Only the first script's edge, fetch, io:host and io:fail attributes count, so put them there. The scripts must stay one valid program: two export default statements across scripts make every request fail.

On RTEdge.net, only the edge scripts are merged, in document order, into the worker of the route. A deploy of a page with no edge script deletes the route's existing worker.

Data and assets

Use env.KV for data that must outlive a request, and env.FS for files that only the worker should read:

js
export default {
  fetch: async (request, { KV, FS }) => {
    const { user } = request.rt
    if (!user) return new Response('Sign in first', { status: 401 })
    const kv = KV.open({ user })                       // call it, never await it
    await kv.put('last-seen', new Date().toISOString())
    return new Response(FS['greeting.txt'].body)       // a worker:fs asset
  }
}

Always check request.rt.user before you open per-user storage: a null user opens the scope that all visitors share. See Storing data in KV.

Making outbound requests

A worker calls other servers with fetch(). Use absolute URLs, such as https://api.example.com/v1/x, or protocol-relative ones such as //api.example.com/v1/x, so that the code works in both places.

The playground and RTEdge.net

The same worker code behaves differently in these areas:

AreaPlaygroundRTEdge.net
Where it runsThe service worker of the output, as a function body.A Cloudflare Workers for Platforms user worker for each route, built as one ES module.
Scripts usedEvery <script type=worker>.Only the edge scripts.
Top-level codeRuns again for each request. The handler of the first evaluation stays cached.Runs once per isolate. State may persist, but isolates are evicted unpredictably.
request.rtcode: true, and your account as user.edge: true, ip, ray, colo, country, and the real domain and route.
request.cfEmulated.Do not rely on it. Use request.rt.colo and country.
KVIndexedDB in your browser, 64 KiB per value plus metadata.Cloudflare KV, 10 MiB per value plus metadata, real scopes.
Relative fetch()Resolves against the output origin.Fails. Use absolute URLs, such as //host/x, or worker:fs assets.
Paths without a :Every non-root path reaches the worker.Only unexported dotted names and non-GET requests to the root do.
Page navigationsArrive as a bare GET, without method, headers and body.Keep method, headers and body.
An undefined responseFalls through to the network.A 501 page.
An exceptionHTTP 500 text, or a network error.An HTML 500 page.
eval and new FunctionAllowed.They throw while a request is handled.
WebAssemblyEvery API.The shapes that deploy extracts.
ImportsBundled in the browser.Bundled by a build service at every deploy. cloudflare: imports work.
io:host, io:fail, sync.requestRT, sibling routesYes.No.

Treat these differences as features: the playground gives you a fast loop and fakes what it can, and RTEdge.net runs the real thing. Test the behaviours you depend on after you deploy.

Deploying a worker

Give the script edge and deploy with Ctrl+S / ⌘+S. The playground bundles static imports first, and RTEdge.net uploads one worker for the route. After a successful upload, a second toast links to name.rt.ht/:, the root of the worker. Errors from the upload appear as separate toasts. Call the worker with a : URL, such as https://name.rt.ht/:api/greet?name=Ada.

Logging and errors

In the playground, calls to console in worker code appear in the playground console with the 🛎️⚙️ prefix, and the network panel lists every request that the worker handled. A failed worker either gives an HTTP 500 with a stack or a network error, and io:fail=open forwards the request to the network instead; see Failing open or closed.

On RTEdge.net, an uncaught exception shows a 500 page, an undefined response a 501 page, and a : call to a route without a worker a 404 page. See Error pages. The console output of a deployed worker is not part of the response. Tail logs Experimental collect it; see Usage and tail logs.

Mistakes to avoid

Module scope and state explains why each of these fails.

See also