In our application, we use long-lived server-sent event streams (SSE). These are set up to have a really long timeout and lifetime - 15 minutes in our setup. However, Google Cloud Run has a known issue in that client disconnects are not propagated to Cloud Run when using HTTP/1.1 to communicate with the backend service. Thus, I started looking into using HTTP/2 for services.
Cloud Run terminates TLS at the frontend, but can forward traffic as either HTTP/1.1 or HTTP/2 cleartext (h2c) traffic. Normally, HTTP/2 always uses TLS, but HTTP clients and servers can often be configured to use a cleartext version of the protocol. Cloud Run also makes you select the protocol to be used. This is basically the “HTTP/2 with Prior Knowledge” setup noted in RFC 9113, section 3.3.
Configuring a Go server for HTTP/2 cleartext
Our first cut of h2c support predated the Go 1.24 changes I will go into more detail below. Most posts and guides on the internet will point you toward the old outdated approach, but I’ve included it below so it is easier to see the before and after, and migrate your own code if you need to.
Before Go 1.24 (old approach)
To use h2c, you had to use the golang.org/x/net/http package, and go through a convoluted setup.
import (
"net/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
handler := ...
// the existing `handler` must be wrapped, attached to server, then configured
h2s := &http2.Server{}
handler = h2c.NewHandler(handler, h2s)
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", "", 9888),
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 35 * time.Second,
// https://cloud.google.com/load-balancing/docs/https/request-distribution#timeout-bes
IdleTimeout: 620 * time.Second,
}
err = http2.ConfigureServer(srv, h2s)
if err != nil {
...
}
Go 1.24+ (new approach)
With Go 1.24, no x/net/http2/h2c wrapper is needed anymore, and the setup is a lot more readable. You can configure protocols directly on http.Server.
Reference: Go 1.24 net/http release notes.
handler := ...
srv := &http.Server{
Addr: fmt.Sprintf("%s:%d", "", 9888),
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 35 * time.Second,
// https://cloud.google.com/load-balancing/docs/https/request-distribution#timeout-bes
IdleTimeout: 620 * time.Second,
}
srv.Protocols = new(http.Protocols)
srv.Protocols.SetHTTP1(true)
srv.Protocols.SetUnencryptedHTTP2(true)
Testing locally before deployment
Testing is pretty simple. This command will fail if HTTP/2 cleartext isn’t set up properly.
curl -i --http2-prior-knowledge http://localhost:9888
Terraform Cloud Run configuration
Once your Go service can speak HTTP/2 cleartext, it needs to be configured in Cloud Run appropriately. If you’re doing it via the Cloud Run console, follow the HTTP/2 for services documentation. We’re doing it via terraform - this snippet below gives a general idea of the configuration.
# Many settings have been omitted that are not relevant to this post.
resource "google_cloud_run_v2_service" "api" {
name = "api"
location = var.primary_region
invoker_iam_disabled = true
ingress = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"
template {
containers {
...
ports {
name = "h2c"
container_port = 9888
}
}
# Default is 80; with idle SSE connections, we can easily scale higher.
max_instance_request_concurrency = 200
# Default is 300s, but with SSE, we want to allow long-lived connections.
timeout = "900s"
}
lifecycle {
ignore_changes = [client, client_version, template[0].revision]
}
}
No real changes were needed at the load balancer level, since HTTPS is used to communicate with Serverless NEGs, and it seems like it properly upgrades the connection to HTTP/2 during that negotiation. Also, the default timeouts for serverless backends is 60 minutes, not 30 seconds like some other backends.