Cloud Run’s managed HTTPS endpoints are actually just a thin layer over a highly optimized HTTP/2 implementation, and gRPC is built on top of that.

Let’s see it in action. Imagine you have a simple Go application listening on 0.0.0.0:8080 that you’ve deployed to Cloud Run.

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// Check the protocol
		if r.Proto == "HTTP/2.0" {
			fmt.Fprintf(w, "Hello from HTTP/2 on Cloud Run!")
		} else {
			fmt.Fprintf(w, "Hello from %s on Cloud Run!", r.Proto)
		}
	})

	log.Println("Server starting on :8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

When you deploy this, Cloud Run automatically handles the TLS termination and HTTP/2 negotiation. You don’t need to configure anything in your application code for basic HTTP/2.

To interact with it using HTTP/2 from your local machine, you can use curl. Ensure your curl supports HTTP/2 (most recent versions do).

curl --http2 https://your-service-url.a.run.app

You should see: Hello from HTTP/2.0 on Cloud Run!

Now, for gRPC. gRPC relies on HTTP/2 for its transport. Cloud Run’s HTTP/2 support means gRPC works out-of-the-box for services that implement the gRPC protocol.

Let’s say you have a simple gRPC service defined in a .proto file:

syntax = "proto3";

package greeter;

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

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

And your Go application serves this gRPC service on the port specified by the PORT environment variable (which Cloud Run sets, defaulting to 8080).

package main

import (
	"context"
	"log"
	"net"
	"os"

	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection" // For testing with grpcurl
	pb "your_module/greeter" // Generated from your proto file
)

const (
	port = ":8080" // Cloud Run injects PORT env var, usually 8080
)

// server is used to implement helloworld.GreeterServer.
type server struct {
	pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer.
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
	// Cloud Run sets the PORT environment variable
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080" // Default if not set
	}

	lis, err := net.Listen("tcp", ":"+port)
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{})
	reflection.Register(s) // Register reflection service on gRPC server.
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

When you deploy this gRPC service to Cloud Run, it will be accessible via its managed HTTPS endpoint. To test it locally, you’d typically use a tool like grpcurl.

First, make sure your service is deployed and you have its URL. Then, run:

grpcurl --plaintext --proto your_greeter.proto your-service-url.a.run.app:443 greeter.Greeter.SayHello '{"name": "World"}'

If your-service-url.a.run.app is your Cloud Run service URL, and your_greeter.proto contains your service definition. The --plaintext flag is usually not needed for Cloud Run as it terminates TLS, but grpcurl might require it or a specific TLS setup if not explicitly handled. For Cloud Run’s managed endpoint, you’d typically connect directly to the HTTPS URL.

The core idea is that Cloud Run’s ingress is always HTTPS, and it always negotiates HTTP/2 with clients that support it. This means your application only needs to listen on a TCP port and handle the underlying protocol (HTTP or gRPC over HTTP/2). The magic is in the Google-managed load balancer and proxy in front of your container.

The most surprising truth about Cloud Run’s HTTP/2 and gRPC support is that it’s not an opt-in feature you enable in your service configuration; it’s the default behavior of the managed ingress. Your application just needs to listen on the correct port and speak the expected protocol.

The specific levers you control are:

  1. Port: Your application must listen on the port specified by the PORT environment variable. Cloud Run injects this, and it’s typically 8080. If your app hardcodes 8080, it’s usually fine, but using the env var makes it more robust.
  2. Protocol Handling: For standard HTTP, your web framework will usually handle HTTP/2 transparently. For gRPC, your gRPC server library will handle the HTTP/2 framing. You don’t need to configure TLS or HTTP/2 explicitly within your application code when running on Cloud Run’s managed ingress.
  3. Service URL: Cloud Run provides a stable HTTPS URL. This is what you point your clients to.

The one thing most people don’t know is that Cloud Run’s managed ingress performs advanced load balancing and request routing at the HTTP/2 stream level. This means that multiple gRPC requests, or even a mix of HTTP/2 and gRPC requests, can be multiplexed over a single TCP connection to your container, and Cloud Run’s infrastructure intelligently directs them. Your application simply receives the raw request on its listening port, unaware of the multiplexing happening upstream. This is a key reason for its performance and efficiency.

The next concept you’ll likely encounter is handling custom domains and certificates for your Cloud Run services, which also integrates seamlessly with this managed ingress.

Want structured learning?

Take the full Cloud-run course →