close
Skip to content

Dynamic dispatch Worker

Last updated View as MarkdownAgent setup

A dynamic dispatch Worker is a specialized routing Worker that directs incoming requests to the appropriate user Workers in your dispatch namespace. Instead of using Workers Routes, dispatch Workers let you programmatically control request routing through code.

Figure 1: Workers for Platforms: Main Flow

Why use a dynamic dispatch Worker?

  • Scale: Route requests to millions of hostnames to different Workers, without defining Workers Routes configuration for each one
  • Custom routing logic: Write code to determine exactly how requests should be routed. For example:
    • Store hostname-to-Worker mappings in Workers KV and look them up dynamically
    • Route requests based on subdomain, path, headers, or other request properties
    • Use custom metadata attached to custom hostnames for routing decisions
  • Add platform functionality: Build additional features at the routing layer:
    • Run authentication checks before requests reach user Workers
    • Remove or add headers or metadata from incoming requests
    • Attach useful context like user IDs or account information
    • Transform requests or responses as needed

Configure the dispatch namespace binding

To allow your dynamic dispatch Worker to dynamically route requests to Workers in a namespace, you need to configure a dispatch namespace binding. This binding enables your dynamic dispatch Worker to call any user Worker within that namespace using env.dispatcher.get().

{
	"dispatch_namespaces": [
		{
			"binding": "DISPATCHER",
			"namespace": "my-dispatch-namespace"
		}
	]
}
[[dispatch_namespaces]]
binding = "DISPATCHER"
namespace = "my-dispatch-namespace"

Once the binding is configured, your dynamic dispatch Worker can route requests to any Worker in the namespace. Below are common routing patterns you can implement in your dispatcher.

Pass data and capabilities per request

When your dispatch Worker invokes a user Worker, it can send additional values with that invocation. For example, the dispatch Worker can authenticate a request and send the resulting user ID, permissions, or account information to the user Worker.

This is useful when the context should come from your platform code instead of directly from the incoming request. To send this context, add the values to props in the second argument to env.DISPATCHER.get().

In the dispatch Worker, pass the user ID and permissions when you retrieve the user Worker from the dispatch namespace:

dispatcher/src/index.jsjs
export default {
	async fetch(request, env) {
		const userId = "user-123";
		const permissions = ["read", "write"];

		const userWorker = env.DISPATCHER.get("user-worker", {
			props: { userId, permissions },
		});

		return userWorker.fetch(request);
	},
};
dispatcher/src/index.tsts
export default {
	async fetch(request, env): Promise<Response> {
		const userId = "user-123";
		const permissions = ["read", "write"];

		const userWorker = env.DISPATCHER.get("user-worker", {
			props: { userId, permissions },
		});

		return userWorker.fetch(request);
	},
} satisfies ExportedHandler<Cloudflare.Env>;

In the user Worker, receive these values through ctx.props. In a WorkerEntrypoint, access them through this.ctx.props:

user-worker/src/index.jsjs
import { WorkerEntrypoint } from "cloudflare:workers";

export default class UserWorker extends WorkerEntrypoint {
	async fetch(_request) {
		const { userId, permissions } = this.ctx.props;
		return Response.json({ userId, permissions });
	}
}
user-worker/src/index.tsts
import { WorkerEntrypoint } from "cloudflare:workers";

interface UserWorkerProps {
	userId: string;
	permissions: string[];
}

export default class UserWorker extends WorkerEntrypoint<
	Cloudflare.Env,
	UserWorkerProps
> {
	async fetch(_request: Request): Promise<Response> {
		const { userId, permissions } = this.ctx.props;
		return Response.json({ userId, permissions });
	}
}

These data values are visible to the user code. The dispatch Worker can select different props for each invocation without changing or redeploying the user Worker.

Give access to specific platform functions

You may not want the user Worker to have direct access to all the context from the dispatch Worker. For example, authentication data is managed by your platform and should remain hidden from user code.

In this case, pass a capability instead of passing the data directly. A capability exposes specific methods that the user Worker can call, while the underlying data, credentials, and resources remain in the dispatch Worker. Workers passes the capability as an RPC stub, which forwards method calls to your dispatch Worker.

