Why Cross-Language RPC Matters

For years, getting Python and JavaScript to talk to each other meant one of two things: building a custom HTTP API, or adopting a language-agnostic serialization format like Protocol Buffers or gRPC. Both approaches add friction — schemas to maintain, codegen steps to wire up, and a mental model that never quite feels native to either language.

Cloudflare Workers has been shipping a JavaScript-native RPC system built on Cap'n Proto for a couple of years now. It lets Workers call methods on other Workers and Durable Objects as if they were local functions — no schema definitions, no dependencies. Last year that same primitive was extended to browsers via Cap'n Web.

Now it's cross-language. A JavaScript Worker can call a method on a Python Worker, and a Python Worker can call back into JavaScript. Objects, functions, streams, and exceptions all cross the boundary. The goal is simple: make multi-language systems feel like importing a library.

If you've been putting off Python on the edge because of glue-code overhead, this changes the calculus. The evidence for this write-up comes from Cloudflare's own engineering blog — 근거자료.

Python and JavaScript code editors side by side illustrating cross-language RPC type bridging Coding Session Visual

The 30-Second Version

Define an RPC method in a TypeScript Worker:

import { WorkerEntrypoint } from "cloudflare:workers";

export class RpcService extends WorkerEntrypoint {
  async add(a: number, b: number): Promise<number> {
    return a + b;
  }
}

Call it from Python — no imports, no SDK, no schema:

from workers import Response, WorkerEntrypoint

class Default(WorkerEntrypoint):
    async def fetch(self, request):
        # Get the RPC stub from the TypeScript Worker.
        rpc = self.env.RPC
        # Call the TypeScript RPC method.
        result = await rpc.add(42, 144)
        return Response.json({"result": result})

Wire it up with a single service binding in wrangler.jsonc:

"services": [
  {
    "binding": "RPC",
    "service": "ts-rpc-server",
    "entrypoint": "RpcService"
  }
]

That's it. rpc.add(42, 144) returns a promise on the JS side and a future on the Python side. Exceptions propagate and throw at the call site.

Real-World Example: Pygments From JavaScript

Want to use a Python package in a JavaScript app? Here's how to expose Pygments (a syntax highlighter) as an RPC method.

TypeScript side — call the Python Worker:

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);
  }
}

Python side — implement the method:

from workers import WorkerEntrypoint
from pygments import highlight
from pygments.formatters import HtmlFormatter
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}

Bind them together in the JS Worker's config:

"services": [
  { "binding": "PYTHON_RPC", "service": "py-rpc-server" }
]

Run both dev servers in separate terminals:

# Terminal 1 — JS Worker
npx wrangler dev

# Terminal 2 — Python Worker
uv run pywrangler dev

A full working example lives in the Cloudflare python-workers-examples repo.

Cloudflare Workers serverless edge architecture diagram showing Python and TypeScript workers communicating via RPC Developer Related Image

How the Type Bridge Actually Works

Cross-language RPC only works if the type systems agree. Cloudflare leans on Pyodide's Foreign Function Interface (FFI) — the same CPython-to-WebAssembly layer that has powered Python Workers from day one — plus a thin custom conversion layer for Workers-specific objects.

Built-in Type Mapping

Python TypeJavaScript Equivalent
int, floatNumber
boolBoolean
dictObject
listArray
datetimeDate

When no direct mapping exists (custom classes, functions), Pyodide creates a Proxy that forwards attribute access and method calls across the boundary. This is what lets you pass a Python function as a callback into JavaScript.

Keyword Arguments Just Work

A JS method like get(key, options?) can be called from Python either way:

# Object-style (dictionary)
JSRPC.get("myKey", { "type": "text" })

# Native Python keyword arguments
JSRPC.get("myKey", type="text")

Both translate to the exact shape the JS Worker expects.

Handling Web API Objects

Pyodide doesn't natively understand Request, Response, Blob, or File. By default it wraps them in JavaScript proxies — functional, but leaky. The workers-runtime-sdk package (bundled automatically when you deploy with uv run pywrangler deploy) intercepts these objects and converts them into idiomatic Python forms.

from workers import Response  # you're already using the SDK

Limitations and Gotchas

  • Not every type round-trips cleanly. Structured Cloneable types work; anything with circular references or host-bound state won't.
  • Zero-copy isn't guaranteed. Large payloads still serialize across the boundary — don't assume passing a 100 MB array is free.
  • Same-thread assumption. RPC between Workers usually runs in the same thread as the caller, giving near-zero overhead. If you force cross-network calls, that advantage disappears.
  • Python Worker cold starts still exist. Pyodide boot time is real; keep heavy Pygments-style imports lazy if latency matters.
  • Debugging is harder. Stack traces span two runtimes. Budget extra time when things go wrong.

Developers deploying Python Workers with pywrangler on Cloudflare edge network dashboard Technical Structure Concept

Where to Go Next

If you're already running Workers, the fastest win is to take a Python library you've been jealous of — Pygments, Pillow, pandas (within size limits) — and wrap it as an RPC method. The binding config is three lines.

For teams building polyglot systems, this is a meaningful shift: you no longer have to pick a language for the whole service. Write the hot path in TypeScript, keep the data-crunching in Python, and let RPC handle the seam.

If you're exploring how platform-level access controls shape team workflows, it's worth reading Vercel Just Gave Pro Teams the Developer Role — What It Means for Your Workflow — the same granularity trend is showing up across every edge platform.

And if you're weighing where to run analytics-heavy workloads that sit next to your Workers, Real-Time ERP Analytics on AWS: How Oldcastle Solved Batch Reporting with Aurora and QuickSight is a solid case study for the data side of the same architecture.

Next steps: read the Cloudflare RPC docs, then try the python-workers-examples repo. Ship something small this week.

This content was drafted using AI tools based on reliable sources, and has been reviewed by our editorial team before publication. It is not intended to replace professional advice.