| name | lambda-go-lazy-init-segregated |
| description | >- Use when this capability is needed. |
Segregated lazy init for Go Lambdas
AWS Lambda's
best practices say verbatim:
"Initialize SDK clients and database connections outside of the function handler … Subsequent
invocations processed by the same instance of your function can reuse these resources." True — but
the language-neutral doc stops short of the Go-specific refinement: in Go, anything you initialize
during INIT (an init() block or an eager package-level global) runs unconditionally on every cold
start, on every code path. init() cascades across all imported packages. So a Mongo connection
opened in init() taxes 100% of cold starts even if 70% of invocations never touch Mongo.
The fix: defer each expensive client behind its own sync.Once, grouped by temporal locality of
use — not one initAll().
When to invoke
- A Go Lambda has 2+ clients (AWS SDK service clients, Mongo, an HTTP client to a slow upstream)
used on different branches.
- You see
init() (or an eager global) opening a database/Mongo connection or constructing a client
the common path doesn't use.
- Adding a new client to an existing handler — wire it to the branch that needs it, not to
init().
Announce on invoke: "Using lambda-go-lazy-init-segregated to split sync.Once by usage path so the happy path doesn't pay cold-start for unused clients."
The mental model
- The AWS
aws.Config itself is cheap — load it once (globally or behind one sync.Once).
- Each
serviceX.NewFromConfig(cfg) is cheap-ish; a Mongo dial or a TLS handshake to a slow
upstream is the expensive part.
- Group
sync.Once by which branch needs the client together, not "all infra clients in one Once."
sync.Once.Do guarantees exactly-once, goroutine-safe, lazy execution — the idiomatic deferral
primitive.
Example shape: an inbound-federation handler with 3 clients (an identity provider, EventBridge,
Mongo) across 2 segregated Once values. The most common branch — a brand-new federated
sign-up with no candidate to reconcile — initializes only the identity-provider client, never
Mongo or EventBridge.
Why Go specifically
Java/Python/.NET have SnapStart, which snapshots a pre-initialized environment so eager init is
effectively free. Go has no SnapStart. Every cold start re-runs INIT from scratch, so segregated
lazy init is the primary lever for Go cold-start cost. (If you do use Provisioned Concurrency, INIT
runs during pre-warm and the value diminishes — this pattern matters most for on-demand Lambdas
with bursty traffic and seldom-used branches.)
Canonical example
package main
import (
"context"
"sync"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/eventbridge"
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
"go.mongodb.org/mongo-driver/v2/mongo"
)
var (
cfgOnce sync.Once
awsCfg aws.Config
)
func loadAWSConfig(ctx context.Context) aws.Config {
cfgOnce.Do(func() {
c, err := config.LoadDefaultConfig(ctx)
if err != nil {
panic(err)
}
awsCfg = c
})
return awsCfg
}
var (
idpOnce sync.Once
idpClient *cognitoidentityprovider.Client
)
func initIdentity(ctx context.Context) {
idpOnce.Do(func() { idpClient = cognitoidentityprovider.NewFromConfig(loadAWSConfig(ctx)) })
}
var (
dataOnce sync.Once
ebClient *eventbridge.Client
mongoClient *mongo.Client
)
func initDataPlane(ctx context.Context) {
dataOnce.Do(func() {
ebClient = eventbridge.NewFromConfig(loadAWSConfig(ctx))
mongoClient = connectMongo(ctx)
})
}
(MyResp, ) {
initIdentity(ctx)
needsReconcile(e) {
initDataPlane(ctx)
}
doWork(ctx, e)
}
{ lambda.Start(handler) }
Anti-pattern to detect (greppable)
- A single
func initAll() or one sync.Once that constructs every client.
init() calling connectMongo(...), sql.Open(...), or any dial unconditionally.
- An eager package-level
var mongoClient = mustConnect() outside any Once/branch.
- A
sync.Once shared across two branches that never co-occur.
Decision aid
- Client used on (almost) every invocation, cheap to build? → fine to init eagerly or behind one
Once.
- Client expensive (Mongo dial, slow-upstream TLS) and used on a minority branch? → its own
sync.Once, called inside that branch only.
- On Provisioned Concurrency? → the asymmetry shrinks; still prefer segregation for the
on-demand overflow path.
Related skills
global-skills/aws-go/lambda-go-refactor-purge-audit/SKILL.md — when a branch (and its client) is
removed, go mod tidy purges the module; audit the now-dead IAM actions.
global-skills/aws-go/aws-sdk-go-v2-version-policy/SKILL.md — share one aws.Config; construct
each chosen service client with NewFromConfig.
Sources
Last verified: 2026-06-03 against the AWS Lambda best-practices guide (live — "Initialize SDK
clients and database connections outside of the function handler"), sync.Once semantics, and
aws-lambda-go/lambda v1.54.0 (lambda.Start). Go has no SnapStart, confirmed against the
SnapStart docs (Java/Python/.NET only).
Re-check after: AWS SDK Go v2 major / CDK CLI major, or by 2026-09-03. Decay risk: low (the
INIT-phase cost model and sync.Once are stable primitives).
Found a drift? Run /skill-pattern-freshness-audit aws-go.
Source: esaldgut/ai-native-engineering-workspace — distributed by TomeVault.