To create a capability, export a WorkerEntrypoint class from your dispatch Worker. The ctx.exports object lets the dispatch Worker create an RPC stub for that exported class, which it can then pass to the user Worker.

The following example shows this pattern. The dispatch Worker uses a site ID and visitor ID to create a Connector capability. It passes the capability to the user Worker through props, without passing those IDs as separate data values. The user Worker can then call the methods exposed by Connector.

In the dispatch Worker, define the Connector methods, configure the connector with the site and visitor IDs, and pass it to the user Worker:

dispatcher/src/index.jsjs
import { WorkerEntrypoint } from "cloudflare:workers";

export class Connector extends WorkerEntrypoint {
	async invoke() {
		return `${this.ctx.props.siteId}:${this.ctx.props.visitorId}`;
	}
}

export default {
	async fetch(request, env, ctx) {
		const siteId = "site-123";
		const visitorId = "visitor-456";
		const connector = ctx.exports.Connector({
			// These props configure the Connector stub.
			// The user Worker cannot read them directly.
			props: { siteId, visitorId },
		});

		const userWorker = env.DISPATCHER.get("user-worker", {
			// The user Worker receives these values through ctx.props.
			props: {
				CONNECTOR: connector,
			},
		});

		return userWorker.fetch(request);
	},
};
dispatcher/src/index.tsts
import { WorkerEntrypoint } from "cloudflare:workers";

interface ConnectorProps {
	siteId: string;
	visitorId: string;
}

export class Connector extends WorkerEntrypoint<
	Cloudflare.Env,
	ConnectorProps
> {
	async invoke(): Promise<string> {
		return `${this.ctx.props.siteId}:${this.ctx.props.visitorId}`;
	}
}

export default {
	async fetch(request, env, ctx): Promise<Response> {
		const siteId = "site-123";
		const visitorId = "visitor-456";
		const connector = ctx.exports.Connector({
			// These props configure the Connector stub.
			// The user Worker cannot read them directly.
			props: { siteId, visitorId },
		});

		const userWorker = env.DISPATCHER.get("user-worker", {
			// The user Worker receives these values through ctx.props.
			props: {
				CONNECTOR: connector,
			},
		});

		return userWorker.fetch(request);
	},
} satisfies ExportedHandler<Cloudflare.Env>;

In the user Worker, receive the capability through this.ctx.props and call its exposed methods:

user-worker/src/index.jsjs
import { WorkerEntrypoint } from "cloudflare:workers";

export default class UserWorker extends WorkerEntrypoint {
	async fetch(_request) {
		return Response.json({
			connector: await this.ctx.props.CONNECTOR.invoke(),
		});
	}
}
user-worker/src/index.tsts
import { WorkerEntrypoint } from "cloudflare:workers";

interface Connector {
	invoke(): Promise<string>;
}

interface UserWorkerProps {
	CONNECTOR: Connector;
}

export default class UserWorker extends WorkerEntrypoint<
	Cloudflare.Env,
	UserWorkerProps
> {
	async fetch(_request: Request): Promise<Response> {
		return Response.json({
			connector: await this.ctx.props.CONNECTOR.invoke(),
		});
	}
}

Pass data to an Outbound Worker

To send data from the dispatch Worker to an Outbound Worker, first declare the parameter name in the dispatch namespace binding:

{
	"dispatch_namespaces": [
		{
			"binding": "DISPATCHER",
			"namespace": "my-dispatch-namespace",
			"outbound": {
				"service": "outbound-worker",
				"parameters": ["requestContext"]
			}
		}
	]
}
[[dispatch_namespaces]]
binding = "DISPATCHER"
namespace = "my-dispatch-namespace"

  [dispatch_namespaces.outbound]
  service = "outbound-worker"
  parameters = [ "requestContext" ]

In the dispatch Worker, pass a value with the same name through the outbound option in the third argument to env.DISPATCHER.get():

