After 54 days of building Go services, deployment is where theory meets reality. The wrong choice here costs money, reliability, and your team's sleep at night. Deploying Go services to AWS: what developers should know starts with understanding which service fits your workload, not which one is trendy.
Options — Pick by workload
AWS gives us three main paths for Go services: Lambda, ECS Fargate, and EC2. Each solves a different problem, and picking wrong means overpaying or rebuilding later.
| Service | Best For | Cold Start | Cost Model |
|---|---|---|---|
| Lambda | Event-driven, spiky traffic | ~50–200ms (Go) | Per request + duration |
| ECS Fargate | Long-running APIs | ~10–30s (container startup) | Per task hour |
| EC2 | Full control, custom networking | None (always on) | Per instance hour + data transfer |
Event-driven workloads—webhooks, scheduled tasks, image resizing—suit Lambda. You pay only when invoked. For REST APIs with steady traffic, Fargate removes the instance management headache. EC2 remains the choice when you need custom kernel tuning, specific security group rules, or bare-metal performance.
A real scenario: we built a reporting service that ran once per night and processed user data. Lambda cost us $20/month. When requirements changed to real-time dashboards, we migrated to Fargate at $50/month for sustained traffic. EC2 would have been $80+, with no clear benefit.
Lambda with Go
Go on Lambda uses the provided.al2023 runtime. You compile your binary, package it, and AWS runs your code. No language-specific runtime overhead—just your binary and the AWS Lambda runtime interface.
The bootstrap mechanism is straightforward. AWS runs a binary named bootstrap at the root of your deployment package. That binary communicates with the Lambda Runtime API to fetch events and return responses.
package main
import (
"context"
"fmt"
"log"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
)
type Response struct {
StatusCode int `json:"statusCode"`
Body string `json:"body"`
}
func HandleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (Response, error) {
log.Printf("Received: %s", request.Body)
return Response{
StatusCode: 200,
Body: fmt.Sprintf("Hello, %s", request.Path),
}, nil
}
func main() {
lambda.Start(HandleRequest)
}
Compile for Linux and package it as a zip with the binary renamed to bootstrap:
GOOS=linux GOARCH=amd64 go build -o bootstrap main.go
zip function.zip bootstrap
Deploy via the AWS CLI or Terraform. Cold starts average 50–200ms for Go, much faster than Python or Node.js interpreters. This matters when traffic is bursty and Lambda scales your concurrency automatically.
One gotcha: Lambda's default timeout is 3 seconds. Long-running operations or database calls need explicit timeout increases. Also, the $LAMBDA_TASK_ROOT environment variable points to your function's directory; relative paths for config files should reference it.
Small images — Go images start fast
Go's static binaries and minimal runtime footprint make container images tiny. A Go API service typically compiles to 10–30 MB. Add Alpine Linux and you're under 50 MB. This matters for Fargate scaling and EC2 deployment speed.
A multi-stage Dockerfile reduces your image to bare essentials:
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o api main.go
FROM alpine:3.19
WORKDIR /root/
COPY --from=builder /app/api .
EXPOSE 8080
CMD ["./api"]
Build and check the final image size:
docker build -t my-api:latest .
docker images | grep my-api
Expect 20–40 MB. Compare that to a Python API with dependencies, which easily reaches 200–500 MB.
Why does this matter in production? When Fargate scales from 2 to 20 tasks during a traffic spike, smaller images pull faster from ECR. Container startup time drops from 30 seconds to 10–15 seconds. In real dashboards and live services, that 15-second difference keeps customers happy and reduces timeout errors.
ECR also stores fewer bytes, so data transfer and storage costs decline. For companies running thousands of container deployments monthly, Go's small images save thousands of dollars.
Graviton — ARM instances are cheaper and fast for Go
AWS Graviton processors are ARM-based chips designed in-house. Graviton2 and Graviton3 instances cost 20–30% less than x86 equivalents and often perform comparably or better. Go compiles to ARM as easily as to x86.
For Fargate, specify a Graviton CPU architecture in your task definition:
{
"family": "my-api",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"runtimePlatform": {
"cpuArchitecture": "ARM64",
"operatingSystemFamily": "LINUX"
},
"containerDefinitions": [
{
"name": "api",
"image": "123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:latest",
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
]
}
]
}
Your Go binary must be compiled for ARM64. Update your build pipeline:
GOOS=linux GOARCH=arm64 go build -o api main.go
A real-world case: a ride-hailing team migrated their Go API services from x86 Fargate to Graviton. CPU instances dropped from 2 vCPU to 1 vCPU per task, memory stayed the same, and latency improved slightly. Annual savings: $40K across 200 tasks, with zero code changes.
Go's performance on ARM is genuinely good because Go doesn't rely on JIT compilation or runtime optimization. The binary is the same speed regardless of architecture. Other languages (Java, Python) don't see the same efficiency gains on Graviton because they depend on runtime behavior.
For EC2, use t4g, m7g, or c7g instance types. They're Graviton-based and widely available across AWS regions. Spot instances with Graviton are also discounted further, making them ideal for batch jobs or non-critical services.
Common mistakes
- Not setting
GOOSandGOARCHat build time. Cross-compilation is essential. Building on macOS and deploying to Linux breaks at runtime. Always specify the target OS and architecture explicitly. - Ignoring cold start sensitivity. Lambda cold starts matter when your API SLA is under 200ms. If you have sustained traffic, Fargate is the better choice. If you force Lambda into steady-state usage, you're paying for features you don't use.
- Leaving default timeouts unchanged. Lambda's 3-second default kills database queries and external API calls. Set timeouts based on your service's needs, not AWS's defaults.
- Not testing multi-architecture builds locally. Build ARM64 binaries, run them on an ARM machine (M1/M2 Mac, or EC2 t4g instance), and validate behavior before deploying to production.
- Over-provisioning task CPU and memory. Graviton and ARM adoption let you right-size workloads. A 512 MB Go API often needs only 256 MB in reality. Start smaller, monitor, and scale up.
- Shipping large base images to ECR. Every byte counts at scale. Remove unnecessary tools, use Alpine, and validate layer sizes in your CI/CD pipeline.
Key takeaways
- Lambda suits event-driven, bursty traffic; Fargate handles steady APIs; EC2 is for custom control and bare-metal performance.
- Go's static binaries and fast startup make it ideal for Lambda and container-based deployments. Cold starts average 50–200ms.
- Small container images (20–50 MB for Go) reduce pull time, startup latency, and storage costs across large deployments.
- Graviton ARM instances cost 20–30% less and deliver comparable or better performance for Go workloads. Compile with
GOARCH=arm64and verify on ARM hardware before production. - Always cross-compile for your target platform, set appropriate timeouts, and test multi-architecture builds in your pipeline.
- Profile your workload before choosing a service. Event-driven, steady-state, and custom-control workloads have different cost and operational trade-offs.






Comments
Loading comments…