gRPC is a great fit for internal services once REST starts feeling too chatty. You get strongly typed contracts, generated clients, and efficient transport over HTTP/2 without building your own conventions around JSON and versioning.
The Proxy Model
Cloud Run terminates TLS at Google's edge infrastructure. Your container never sees a TLS connection — the Google Front End (GFE) forwards requests to your container over h2c (HTTP/2 cleartext). The TLS is handled externally; the internal leg is plaintext.
This means:
- Your gRPC server uses
grpc.NewServer()with no TLS credentials. The GFE handles TLS for external clients. - For streaming RPCs, you must deploy with
--use-http2. Without it, Cloud Run downgrades the internal connection to HTTP/1.1 between the GFE and your container. HTTP/1.1 cannot carry gRPC streams — in practice, unary RPCs may appear to work but streaming will hang or fail silently. Always set this flag for any gRPC workload.
That single detail explains why your server runs without TLS inside the container, while clients still connect securely over 443.
The Service
// api/v1/ping.proto
syntax = "proto3";
package ping.v1;
option go_package = "github.com/emrecavunt/grpc-cloud-run/pkg/api/v1;pingv1";
service PingService {
rpc Ping(PingRequest) returns (PingResponse);
rpc PingStream(PingRequest) returns (stream PingResponse);
}
message PingRequest { string message = 1; }
message PingResponse { string message = 1; int64 timestamp = 2; }// api/v1/ping.proto
syntax = "proto3";
package ping.v1;
option go_package = "github.com/emrecavunt/grpc-cloud-run/pkg/api/v1;pingv1";
service PingService {
rpc Ping(PingRequest) returns (PingResponse);
rpc PingStream(PingRequest) returns (stream PingResponse);
}
message PingRequest { string message = 1; }
message PingResponse { string message = 1; int64 timestamp = 2; }// cmd/server/main.go
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
pingv1 "github.com/emrecavunt/grpc-cloud-run/pkg/api/v1"
)
type server struct{ pingv1.UnimplementedPingServiceServer }
func (s *server) Ping(_ context.Context, req *pingv1.PingRequest) (*pingv1.PingResponse, error) {
return &pingv1.PingResponse{
Message: fmt.Sprintf("pong: %s", req.GetMessage()),
Timestamp: time.Now().UnixNano() / int64(time.Millisecond),
}, nil
}
func (s *server) PingStream(req *pingv1.PingRequest, stream pingv1.PingService_PingStreamServer) error {
for i := 0; i < 5; i++ {
if err := stream.Send(&pingv1.PingResponse{
Message: fmt.Sprintf("stream %d: %s", i, req.GetMessage()),
Timestamp: time.Now().UnixNano() / int64(time.Millisecond),
}); err != nil {
return err
}
time.Sleep(500 * time.Millisecond)
}
return nil
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
lis, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatalf("listen failed: %v", err)
}
// No TLS — Cloud Run's GFE terminates it externally.
srv := grpc.NewServer()
pingv1.RegisterPingServiceServer(srv, &server{})
reflection.Register(srv) // enables grpcurl without a local proto
log.Printf("serving on port %s", port)
if err := srv.Serve(lis); err != nil {
log.Fatalf("serve failed: %v", err)
}
}// cmd/server/main.go
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
pingv1 "github.com/emrecavunt/grpc-cloud-run/pkg/api/v1"
)
type server struct{ pingv1.UnimplementedPingServiceServer }
func (s *server) Ping(_ context.Context, req *pingv1.PingRequest) (*pingv1.PingResponse, error) {
return &pingv1.PingResponse{
Message: fmt.Sprintf("pong: %s", req.GetMessage()),
Timestamp: time.Now().UnixNano() / int64(time.Millisecond),
}, nil
}
func (s *server) PingStream(req *pingv1.PingRequest, stream pingv1.PingService_PingStreamServer) error {
for i := 0; i < 5; i++ {
if err := stream.Send(&pingv1.PingResponse{
Message: fmt.Sprintf("stream %d: %s", i, req.GetMessage()),
Timestamp: time.Now().UnixNano() / int64(time.Millisecond),
}); err != nil {
return err
}
time.Sleep(500 * time.Millisecond)
}
return nil
}
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
lis, err := net.Listen("tcp", ":"+port)
if err != nil {
log.Fatalf("listen failed: %v", err)
}
// No TLS — Cloud Run's GFE terminates it externally.
srv := grpc.NewServer()
pingv1.RegisterPingServiceServer(srv, &server{})
reflection.Register(srv) // enables grpcurl without a local proto
log.Printf("serving on port %s", port)
if err := srv.Serve(lis); err != nil {
log.Fatalf("serve failed: %v", err)
}
}Deploy
gcloud builds submit --tag gcr.io/$PROJECT_ID/grpc-ping
gcloud run deploy grpc-ping \
--image gcr.io/$PROJECT_ID/grpc-ping \
--region europe-west1 \
--use-http2 \
--no-allow-unauthenticatedgcloud builds submit --tag gcr.io/$PROJECT_ID/grpc-ping
gcloud run deploy grpc-ping \
--image gcr.io/$PROJECT_ID/grpc-ping \
--region europe-west1 \
--use-http2 \
--no-allow-unauthenticated--use-http2 is required. Set it for every gRPC service no downside, and without it you will lose streaming in a way that isn't obvious until you're staring at a hanging client.
Test It
TOKEN=$(gcloud auth print-identity-token)
HOST=$(gcloud run services describe grpc-ping --region europe-west1 --format 'value(status.url)')
HOST=${HOST#https://}
grpcurl -H "Authorization: Bearer $TOKEN" \
-d '{"message": "hello"}' \
$HOST:443 ping.v1.PingService/Ping
grpcurl -H "Authorization: Bearer $TOKEN" \
-d '{"message": "hello"}' \
$HOST:443 ping.v1.PingService/PingStreamTOKEN=$(gcloud auth print-identity-token)
HOST=$(gcloud run services describe grpc-ping --region europe-west1 --format 'value(status.url)')
HOST=${HOST#https://}
grpcurl -H "Authorization: Bearer $TOKEN" \
-d '{"message": "hello"}' \
$HOST:443 ping.v1.PingService/Ping
grpcurl -H "Authorization: Bearer $TOKEN" \
-d '{"message": "hello"}' \
$HOST:443 ping.v1.PingService/PingStreamCloud Run removes most of the infrastructure work from running a gRPC service, but it does not remove the need to understand the transport path. Once you know that TLS ends at the edge and your container receives h2c, the deployment model becomes simple.