Skip to content

Service Bindings

Service Bindings enable Worker-to-Worker communication with zero network overhead. Instead of routing through the public internet, a Service Binding calls the target Worker’s fetch() handler directly via in-process communication within the same Cloudflare data center.

When a Cloudflare Worker makes a regular fetch() call to another Worker’s public URL, the request traverses the full HTTP stack — DNS resolution, TLS handshake, routing, and serialization. A Service Binding bypasses all of this. The Workers runtime resolves the binding to the target Worker at the platform level and invokes it directly. There is no cold start, no DNS lookup, and no network hop.

This makes Service Bindings the optimal upstream type for internal microservice communication on Cloudflare Workers.

Use the service-binding upstream type and reference the binding name from your wrangler.toml:

import { createGateway, jwtAuth, rateLimit } from "@homegrower-club/stoma";
const gateway = createGateway({
name: "api-gateway",
basePath: "/api",
routes: [
{
path: "/auth/*",
pipeline: {
policies: [rateLimit({ max: 200 })],
upstream: {
type: "service-binding",
service: "AUTH_WORKER",
rewritePath: (path) => path.replace("/api/auth", ""),
},
},
},
{
path: "/projects/*",
pipeline: {
policies: [
jwtAuth({ secret: env.JWT_SECRET }),
rateLimit({ max: 500 }),
],
upstream: {
type: "service-binding",
service: "PROJECTS_WORKER",
rewritePath: (path) => path.replace("/api/projects", ""),
},
},
},
],
});
export default gateway.app;

The service field in the gateway config must match the binding name in your wrangler.toml. The service field in wrangler.toml is the name of the target Worker as deployed to your Cloudflare account:

[[services]]
binding = "AUTH_WORKER"
service = "auth-worker"
[[services]]
binding = "PROJECTS_WORKER"
service = "projects-worker"

The optional rewritePath function transforms the incoming request path before forwarding it to the bound service. This lets the target Worker define its own route structure without knowing about the gateway’s basePath or route prefixes:

{
path: "/users/*",
pipeline: {
upstream: {
type: "service-binding",
service: "USERS_WORKER",
// /api/users/123 becomes /123
rewritePath: (path) => path.replace("/api/users", ""),
},
},
}

At request time, the gateway:

  1. Reads c.env[service] from the Hono context (populated by the Workers runtime)
  2. Validates that the binding exists and has a fetch method
  3. Applies rewritePath if configured
  4. Clones the request headers and strips hop-by-hop headers (RFC 2616)
  5. Propagates W3C traceparent with a fresh span ID for the upstream leg
  6. Calls binding.fetch() with the rewritten request
  7. Returns the upstream response with hop-by-hop headers stripped

If the binding is not available (missing from wrangler.toml or misconfigured), the gateway returns a 502 error:

{
"error": "upstream_error",
"message": "Service binding \"AUTH_WORKER\" is not available in the Worker environment",
"statusCode": 502
}
CharacteristicService BindingURL Proxy (fetch())
LatencyNear-zero (in-process)Variable (network RTT)
Cold startsNone (co-located)Possible
DNS resolutionNoneRequired
TLS handshakeNoneRequired
Data centerSame colo guaranteedMay cross colos
BillingIncluded in callerSeparate per-request

The upstream config is typed as a ServiceBindingUpstream:

interface ServiceBindingUpstream {
type: "service-binding";
/** Name of the Service Binding in wrangler.toml (e.g. "AUTH_SERVICE"). */
service: string;
/** Rewrite the path before forwarding to the bound service. */
rewritePath?: (path: string) => string;
}