Skip to content

Polyglot gRPC — Multi-Language gRPC Services with Cross-Language Interop

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Polyglot gRPC. We cover key concepts, practical examples, and best practices to help you master this topic.

Polyglot gRPC enables services written in different languages (Go, Python, TypeScript, Java) to communicate seamlessly using shared protobuf definitions and the wire-compatible gRPC protocol.

What You'll Learn

  • Shared protobuf definitions across languages
  • Go server with Python and Node.js clients
  • Cross-language error handling compatibility
  • Streaming across language boundaries
  • Testing cross-language interop
  • Common cross-language pitfalls

Why It Matters

Microservice architectures rarely use one language. Teams choose the best language for each service's job — Go for high-performance services, Python for ML, Node.js for I/O-bound services. gRPC's cross-language support is critical for these polyglot architectures. DodaTech's Durga Antivirus Pro uses Go for core services, Python for ML analysis, and TypeScript for the web dashboard, all communicating via shared protobuf definitions.

Real-World Use

A threat analysis request flows through: Go API Gateway -> Python ML service (threat scoring) -> Go database service. Each service uses the same protobuf definition. The Go client calls the Python service seamlessly, and errors propagate correctly across the language boundary.

flowchart LR
    A["Go API Gateway"] --> B["Python ML Service\n(threat scoring)"]
    A --> C["Node.js Notification\n(real-time alerts)"]
    B --> D["Go Database Service"]
    C --> D
    E["Shared Protobuf\nthreat/v1/threat.proto"] --> A
    E --> B
    E --> C
    E --> D
    style E fill:#fef3c7,stroke:#d97706

Code Examples

Example 1: Shared Protobuf Definition

syntax = "proto3";

package threat.v1;

// Shared across Go, Python, Node.js
service ThreatService {
  rpc ReportThreat(ThreatRequest) returns (ThreatResponse);
  rpc StreamThreats(StreamRequest) returns (stream ThreatAlert);
  rpc AnalyzeThreat(AnalyzeRequest) returns (AnalyzeResponse);
}

message ThreatRequest {
  string device_id = 1;
  string threat_name = 2;
  Severity severity = 3;
  bytes file_hash = 4;
}

message ThreatResponse {
  string threat_id = 1;
  ThreatStatus status = 2;
  string message = 3;
}

enum Severity {
  SEVERITY_UNSPECIFIED = 0;
  LOW = 1;
  MEDIUM = 2;
  HIGH = 3;
  CRITICAL = 4;
}

enum ThreatStatus {
  STATUS_UNSPECIFIED = 0;
  QUARANTINED = 1;
  BLOCKED = 2;
  CLEANED = 3;
  PENDING_ANALYSIS = 4;
}

Example 2: Go Server with Python Client

// Go Server
package main

import (
    "context"
    "log"
    "net"
    
    "google.golang.org/grpc"
    pb "path/to/threat/v1"
)

type server struct {
    pb.UnimplementedThreatServiceServer
}

func (s *server) ReportThreat(ctx context.Context,
    req *pb.ThreatRequest) (*pb.ThreatResponse, error) {
    
    log.Printf("Received threat from %s: %s (severity: %v)",
        req.DeviceId, req.ThreatName, req.Severity)
    
    return &pb.ThreatResponse{
        ThreatId: generateID(),
        Status:   pb.ThreatStatus_QUARANTINED,
        Message:  "Threat quarantined successfully",
    }, nil
}

func main() {
    lis, _ := net.Listen("tcp", ":50051")
    s := grpc.NewServer()
    pb.RegisterThreatServiceServer(s, &server{})
    s.Serve(lis)
}
# Python Client
import grpc
from threat.v1 import threat_pb2, threat_pb2_grpc

