The edge worker runtime

This article describes how a worker runs after you deploy it to RTEdge.net: what the deploy does to your <script type=worker edge>, the settings the worker runs with, which requests reach it, what it can call, and what visitors see when it fails.

Note: Everything here is about deployed workers. The playground runs the same code in a service worker, with differences that Playground and edge differences lists.

From a script to a deployed worker

Each route has at most one edge worker. At deploy, RTEdge.net takes the <script type=worker edge> elements of the playground page, builds one ES module and uploads it as a Cloudflare Workers for Platforms user worker for that route. The edge router sends the requests for the route to it.

Source privacy

When a deployment contains an edge worker without io:guest, its source is private: only the owner, the editors and io:user permits can open it with ?. The source stays public only if every edge script carries io:guest and nothing else restricts it. A worker may therefore hold private logic. Redaction is not secrecy, though: edge:redact and edge:remove change only the served HTML, and a public source shows everything. See Restricting access and Keeping secrets out of deployments.

Upload errors

Cloudflare validates the module when it is uploaded. Exceptions thrown while the module loads, such as a fetch() or a timer at the top level, and imports that cannot be resolved fail the upload. The playground shows each Cloudflare error as a toast for its route, and a successful upload shows a toast with a link to name.rt.ht/:. Check these toasts after every deploy, and call the worker once to see it answer.

Compatibility settings and bindings

Every upload uses the same settings, and you cannot change them. The compatibility date is the UTC date of the deploy, so date-gated changes in the Cloudflare runtime reach a worker only when you deploy it again.

FlagEffect
no_nodejs_compat, no_nodejs_compat_v2Node.js APIs are off: there is no process or Buffer global, and node:* modules cannot be imported.
webcrypto_modern_algorithmsML-KEM and ML-DSA are available; see Post-quantum cryptography.
enable_request_signalrequest.signal exists. Cloudflare aborts it when the client cancels.
disallow_importable_envimport { env } from 'cloudflare:workers' is not allowed.

A worker has exactly two bindings, env.KV and env.FS. There are no environment variables or secrets, Durable Objects, R2, D1, Queues, service bindings, AI, Vectorize, Analytics Engine, Hyperdrive, Browser Rendering, send_email or rate-limit bindings, and no cron triggers. Code cannot declare any of them. A secret has to live in the worker source, which is private unless you add io:guest.

Shell versions

The wrapper that RTEdge.net compiles around your code is called the shell. Its version date is baked into each worker at deploy. It shows as request.rt.worker.version.shell and in the edge-worker header, and the current shell is 2026-04-30. A deployed worker keeps the behaviour of its shell until you redeploy it, covering the fields of request.rt, the KV API and its size limit, the patched fetch(), env.FS, the error pages and the compatibility settings.

ShellUser-visible change
2023-09-22request.rt.edge
2023-12-15The first KV getters, with a 64 KiB limit per value.
2024-10-01request.rt.user and request.rt.worker.owner
2024-10-30env.KV.open(), request.rt.domain and request.rt.route
2024-12-30The current KV key format, the key checks and the depth bounds.
2025-01-03request.rt.ip
2025-09-29The fetch() URL patch, env.FS, request.rt.ray, colo and country, and the 10 MiB limit for KV values.
2026-03-28env.FS[name].response Experimental, in some builds.
2026-04-30The current shell. Error pages show the error name, message and two stack lines, in some builds.

Not every build with a given shell date has every change. Redeploying always gives you the current behaviour.

The handler and its response

The accepted forms are the same as in the playground, because both use one transform: export default { fetch(request, env, ctx) { … } }, with async or as an arrow function, and a module-scope function fetch declaration. Each becomes a module-scope async function fetch(…). See The fetch() handler for the forms that do not work. Only fetch is wired: scheduled, queue, email and tail members never run.

The handler is called as fetch(request, env, ctx). ctx is Cloudflare's ExecutionContext, passed through unchanged, so ctx.waitUntil() works.

Module scope and runtime

Which requests reach the worker

See Request routing for the full order in which RTEdge.net handles a request.

Requests with a colon

A request whose path contains : is dispatched straight to the worker of the route. The router reads no deployment, does not check edge:user, does no file lookup and makes no ? or ?? decision. The worker pathname is the text after the first :, and an empty remainder becomes /. The text before it, minus its last segment, selects the route.

  • route/:x, route/:/x and route/: are all valid. Pages can use the relative form :x, as in <iframe src=:frame>.
  • A path that ends in route: is redirected with 307 to route/:.
  • route:x without the slash belongs to the parent route. On a path route, always write /docs/:x.
  • edge:user does not protect these calls. The worker must check request.rt.user itself.

Requests without a colon

The router reads the deployment, applies ?, ??, ?m=uo and edge:user, and then serves an exported file whose name matches the last path segment, for any method. A worker never sees a request for a path that matches an o: export of its route, a POST included. Otherwise it calls the worker with the original URL when any of these holds:

  • the page is empty, or only whitespace, once the worker is removed, which makes an API-only route;
  • the path is not the route root, that is, an unexported dotted name directly under the route;
  • the method is not GET, for a request to the route root.

These calls differ from : calls in the pathname the worker sees: it is the full original pathname, route prefix included. On a path route, a worker sees /docs/x.json from a fallback call but /x from /docs/:x. Without a worker, the answer is File does not exist.

A clean path, one without ., : or * and without a trailing slash, is first redirected with 307 to path/, and then counts as a different route. name.rt.ht/api/users never reaches the worker of the root route and ends in Code does not exist. Use /:api/users.

Paths the platform answers itself

