Workers and Containers open up to inbound TCP and gRPC

Cloudflare is expanding its Workers platform to handle inbound TCP connections directly, opening the door for full-duplex, bidirectional gRPC services. The company announced three capabilities in private beta: a new connect() handler for Workers, gRPC support for Cloudflare Containers, and gRPC-to-gRPC-web translation for Workers that allows them to act as gRPC servers or clients.

These features address a growing need for low-latency communication in voice AI and real-time applications. Many such systems rely on gRPC, an RPC framework built on HTTP/2 and TCP that supports persistent, bidirectional connections. Previously, Workers could initiate outbound TCP connections but could not accept inbound ones.

The connect() handler: accepting inbound TCP

Workers can now accept incoming TCP sockets via a new connect() handler in the Workers runtime. The handler provides a socket that the Worker can read from and write to:

export default {
	async connect(socket): Promise<void> {
		const writer = socket.writable.getWriter();
		await writer.write(new TextEncoder().encode("Hello, world!\n"));
		await writer.close();
	},
} satisfies ExportedHandler;

Because the socket is a first-class object, it can be passed between Workers, to Durable Objects, or from a Durable Object to its associated Container:

import { DurableObject } from "cloudflare:workers";

export class SocketDurableObject extends DurableObject<Env> {
	async connect(socket: Socket): Promise<void> {
		// Echo bytes from inside the Durable Object
		await socket.readable.pipeTo(socket.writable);
	}
}

export default {
	async connect(socket, env): Promise<void> {
		const stub = env.SOCKET_DO.getByName("my-server");
		const durableObjectSocket = stub.connect("host:port");

		await Promise.all([
			socket.readable.pipeTo(durableObjectSocket.writable),
			durableObjectSocket.readable.pipeTo(socket.writable),
		]);
	},
} satisfies ExportedHandler<Env>;
import { DurableObject } from "cloudflare:workers";

export class SocketContainer extends DurableObject<Env> {
	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		this.ctx.container!.start();
	}

	async connect(socket: Socket): Promise<void> {
		const containerSocket = this.ctx.container!
			.getTcpPort(8080)
			.connect("10.0.0.1:8080");

		await containerSocket.opened;

		await Promise.all([
			socket.readable.pipeTo(containerSocket.writable),
			containerSocket.readable.pipeTo(socket.writable),
		]);
	}
}

Once the socket arrives at a Container, it can be handled directly:

# server.py
import socketserver

class Handler(socketserver.BaseRequestHandler):
    def handle(self):
        while data := self.request.recv(64 * 1024):
            self.request.sendall(b"Echo: " + data)

class Server(socketserver.ThreadingTCPServer):
    allow_reuse_address = True
    daemon_threads = True

with Server(("0.0.0.0", 8080), Handler) as server:
    server.serve_forever()

This means developers have full control over the path from client to server, supporting any TCP-based protocol or application code. To expose the raw TCP endpoint to clients, Cloudflare is introducing a new Spectrum application type that routes incoming TCP connections to a designated Worker. Spectrum already serves as Cloudflare's ingress proxy for non-HTTP TCP and UDP traffic.

Bidirectional gRPC from Containers

gRPC, originally released by Google nearly a decade ago, is widely used in mobile apps, distributed systems, and more recently voice AI. Real-time voice applications typically require a persistent connection where both client and server can send messages freely. While WebSockets and Durable Objects work well for this, a large ecosystem of existing software depends on gRPC.

With the new connect() handler and socket routing, developers can deploy gRPC servers on Cloudflare Containers—in any language—with complete support for bidirectional streaming. This allows services to run closer to users across a network of 330+ locations, which is important for latency-sensitive voice workloads and colocated inference.

A minimal gRPC echo server, shown below, illustrates how straightforward this deployment can be:

package main

import (
	"io"
	"log"
	"net"

	pb "example/proto"
	"google.golang.org/grpc"
)

type server struct {
	pb.UnimplementedByteStreamServer
}