def report_threat_from_python():
    channel = grpc.insecure_channel("localhost:50051")
    stub = threat_pb2_grpc.ThreatServiceStub(channel)
    
    request = threat_pb2.ThreatRequest(
        device_id="dev-python-001",
        threat_name="Cross-Language-Malware",
        severity=threat_pb2.HIGH,
        file_hash=b"\x00\x01\x02\x03",
    )
    
    response = stub.ReportThreat(request)
    print(f"Threat ID: {response.threat_id}")
    print(f"Status: {threat_pb2.ThreatStatus.Name(response.status)}")

Example 3: Node.js Client for Streaming

// Node.js gRPC Client
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const packageDef = protoLoader.loadSync(
  '../protos/threat/v1/threat.proto',
  { keepCase: false, defaults: true }
);

const proto = grpc.loadPackageDefinition(packageDef);
const client = new proto.threat.v1.ThreatService(
  'localhost:50051',
  grpc.credentials.createInsecure()
);

// Unary call from Node.js to Go server
const request = {
  device_id: 'dev-node-001',
  threat_name: 'NodeMalware',
  severity: 'CRITICAL',
};

client.ReportThreat(request, (error, response) => {
  if (error) {
    console.error('Cross-language error:', error);
    // gRPC error codes are consistent across languages
    if (error.code === grpc.status.INVALID_ARGUMENT) {
      console.log('Go server returned validation error');
    }
    return;
  }
  console.log(`Go server responded: ${response.status}`);
});

// Streaming from Go server
const stream = client.StreamThreats({ device_id: 'dev-001' });
stream.on('data', (alert) => {
  console.log(`Alert from Go server: ${alert.threat_name}`);
});

Common Mistakes

  1. Using language-specific protobuf features — some languages have custom protobuf options (gogoproto in Go). Avoid them in shared proto files.
  2. Inconsistent enum naming across languages — protobuf enum values use UPPERCASE in Go, PascalCase in Python, and as-defined in JS. Use the numeric value for cross-language checks.
  3. Assuming field ordering in JSON mapping — JSON field ordering is not guaranteed. Always use field names, not positions, for JSON deserialization.
  4. Different timestamp handling — Go's time.Time vs Python's datetime vs JS's Date. Use google.protobuf.Timestamp for cross-language date/time.
  5. Ignoring error code mapping — gRPC status codes are consistent across languages, but error detail types may be language-specific. Use google.rpc.Status for portable error details.

Practice Questions

  1. Why should you avoid language-specific protobuf options in shared definitions?
  2. How do protobuf enums differ across Go, Python, and Node.js?
  3. What type should you use for timestamps in cross-language protobuf?
  4. How are gRPC error codes handled differently across languages?
  5. What Serialization format ensures the best cross-language compatibility?

Challenge: Build a 3-service polyglot gRPC system: Go server for threat storage, Python server for ML analysis, Node.js client that calls both. Share one protobuf file across all three. Test that errors, enums, and streaming work consistently.

Mini Project

Build a polyglot gRPC demo with: Go API server, Python ML service, Node.js notification service, and TypeScript web client (gRPC-Web). All services share protobuf definitions. Include cross-language error handling, enum consistency tests, and streaming across language boundaries.

FAQ

What languages does gRPC support?

First-class: Go, Java, Python, Node.js, C++, C#, Ruby, PHP, Dart. Community: Rust, Swift, Kotlin, TypeScript (gRPC-Web).

Is there performance overhead for cross-language gRPC?

No. gRPC wire format (protobuf over HTTP/2) is identical regardless of language. Cross-language calls have the same performance as same-language calls.

How do I share protobuf files across teams?

Use a monorepo for protos, or publish proto files as a package (npm, pip, Go module). Use buf.build for proto management across teams.

Can I call gRPC from the browser?

Yes, via gRPC-Web (JavaScript/TypeScript) which translates HTTP/2 gRPC to HTTP/1.1 fetch requests through an Envoy proxy.

What is the best way to test cross-language interop?

Run the gRPC interop test suite (grpc-interop-test). It tests 20+ scenarios including empty messages, large messages, streaming, and status codes across languages.

What's Next

Learn about gRPC-Web for browser clients

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro