Go / Gin

Go AWS Lambda Function Cold Start Latency: How to Optimize Package Size

5 min read by DebuggedIt

Quick answer

Your Go Lambda function responds quickly once warm, but the first invocation after a period of inactivity is noticeably slower. Cold starts are an inherent...

Your Go Lambda function responds quickly once warm, but the first invocation after a period of inactivity is noticeably slower. Cold starts are an inherent part of how serverless functions work, but Go's cold start times are usually already fast relative to other runtimes β€” when they're not, deployment package size and unnecessary initialization work are almost always the reason.

The Problem

Checking CloudWatch metrics or logs reveals a clear gap between cold and warm invocation latency:

REPORT RequestId: abc-123 Duration: 812.44 ms Billed Duration: 813 ms
    Memory Size: 256 MB Max Memory Used: 198 MB Init Duration: 621.09 ms

Compare that Init Duration against a subsequent warm invocation of the same function, which skips initialization entirely:

REPORT RequestId: def-456 Duration: 12.31 ms Billed Duration: 13 ms
    Memory Size: 256 MB Max Memory Used: 199 MB
Where cold start time actually goes Download package Init runtime + globals Handler runs (fast) Smaller package + lazy init shrinks the first two boxes

Why It Happens

A cold start includes downloading and unpacking your deployment package, initializing the Go runtime, and running any package-level initialization code (global variable assignments, init() functions) before your handler ever executes. Each of these stages scales with different factors, but package size and unnecessary startup work are the two most controllable ones. Common contributors:

  • A larger-than-necessary deployment package β€” unused dependencies, debug symbols, or accidentally including files that aren't actually needed at runtime, all increasing download and unpack time.
  • Expensive work in init() functions or package-level variable initialization β€” establishing database connections, loading large configuration files, or doing network calls before the handler even starts, all of which run on every cold start regardless of whether that specific invocation needs them.
  • Not using AWS's provided Go runtime efficiently β€” an older deployment pattern or unnecessarily heavy custom runtime layer adding overhead that a leaner, more current setup wouldn't have.
  • Higher memory configuration than needed paradoxically not helping cold start (Lambda scales CPU with memory, so this one actually helps performance, but package size and init work matter more for the specific cold-start-vs-warm gap you're likely troubleshooting).

The Fix

Check your current deployment package size first, since this is often the single biggest lever:

ls -lh deployment.zip

Build with symbol and debug information stripped, which meaningfully reduces binary size without affecting functionality:

GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bootstrap main.go

-s strips the symbol table, and -w strips DWARF debug information β€” neither is needed for a production Lambda function, and together they typically shrink the binary by 20-30%.

Use the newer provided.al2023 custom runtime with a bare bootstrap executable, which is the current recommended pattern for Go Lambdas and avoids the overhead of the older Go-specific managed runtime approach:

# Zip just the compiled binary, named exactly "bootstrap"
zip deployment.zip bootstrap

Audit your dependencies and remove anything not actually needed at runtime β€” a surprisingly common source of bloat is accidentally importing a large SDK for functionality you only use a small part of:

go mod tidy
go list -m all

If you're using the full AWS SDK for Go, check whether you can import only the specific service clients you actually need rather than the entire SDK, since AWS SDK for Go v2's modular design allows this and can meaningfully reduce compiled binary size:

// Instead of importing broadly, import just what you use
import "github.com/aws/aws-sdk-go-v2/service/s3"

Move expensive initialization out of package-level init() and into lazy, on-demand initialization where the cost is only paid if actually needed for a given invocation, rather than on every single cold start regardless of what that invocation actually does:

var (
    dbOnce sync.Once
    db     *sql.DB
)

func getDB() *sql.DB {
    dbOnce.Do(func() {
        db, _ = sql.Open("postgres", dsn)
    })
    return db
}

If your function's cold start is still too slow for latency-sensitive use cases even after these optimizations, consider Provisioned Concurrency, which keeps a specified number of execution environments pre-initialized and ready, eliminating cold starts entirely for the traffic within that provisioned capacity:

aws lambda put-provisioned-concurrency-config \
  --function-name my-function \
  --qualifier prod \
  --provisioned-concurrent-executions 5

Still Not Working?

If package size and initialization are already optimized but cold starts remain slower than expected, check whether the function is inside a VPC β€” VPC-attached Lambda functions historically had meaningfully higher cold start overhead due to elastic network interface setup, though AWS has significantly improved this in recent years with Hyperplane ENIs. Confirm you're on a current runtime and check whether VPC attachment is actually necessary for your function's specific needs, since removing it entirely (if your function doesn't need to reach VPC-only resources) sidesteps the concern altogether:

aws lambda get-function-configuration --function-name my-function --query 'VpcConfig'

If VPC access is genuinely required, ensure you're using a recent Lambda runtime and that your account has Hyperplane-based networking (essentially all accounts by now), since this specific cold-start penalty has been substantially reduced compared to Lambda's earlier VPC networking implementation.