Workers RPC crosses the language barrier
Two years after introducing JavaScript-native RPC, Cloudflare has extended its Workers RPC system to work across JavaScript and Python. The feature, built on Cap'n Proto RPC, lets Workers written in different languages call each other's methods directly, share objects, and pass functions back and forth — no schemas, dependencies, or serialization formats like protobuf required.
The system works in both directions: a TypeScript Worker can call a method defined in a Python Worker and vice versa. For example, a method add() defined in TypeScript can be called from Python with no extra configuration beyond a Service binding.

Cross-language RPC characteristics
- RPC calls behave like ordinary function calls, returning promises in JavaScript/TypeScript and futures in Python. Exceptions propagate and are thrown at the call site.
- Any Structured Cloneable type can be used as a parameter or return value and is converted to the appropriate type — a JS
Datebecomes a Pythondatetime, for example. - Functions can be passed from one language to the other; when the receiving side calls the function, it makes a new RPC back to the origin.
- When the other Worker runs in the same thread — the typical case — there is near-zero performance overhead compared to same-Worker code.
- The implementation is open source in
workerdand the workers-runtime-sdk Python package.
Bridging type systems
The core technical challenge is translating between JavaScript and Python's differently structured types. JavaScript developers commonly pass an Object as a function argument; Python developers typically use keyword arguments. The goal was to make the translation invisible so developers on either side write code that feels native.
function myFunction(params: { key: string, value: boolean, optional?: number }) { ... }
// Called like this:
myFunction({ key: “myKey”, value: true, optional: 1 });
def my_complex_function(key: str, value: bool, optional: int | None): ...
# Called like this:
my_complex_function(“myKey”, True, optional=1)
The solution combines Pyodide's Foreign Function Interface (FFI) with a custom type-conversion layer in the workers-runtime-sdk Python package.
Pyodide, the CPython interpreter compiled to WebAssembly that powers Python Workers, already automatically translates types between JavaScript and Python. When a Python Worker communicates with a JavaScript Worker over a Service binding, Pyodide's FFI handles object conversion under the hood. Native types map directly between environments, and when direct translation isn't possible — such as with custom classes — Pyodide creates a Proxy that forwards attribute accesses and method calls across the boundary.

Python Type | JavaScript Equivalent |
int, float | Number |
bool | Boolean |
dict | Object |
list | Array |
Pyodide's FFI also maps Python keyword arguments to JavaScript's object-style parameters. Given a JavaScript method that accepts an options object, a Python caller can either pass a dictionary mirroring the object's structure or use native Python keyword arguments:
async get(key: string, options?: { type: string });
JSRPC.get("myKey", { "type": "text" })
JSRPC.get("myKey", type="text")
Both calling patterns translate into the exact structure the JavaScript Worker expects.
Handling Workers-specific types
Pyodide's FFI doesn't automatically understand Web API objects like Request, Response, Blob, or File. Without explicit handling, these become JavaScript Proxies in Python, leaking implementation details into Python code.
The workers-runtime-sdk package addresses this by wrapping RPC stubs and translating these non-standard objects into native Python forms. The SDK is included by default when deploying a Python Worker with uv run pywrangler deploy, and it's already active whenever you import from the workers namespace.
from workers import Response
...
Calling Python packages from JavaScript
Cross-language RPC makes it possible to use a Python library directly from a JavaScript application. Pygments, the Python syntax highlighting package, serves as a working example.
A JavaScript Worker calls a Python Worker's method via the request's env:
export default {
async fetch(request, env) {
// Get the RPC stub from the Python Worker.
const rpc = env.PYTHON_RPC;
// Call the Python RPC method.
const result = await rpc.highlight_code('print(42)', 'python');
return Response.json(result);
}
}
The Python Worker defines that method and its supporting logic:
from workers import WorkerEntrypoint
class Default(WorkerEntrypoint):
async def highlight_code(self, code: str, language: str) -> dict:
# Implementation goes here
# ...
from pygments.formatters import HtmlFormatter
from pygments import highlight
from pygments.lexers import get_lexer_by_name
class Default(WorkerEntrypoint):
async def highlight_code(self, code: str, language: str) -> dict:
# Retrieve the lexer for the language specified.
lexer = get_lexer_by_name(language, stripall=True)
# Create the formatter and run the highlighter on the specified code.
formatter = HtmlFormatter(linenos=True, cssclass="highlight", style="monokai")
highlighted_html = highlight(code, lexer, formatter)
# Get the CSS for styling.
css = formatter.get_style_defs(".highlight")
return {
"html": highlighted_html,
"css": css
}
The JavaScript Worker's wrangler.jsonc configures the Service binding, where the service name must match the Python Worker's name:
"services": [
{
"binding": "PYTHON_RPC",
"service": "py-rpc-server"
}
]
To test locally, run npx wrangler dev in the JavaScript Worker's directory and uv run pywrangler dev in the Python Worker's directory, each in its own terminal. A complete Pygments-backed example is available on GitHub and can be launched directly:
git clone [email protected]:cloudflare/python-workers-examples.git
cd python-workers-examples/13-js-api-pygments/
# Terminal 1
cd ts/
npx wrangler dev
# Terminal 2
cd py/
uv run pywrangler dev
More examples and RPC documentation are available on Cloudflare's developer site.



