GCP Cloud Run & AWS Fargate Patterns
Container platforms let Go teams run a normal main with net/http or gRPC while the cloud manages scaling, patching, and load balancing.
Cloud Run optimizes for request-driven HTTP with scale to zero; AWS Fargate runs ECS tasks without managing EC2 - often for steady services rather than idle-to-zero economics.
Summary
Ship a minimal OCI image with a static Go binary, listen on $PORT, and configure platform health checks.
Cloud Run sets max concurrent requests per instance - tune it against handler latency and DB pool size.
Fargate tasks need CPU/memory in the task definition, awsvpc networking, and optional Service Connect for service mesh-style discovery.
Both platforms benefit from SIGTERM-aware graceful shutdown in Go.
Recipe
Quick-reference recipe card - copy-paste ready.
srv := &http.Server{
Addr: ":" + port,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
<-shutdown
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
}()
log.Fatal(srv.ListenAndServe())# Cloud Run deploy sketch
gcloud run deploy api --image IMAGE --region REGION --concurrency 80 --cpu 1 --memory 512MiWhen to reach for this:
- Public REST APIs with variable traffic that should idle cheaply (Cloud Run).
- Background workers or always-on internal APIs on AWS without Lambda limits (Fargate).
- Teams already containerizing Go in CI and wanting parity with local
docker run.
Working Example
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("GET /readyz", func(w http.ResponseWriter, r *http.Request) {
// optional: ping database with short timeout
w.WriteHeader(http.StatusNoContent)
})
mux.HandleFunc("GET /api/status", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"status":"ok"}`))
})
srv := &http.Server{
Addr: ":" + port,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-stop
slog.Info("shutdown started")
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
slog.Error("shutdown error", "err", err)
}
}()
slog.Info("listening", "addr", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
slog.Error("server error", "err", err)
os.Exit(1)
}
}What this demonstrates:
- Separate liveness (
/healthz) and readiness (/readyz) for orchestrators. ReadHeaderTimeouton the server for slow-client protection.- SIGTERM handler with bounded
Shutdown- Cloud Run sends SIGTERM before killing the container.
Deep Dive
How It Works
- Cloud Run fronts your container with a Google-managed proxy, routes HTTP to the container port, and scales instances based on request rate and concurrency settings.
- When traffic drops to zero, Cloud Run can scale to zero instances - you pay nothing for compute until the next request (cold start applies).
- Fargate schedules tasks on AWS-managed capacity; ECS services keep desired count tasks running - no scale-to-zero by default.
Cloud Run Knobs for Go
| Setting | Effect on Go service |
|---|---|
--concurrency | Max simultaneous requests per instance; raise when handlers are I/O bound |
--cpu / --memory | More CPU speeds JSON and TLS; memory affects GC headroom |
--min-instances | Reduces cold starts at baseline cost |
--timeout | Request deadline enforced at the proxy |
| VPC connector | Private RFC1918 access to Cloud SQL / Memorystore |
Fargate Task Pattern
{
"family": "go-api",
"cpu": "512",
"memory": "1024",
"networkMode": "awsvpc",
"containerDefinitions": [{
"name": "api",
"image": "ACCOUNT.dkr.ecr.REGION.amazonaws.com/go-api:latest",
"portMappings": [{"containerPort": 8080}],
"healthCheck": {
"command": ["CMD-SHELL", "wget -q -O- http://127.0.0.1:8080/healthz || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3
},
"stopTimeout": 30
}]
}Match container stopTimeout with your Go Shutdown budget so ECS does not SIGKILL mid-drain.
Scale-to-Zero Trade-offs
| Platform | Idle cost | Cold start | Best fit |
|---|---|---|---|
| Cloud Run | Near zero at scale-to-zero | Container start + Go init | Bursty HTTP |
| Fargate (desired >= 1) | Steady task hours | None while tasks run | Workers, stable RPC |
| Lambda | Zero between invokes | Binary load + init | Event triggers |
Gotchas
- Hardcoded
:8080ignoringPORT- Cloud Run injectsPORT; binding wrong port fails deploy health checks. Fix: Reados.Getenv("PORT")with fallback. - No SIGTERM handling - Platform kills the process after a short grace period. Fix:
signal.Notify+srv.Shutdown. - Concurrency too high for DB pools - 80 concurrent requests with
MaxOpenConns=10causes waits or timeouts. Fix: Align Cloud Run concurrency with pool math. - Large container images - Slow cold starts on Cloud Run. Fix: Distroless or scratch images; multi-stage builds.
- Fargate task role missing - AWS SDK calls fail with access denied. Fix: Attach IAM task role parallel to Lambda execution roles.
- Readiness equals liveness - DB outages restart the whole task instead of stopping traffic. Fix: Fail
/readyzonly, keep/healthzcheap.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| AWS Lambda | Event spikes, short handlers | Long HTTP streaming sessions |
| GKE / EKS | Operators, complex networking | Simple single-service APIs |
| App Engine standard | Legacy GCP PaaS standards | You already standardized on Cloud Run |
| EC2 + systemd | Full host control required | You want managed patching |
FAQs
Should Cloud Run services use gRPC?
Cloud Run supports gRPC on HTTP/2 with TLS.
For internal east-west traffic, many teams still use REST or connect via VPC to GKE gRPC backends.
How do I reduce Cloud Run cold starts?
Use smaller images, set --min-instances for baseline traffic, and avoid heavy init() work.
Measure with Cloud Trace before over-provisioning CPU.
Is Fargate serverless?
You do not manage EC2, but you still choose task size and desired count.
It is "serverless capacity" rather than scale-to-zero functions.
Can one image run on Cloud Run and Fargate?
Yes if the process listens on a configurable port and reads config from environment variables.
IaC differs per cloud, not the Go binary.
When should I pick Fargate over Cloud Run on AWS?
When you need always-on tasks, longer runtimes without API Gateway limits, or tight integration with ECS service discovery and AWS networking primitives.
Related
- Cloud Deploy Basics - Dockerfile and
gcloud run deploy - Go in the Cloud: Lambda, Cloud Run, and Beyond - container vs function models
- Secrets, IAM & Workload Identity - Cloud Run service accounts and Fargate task roles
- HTTP/2, Timeouts & ReverseProxy - production
http.Servertuning - Serverless Go Architecture Decision Guide - platform selection
- Cloud & Serverless Best Practices - cost and observability
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).