Some requests never reach the worker, even on a : path:

  • any path that contains ::s, which the sign-in handling answers with a sign-in page, JSON, a 404 or a 501, so even /:api::save opens a sign-in page;
  • any path that contains ::i, ::e, ::c, ::u, ::t or ::v, unless it ends in ::clear. A path that ends in ::u or ::t is redirected to the usage and tail views, and one that ends in ::i…, ::e…, ::c… or ::v… to the source or versions view. Any other path of this family gets an empty 404, for example /:a::tx;
  • paths that end in ::qr or ::clear, and paths that contain /::edge;
  • ::kv, ::owner and ::meta at the end of a path;
  • exactly /favicon.ico and /robots.txt on a host root. The same names under a path route, such as /docs/favicon.ico, are not affected.

Other ::x suffixes reach the worker with :x in the pathname: /::zz arrives as /:zz and /:a::zz as /a::zz. See Reserved paths and URL suffixes.

CORS preflights

A deployed worker answers its own CORS preflights. Preflights that RTCode.io pages send, such as a playground output that calls your deployed API, are answered by the platform and never reach the worker. Preflights from other sites do reach it, as non-GET calls. The example below shows a worker that serves other sites.

The incoming request

Outbound requests

Since shell 2025-09-29 the wrapper patches the global fetch(). A string or URL input is parsed against the base https://none:

Never call fetch('/data.json') at the edge. Use env.FS for files that belong to your page, or an absolute URL. Older shells have no patch, so a protocol-relative or relative URL may throw an invalid-URL error until you redeploy.

To call another deployment, use an absolute URL or the in.rt.ht gateway: https://in.rt.ht/<route>/<path> reaches any route through the edge router, and a bare name gets .rt.ht appended, as in //in.rt.ht/<route>/:….

Every outbound request passes through a platform outbound worker. It removes request headers whose names start with rt-, and the Host header, and adds its own rt- headers that identify the RTEdge route and sign the request. connect(), the TCP sockets of cloudflare:sockets, is unavailable.

Response headers

The response rules of the platform apply to the responses of workers as well. Every host gets Server-Actual: RTEdge.net and Server-Vendor. Hosts under rt.ht get Strict-Transport-Security and Referrer-Policy: unsafe-url, which overrides a Referrer-Policy that your worker sets, and hosts that are not under rt.ht get Edge: RTEdge.net. The platform does not cache worker responses, and the Cache-Control of your response passes through to the client. See Response headers.

Errors and status codes

SituationStatusPage
The handler throws, or its promise rejects500Exception, with Worker encountered an exception. and a <pre> with the ray ID and location, an ISO time and the first two stack lines.
The handler returns undefined501Not Implemented, with Worker response is undefined., the ray, the location and the time.
A : call to a route without an edge worker, or to a route that does not exist404Not Found, with Worker does not exist.

These pages are HTML and carry no edge-worker header. Throw Error objects: a thrown string has no stack to show. A failure outside your handler, such as an exceeded resource limit, does not produce these pages, so do not depend on a specific response for it. In the playground, the equivalents are an HTTP 500 text response with a stack when io:fail is closed, and a pass-through to the network when it is open. See Error pages.

Post-quantum Web Crypto

Workers uploaded on or after 2026-10-01 have ML-KEM-768 and ML-KEM-1024 for key encapsulation and ML-DSA-44, ML-DSA-65 and ML-DSA-87 for signatures in crypto.subtle. Redeploy older workers to get them. In the playground, support depends on the browser. See Post-quantum cryptography.

Logging

The console output of a deployed worker does not appear in responses. Tail logs Experimental collect it for the route; see Usage and tail logs.

Limits

RTEdge.net sets no per-route limits of its own, so Cloudflare's default limits for Workers apply: CPU time, memory, subrequests, script size and startup time. See the Cloudflare Workers limits. Exceeding a limit fails the invocation.

By default, an account can have edge workers on up to 5 routes, within a total deployment size of 1 MB. The size counts the stored deployment, not only your source. See Limits and quotas.

What is not supported

Examples

A worker API behind colon URLs

The same code runs in the playground and at the edge. where tells you which.

html
<output id=out></output>
<script type=module>
  out.value = JSON.stringify(await fetch(':hello?name=Ada').then(r => r.json()))
</script>
<script type=worker edge>
  export default {
    fetch: request => {
      const { pathname, searchParams } = new URL(request.url)
      return Response.json({ pathname, name: searchParams.get('name'), where: request.rt.edge ? 'edge' : request.rt.code ? 'playground' : '?' })
    }
  }
</script>

In the playground the output shows {"pathname":"/hello","name":"Ada","where":"playground"}. After you deploy to name.rt.ht, https://name.rt.ht/:hello?name=Ada returns the same JSON with "where":"edge". On a path route such as name.rt.ht/docs/, the relative :hello resolves to /docs/:hello, and the worker still sees /hello.

Outbound requests that work at the edge

js
export default {
  fetch: async () => {
    const other = await fetch('https://in.rt.ht/wasm/:1+2').then(r => r.json())                   // another deployment, through the gateway
    const pkg   = await fetch('//cdn.jsdelivr.net/npm/marked/package.json').then(r => r.json())   // '//' becomes https:// at the edge
    return Response.json({ sum: other.streaming.z, marked: pkg.version })
  }
}

Serving other sites

A preflight from another site arrives as an OPTIONS call. The worker answers it and adds CORS headers to its real responses.

js
const cors = { 'access-control-allow-origin': '*', 'access-control-allow-headers': '*' }
export default {
  fetch: request => request.method === 'OPTIONS'
    ? new Response(null, { status: 204, headers: cors })
    : Response.json({ ok: true }, { headers: cors })
}

See also