func (server) Chat(stream pb.ByteStream_ChatServer) error {
	if err := stream.Send(&pb.ByteChunk{
		Payload: []byte("connected\n"),
	}); err != nil {
		return err
	}

	for {
		message, err := stream.Recv()

		if err == io.EOF {
			return stream.Send(&pb.ByteChunk{
				Payload: []byte("goodbye\n"),
			})
		}
		if err != nil {
			return err
		}

		if err := stream.Send(&pb.ByteChunk{
			Payload: append([]byte("echo: "), message.Payload...),
		}); err != nil {
			return err
		}
	}
}

func main() {
	listener, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatal(err)
	}

	grpcServer := grpc.NewServer()
	pb.RegisterByteStreamServer(grpcServer, &server{})

	log.Println("gRPC server listening on :50051")
	log.Fatal(grpcServer.Serve(listener))
}

Workers as gRPC servers and clients without a Container

Not every gRPC use case needs a Container. For simpler cases where a Worker just needs to serve a basic gRPC API or connect to an external gRPC backend, Cloudflare is providing built-in translation between gRPC and gRPC-web.

gRPC-web is the browser-compatible variant of gRPC. Browsers lack the low-level HTTP/2 framing APIs that gRPC requires—and there is no raw TCP socket API in browsers—which is why WebSockets exist and why Workers have supported them since 2021. HTTP/2 itself is frame-based, enabling multiplexing over a single connection with stream-level control for requests, responses, cancellation, flow control, and trailers. The fetch() API, however, does not expose this level of control.

Cloudflare's solution is protocol translation: incoming gRPC requests are converted to gRPC-web before reaching the Worker, and outgoing calls from the Worker are translated back to full gRPC. Cloudflare has used this approach internally since 2020, when its reverse proxy started converting gRPC to HTTP/1.1 so that traffic could be inspected and protected by security features like WAF and Bot Management.

Given a protobuf definition file:

syntax = "proto3";

package hello;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

A unary gRPC server can be built in a Worker with just a few lines of code using the open-source @connectrpc/connect package:

import { createConnectRouter } from "@connectrpc/connect";
import {
  universalServerRequestFromFetch,
  universalServerResponseToFetch,
} from "@connectrpc/connect/protocol";
import { Greeter } from "./gen/hello_pb";

const router = createConnectRouter();

router.service(Greeter, {
  sayHello: ({ name }) => ({ message: `Hello, ${name}!` }),
});

const handlers = new Map(
  router.handlers.map((handler) => [handler.requestPath, handler]),
);

export default {
  async fetch(request: Request): Promise<Response> {
    const handler = handlers.get(new URL(request.url).pathname);
    return universalServerResponseToFetch(
      await handler(universalServerRequestFromFetch(request, {})),
    );
  },
} satisfies ExportedHandler;

Outbound requests to external gRPC servers can be made the same way, using the client included in @connectrpc/connect:

import { createClient } from "@connectrpc/connect";
import { createGrpcWebTransport } from "@connectrpc/connect-web";
import { Greeter } from "./gen/hello_pb";

const client = createClient(
  Greeter,
  createGrpcWebTransport({
    baseUrl: "https://grpc.example.com",
    fetch: (input, init) =>
      fetch(input, { ...init, redirect: "manual" }),
  }),
);

export default {
  async fetch(): Promise<Response> {
    const reply = await client.sayHello({ name: "Workers" });
    return Response.json(reply);
  },
} satisfies ExportedHandler;

The code always works with gRPC-web; translation to and from full gRPC happens automatically at the edge. This means existing clients and servers require no changes.

Two use cases are immediately practical:

  • Mobile backend for gRPC apps — Mobile applications often use native gRPC libraries such as grpc-swift-2 and grpc-kotlin for efficient payloads and strongly typed clients. Workers can now serve as the backend for those apps.
  • Worker in front of an existing gRPC service — Developers who already front REST APIs with Workers can do the same for gRPC backends, moving critical work closer to users or incrementally migrating state into Durable Objects.

Roadmap

All of these capabilities are in private beta. Cloudflare says it is deliberately rolling out gRPC support gradually, working with a small set of developers first. The company itself uses Cap'n Proto, Cap'n Web, and its own JavaScript-native RPC system in Workers, rather than gRPC, so it wants to validate the offering with external users before a wider release.

The broader direction, Cloudflare notes, is to keep expanding the types of traffic Workers can handle—moving beyond TCP and eventually into UDP-based protocols.