dispatcher/src/index.jsjs
export default {
	async fetch(request, env) {
		const userId = "user-123";

		const userWorker = env.DISPATCHER.get(
			"user-worker",
			{
				props: { userId },
			},
			{
				outbound: {
					requestContext: {
						userId,
						requestId: crypto.randomUUID(),
					},
				},
			},
		);

		return userWorker.fetch(request);
	},
};
dispatcher/src/index.tsts
export default {
	async fetch(request, env): Promise<Response> {
		const userId = "user-123";

		const userWorker = env.DISPATCHER.get(
			"user-worker",
			{
				props: { userId },
			},
			{
				outbound: {
					requestContext: {
						userId,
						requestId: crypto.randomUUID(),
					},
				},
			},
		);

		return userWorker.fetch(request);
	},
} satisfies ExportedHandler<Cloudflare.Env>;

In the Outbound Worker, access the value as an environment binding:

outbound-worker/src/index.jsjs
export default {
	async fetch(request, env) {
		console.log(env.requestContext.userId, env.requestContext.requestId);
		return fetch(request);
	},
};
outbound-worker/src/index.tsts
interface Env {
	requestContext: {
		userId: string;
		requestId: string;
	};
}

export default {
	async fetch(request, env): Promise<Response> {
		console.log(env.requestContext.userId, env.requestContext.requestId);
		return fetch(request);
	},
} satisfies ExportedHandler<Env>;

Outbound Worker parameters support JSON values. They are separate from the props in the second argument, which are passed to the user Worker.

Routing examples

Figure 2: Workers for Platforms: Main Flow

KV-Based Routing

Store the routing mappings in Workers KV. This allows you to modify your routing logic without requiring you to change or redeploy the dynamic dispatch Worker.

export default {
	async fetch(request, env) {
		try {
			const url = new URL(request.url);

			// Use hostname, path, or any combination as the routing key
			const routingKey = url.hostname;

			// Lookup user Worker name from KV store
			const userWorkerName = await env.USER_ROUTING.get(routingKey);

			if (!userWorkerName) {
				return new Response("Route not configured", { status: 404 });
			}

			// Optional: Cache the KV lookup result
			const userWorker = env.DISPATCHER.get(userWorkerName);
			return await userWorker.fetch(request);
		} catch (e) {
			if (e.message.startsWith("Worker not found")) {
				return new Response("", { status: 404 });
			}
			return new Response(e.message, { status: 500 });
		}
	},
};

Subdomain-Based Routing

Route subdomains to the corresponding Worker. For example, my-customer.example.com will route to the Worker named my-customer in the dispatch namespace.

export default {
	async fetch(request, env) {
		try {
			// Extract user Worker name from subdomain
			// Example: customer1.example.com -> customer1
			const url = new URL(request.url);
			const userWorkerName = url.hostname.split(".")[0];

			// Get user Worker from dispatch namespace
			const userWorker = env.DISPATCHER.get(userWorkerName);
			return await userWorker.fetch(request);
		} catch (e) {
			if (e.message.startsWith("Worker not found")) {
				// User Worker doesn't exist in dispatch namespace
				return new Response("", { status: 404 });
			}
			// Could be any other exception from fetch() or from the dispatched Worker
			return new Response(e.message, { status: 500 });
		}
	},
};

Path-Based routing

Route URL paths to the corresponding Worker. For example, example.com/customer-1 will route to the Worker named customer-1 in the dispatch namespace.

export default {
	async fetch(request, env) {
		try {
			const url = new URL(request.url);
			const pathParts = url.pathname.split("/").filter(Boolean);

			if (pathParts.length === 0) {
				return new Response("Invalid path", { status: 400 });
			}

			// example.com/customer-1 -> routes to 'customer-1' worker
			const userWorkerName = pathParts[0];

			const userWorker = env.DISPATCHER.get(userWorkerName);
			return await userWorker.fetch(request);
		} catch (e) {
			if (e.message.startsWith("Worker not found")) {
				return new Response("", { status: 404 });
			}
			return new Response(e.message, { status: 500 });
		}
	},
};

Was this helpful?