Lambda Durable Execution SDK for Go is GA: a practical guide
Explore with AI
AWS announced on 15 October 2026 that the Lambda Durable Execution SDK for Go is generally available, so you can write Lambda durable functions in plain Go that checkpoint each step, pause for up to a year on timers or callbacks, and replay saved results when they resume. You add it with go get, build a static bootstrap binary for provided.al2023, and turn on durable execution when you create the function, because it can't be added to a function that already exists.
On this page
- What a Lambda durable function is
- How replay turns one Go handler into many invocations
- Durable functions, Step Functions and hand-rolled state
- Shipping a first durable Go function, step by step
- Testing replay locally with durabletest and durablelint
- The limits AWS documents for durable Go functions
- Mistakes that break a durable Go workflow on resume
- Reading durable executions in production
- Fixing stuck durable executions with Polylane
- Common questions
The AWS Lambda Durable Execution SDK for Go became generally available on 15 October 2026. It lets Go teams write long multi-step workflows as ordinary Go handlers. Typical examples are infrastructure automation, deploy rollouts with approval gates, and pipelines that call AI models and wait for a person to review. Lambda checkpoints each completed operation and suspends the function while it waits. Later it invokes the function again and replays the saved results, so finished work does not run twice.
The SDK fits the Go tooling you already use. You run go get, build one static binary and deploy it on the provided.al2023 runtime like any other Go function. It also ships two pieces that matter for safety: durabletest, which runs whole workflows on your laptop with no network or AWS credentials, and durablelint, which catches code that would break when a workflow resumes. The catch is that your handler code now has to follow replay rules. Most of this page is about those rules.
What a Lambda durable function is
A durable function is a normal Lambda function created with a DurableConfig object. That config tells Lambda to checkpoint the function’s progress. AWS calls the full life of one run a durable execution. One execution can span many invocations: each invocation runs until the handler blocks on a timer, a callback, another function or the function timeout. Then it ends, and Lambda invokes the function again when there is work to do. Because the execution spans several invocations and uses no compute while it waits, the whole run can last up to a year.
In Go, your handler takes a durable.Context where a Lambda handler normally takes context.Context. The durable context embeds context.Context. Durable operations are package-level functions that take it as their first argument:
durable.Stepruns a function, checkpoints its result and retries on error.durable.Waitsuspends the execution for a set time with no compute running.durable.CreateCallbackanddurable.WaitForCallbackpause until an outside system sends a result. This is how human approvals and agent-in-the-loop flows work.durable.Invokecalls another durable function and waits for its result. This is the announcement’s “durable invocation for reliable function chaining”.durable.Map,durable.Parallelanddurable.Gofan work out concurrently in a way replay can repeat.durable.WaitForConditionpolls a check, saving its state between polls.
The announcement aims it at Go developers who build multi-step applications and today write their own progress tracking. If you keep a status table, chain SQS queues between functions or rerun idempotent batch jobs to fake resumability, this is the service-backed version of that work.
How replay turns one Go handler into many invocations
The key idea is that a durable function does not resume where it stopped. Lambda calls the handler again from the top. The SDK loads the checkpoint log, and each durable operation the code reaches first checks for a saved result. If one exists, the operation returns it straight away without running the body. The first operation with no saved result runs for real, and from that point the code is live again.
graph TB A[Invoke starts a durable execution] --> B[Step runs and its result is checkpointed] B --> C{Wait, callback or invoke pending?} C -->|yes| D[Invocation ends, no compute charge] D --> E[Lambda invokes the function again] E --> F[Handler reruns from the top] F --> G[Done steps return saved results] G --> C C -->|no| H[Handler returns, execution SUCCEEDED] style D fill:#d1fae5,stroke:#6ee7b7,color:#065f46
This puts a strict rule on your code. Replay matches saved results to operations by position, and the SDK checks that operation names and types match the checkpoint log. So the handler must start the same operations in the same order on every invocation. Code between operations may depend only on the event and on results the SDK has already saved. Anything that can change from run to run, such as the clock, random IDs, API calls or database reads, belongs inside a step body. Only the step’s saved result takes part in replay.
The per-invocation timeout and the execution timeout are separate settings. The function Timeout limits one invocation to at most 15 minutes. ExecutionTimeout in DurableConfig limits the whole execution to at most one year.
Durable functions, Step Functions and hand-rolled state
AWS’s own guidance puts durable functions and Step Functions side by side. Both run workflows and manage state for you. Durable functions run inside Lambda, and you define the workflow in a normal programming language. Step Functions is a separate service with a graph-based DSL or a visual designer, and it has native integrations with more than 220 AWS services. AWS suggests durable functions when the workflow is tightly tied to the business logic of a Lambda app. It suggests Step Functions when you want visual design and orchestration across many AWS services.
For a Go team, the practical difference is where the workflow lives. With the SDK, a rollout’s approval gate, bake time and retry rules sit in the same Go module as the deploy logic. Your normal code review, tests and linters cover them. Compared with a homemade state table, the checkpoint store, timers and callback delivery are run by Lambda. You still own idempotency inside each step, as covered below.
Pricing follows the usual Lambda model. AWS lists the billing dimensions as compute duration, requests, durable operations, storage written, storage persisted and data transfer. A suspended execution does not pay for compute. It pays a small storage charge to keep checkpoints during the wait and through the retention period after it finishes. Each operation writes a checkpoint, and each checkpoint has its own cost. AWS points to the Lambda pricing page and the Capabilities by Region page for rates and Region availability.
Shipping a first durable Go function, step by step
The example below is a small deploy rollout: deploy a canary, wait for it to bake, ask a person to approve, then promote it. It uses the operations from the announcement. The SDK needs Go 1.24 or later.
- Create a module and add the SDK.
mkdir rollout && cd rollout
go mod init example.com/rollout
go get github.com/aws/aws-durable-execution-sdk-go
- Write the handler in
main.go. Every side effect goes inside a step body. The approval usesWaitForCallback, which runs the submitter as a step and then blocks until someone sends a result.
package main
import (
"context"
"fmt"
"time"
"github.com/aws/aws-durable-execution-sdk-go/durable"
)
type Rollout struct {
Service string `json:"service"`
Version string `json:"version"`
}
func deployCanary(ctx context.Context, r Rollout) (string, error) { return "canary-" + r.Version, nil }
func requestApproval(ctx context.Context, r Rollout, callbackID string) error { return nil }
func promote(ctx context.Context, r Rollout) (string, error) { return "promoted " + r.Version, nil }
func handler(ctx durable.Context, r Rollout) (string, error) {
if _, err := durable.Step(ctx, "deploy-canary", func(sc durable.StepContext) (string, error) {
return deployCanary(sc, r)
}); err != nil {
return "", err
}
if err := durable.Wait(ctx, "bake", 10*time.Minute); err != nil {
return "", err
}
decision, err := durable.WaitForCallback[string](ctx, "approve-rollout",
func(sc durable.StepContext, callbackID string) error {
sc.Logger().Info("approval requested", "callbackId", callbackID)
return requestApproval(sc, r, callbackID)
},
durable.WithCallbackTimeout(24*time.Hour),
durable.WithCallbackSerdes(durable.JSONSerdes))
if err != nil {
return "", err
}
if decision != "approved" {
return fmt.Sprintf("rollout stopped: %s", decision), nil
}
return durable.Step(ctx, "promote", func(sc durable.StepContext) (string, error) {
return promote(sc, r)
})
}
func main() {
durable.Start(handler)
}
- Build a static Linux binary named
bootstrap. That is the file theprovided.al2023runtime runs.
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/bootstrap .
- Create the function with a
DurableConfig.ExecutionTimeoutis required whenever you setDurableConfig. Without it, creation fails withInvalidParameterValueException. Size it to cover the longest path through your workflow, which here is the bake time plus a full day of waiting for approval.
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
ExecutionRoleArn:
Type: String
Resources:
RolloutFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: rollout
CodeUri: dist/
Handler: bootstrap
Runtime: provided.al2023
Architectures: [x86_64]
Timeout: 60
MemorySize: 128
Role: !Ref ExecutionRoleArn
DurableConfig:
ExecutionTimeout: 172800
RetentionPeriodInDays: 7
-
Give the execution role the two durable API actions. Besides permission to write CloudWatch Logs, the role needs
lambda:CheckpointDurableExecutionandlambda:GetDurableExecutionState. The SDK calls these on every checkpoint and every replay. -
Deploy.
sam deploy --template-file template.yaml \
--stack-name rollout --resolve-s3 --region us-west-2 \
--parameter-overrides ExecutionRoleArn=arn:aws:iam::111122223333:role/durable-lambda-role
- Start an execution asynchronously. A synchronous invoke waits for the execution to finish and is capped at 15 minutes, so a workflow with a day-long approval needs
--invocation-type Event. The call returns 202 straight away, and the response metadata includes theDurableExecutionArn.
aws lambda invoke --function-name rollout --qualifier '$LATEST' \
--invocation-type Event \
--payload '{"service":"checkout","version":"v42"}' \
--cli-binary-format raw-in-base64-out response.json
- Watch it, then approve it. The execution reports
RUNNINGwhile it waits. The callback ID is in the step’s log line and in theCallbackStartedevent of the execution history.
aws lambda get-durable-execution \
--durable-execution-arn <DurableExecutionArn> \
--query '[Status,Result]' --output text
aws lambda get-durable-execution-history \
--durable-execution-arn <DurableExecutionArn>
aws lambda send-durable-execution-callback-success \
--callback-id <ID> --result '"approved"' \
--cli-binary-format raw-in-base64-out
After the callback lands, Lambda invokes the function again. The handler replays the canary step and the bake wait from their checkpoints, gets approved from the callback, runs the promote step and finishes as SUCCEEDED.
Testing replay locally with durabletest and durablelint
The announcement calls out two tools to use before you deploy, and they are the cheapest way to catch replay bugs.
Run whole workflows in-process with durabletest
The durable/durabletest package runs a handler in your process with no network access or AWS credentials. RunUntilComplete invokes the handler as many times as the execution needs. Between invocations it finishes pending timers and step retries, so an hour-long wait takes no real time. The runner checks every checkpoint against the service’s limits and rejects the same updates the service would, with the same error code and message.
func TestBakeThenPromote(t *testing.T) {
handler := func(ctx durable.Context, n int) (int, error) {
if err := durable.Wait(ctx, "bake", time.Hour); err != nil {
return 0, err
}
return durable.Step(ctx, "promote", func(_ durable.StepContext) (int, error) {
return n + 1, nil
})
}
result, err := durabletest.NewLocalRunner(handler).RunUntilComplete(41)
if err != nil {
t.Fatal(err)
}
if result.Status != durabletest.Succeeded {
t.Fatalf("status = %s, want SUCCEEDED", result.Status)
}
}
A handler that returns an error gives back a nil runner error and a result with Status set to Failed. A run that blocks on a callback or an invoke comes back as Pending. SendCallbackSuccess, SendCallbackFailure and SendCallbackHeartbeat complete a callback the handler is blocked on, so you can test both outcomes of an approval gate. TestResult.Operations lists every recorded operation, which lets a test assert that a step ran once across replays. NewCloudRunner runs the same assertions against a deployed function.
Catch replay-breaking code with durablelint
durablelint is a static analyser built on the standard Go analysis framework. It runs as its own binary, under go vet -vettool, or inside golangci-lint.
go install github.com/aws/aws-durable-execution-sdk-go/analysis/cmd/durablelint@latest
durablelint ./...
go vet -vettool=$(which durablelint) ./...
Its five rules cover several of the mistakes in the next section: durablegoroutine, durablenestedop, durablenondeterminism, durablechildctx and durableclosure. The rest, such as error matching, callback decoding, retry configs and subtype changes, need review and tests. The rules look at code structure and resolved types only. They never follow data across function boundaries, so a helper without a durable.Context parameter that reads the clock will not be reported. Put it in CI anyway, and keep durabletest runs alongside it.
The limits AWS documents for durable Go functions
These are the numbers to design around. All of them come from the Lambda configuration guide, the SDK key concepts page and the Go SDK README.
Documented limits for Lambda durable functions and the Go SDK
| Setting or limit | Value | Where it bites |
|---|---|---|
| Whole execution | Up to 1 year | ExecutionTimeout range is 1 to 31622400 seconds |
| One invocation | Up to 15 minutes | Function Timeout still applies to each invocation |
| Synchronous invoke | 15 minutes | Use --invocation-type Event for longer runs |
| RetentionPeriodInDays | 1 to 90, default 14 | History via GetDurableExecutionHistory after completion |
| Enable durable execution | At creation only | Cannot be turned on for an existing function |
| Step result size | 262144 bytes | Rejected by the service and by durabletest |
| Invoke input size | 1048576 bytes | Rejected by the service and by durabletest |
| Minimum Wait | 1 second | Longer waits round up to whole seconds |
| Go version | 1.24 or later | Minimum may rise in a minor release |
A few more details from the README change how you write code:
- Default step retry.
ExponentialBackoff()makes 6 attempts in total, starting 5 seconds apart, doubling each time, capped at 60 seconds, with full jitter. While a retry waits and nothing else can make progress, the invocation ends, so a retrying step does not keep compute running. - SDK client timeouts. The Lambda client the SDK uses has a 5 second connect timeout, a 50 second response timeout and a 55 second total request timeout. The AWS standard retryer handles throttling and server faults. A checkpoint call that still fails ends the invocation even if your code catches the error, and Lambda invokes the function again.
- Large results. A child context result larger than 256 KiB is not stored, so replay runs that child again. For big payloads,
NewFileSystemSerdeswrites values to files on a durable mount such as Amazon EFS. The ephemeral/tmpis not durable, so don’t point it there. - Version stability. The core SDK follows semantic versioning. The plugin API is marked experimental, and the
insightanddurablelintmodules stay below v1, so any release of those may make incompatible changes.
Mistakes that break a durable Go workflow on resume
Reading the clock or randomness between operations
time.Now, math/rand, crypto/rand and UUID constructors give a different value on replay, which sends the handler down a different path. Context.RequestID changes on every invocation too. Fix: wrap the call in a durable.Step so its value is saved, or use durable.ExecutionStartTime when a stable timestamp is enough.
Creating operations while ranging over a map
Go randomises map iteration order, so operations come out in a different order on replay. Fix: sort the keys into a slice and loop over the slice.
Starting goroutines with go or errgroup
A durable.Context belongs to the goroutine that created it. Calling an operation from another goroutine fails with durable.ErrWrongGoroutine. Fix: use durable.Go. It claims the child operation on the calling goroutine and hands the new goroutine a child context of its own. Await several futures with a combinator such as All, AllSettled or Join. A chain of Result calls that returns on the first error leaves the other futures unawaited.
Using the parent context inside a child function
Inside a Go, Map, Parallel or RunInChildContext body, it is easy to type ctx when you meant child. The SDK rejects this with ErrWrongContext or ErrWrongGoroutine, and durablechildctx flags it before deploy.
Writing to captured variables inside a step
On replay a step returns its saved result without running the body, so any variable the body assigned keeps its old value. Fix: return the value from the step and assign it outside.
Assuming a step body runs exactly once
Under the default AtLeastOncePerRetry, a crash or timeout between starting a step and recording its outcome makes the body run again on resume. Charges, deploys and emails must be idempotent. One way to do that is to generate the idempotency key in a step of its own. Its result is saved, so every later attempt of the side-effecting step reuses the same key. AtMostOncePerRetry treats the interrupted attempt as a failure and asks the retry strategy what to do.
Swallowing the suspension signal
A non-nil error from an operation is either a typed failure, such as *durable.StepError, or the signal that the invocation is suspending. The signal matches no public type. Fix: return any error you don’t recognise unchanged, and always return the error from Wait.
Matching errors with your own types
A failed step returns the recorded ErrorType and Message, which the SDK rebuilds the same way on replay. It does not carry the original error value. Fix: match on stepErr.ErrorType. Calling errors.As against your own error type will not match.
Reading callback results without a decoder
By default a callback returns the submitted bytes unchanged. Sending "approved" gives you the string with its quotes. Fix: pass durable.WithCallbackSerdes(durable.JSONSerdes), as in the example above.
Porting retry configs literally from JavaScript
In Go, a zero MaxAttempts selects the default: 3 for RetryConfig, 6 for LinearRetryConfig and 60 for WaitConfig. It does not mean one attempt. A JavaScript maxAttempts: 0 or 1 becomes durable.NoRetry().
Changing operation order or subtypes under live executions
Replay checks names and types against the checkpoint log. The README warns that changing a step’s subtype breaks executions already in flight. Treat any rename or reorder of operations the same way, because executions that are still waiting will replay against the new code.
Reading durable executions in production
The SDK logger is a *slog.Logger. By default it writes one JSON object per record to stderr. Every record includes requestId and executionArn, and records from step bodies add operationId, operationName and attempt. The field names match the other durable execution SDKs, so one CloudWatch query works across languages. Lines are logged once, in the invocation that first runs them. Replayed lines are dropped unless you switch on ReplayLogModeEmit, which marks them with replay: true. Two cases still log a line twice: a step body that reruns under at-least-once semantics, and lines from an invocation that crashed or timed out.
When something looks stuck, start with the execution. get-durable-execution gives the status and any recorded error. get-durable-execution-history lists each step, wait, callback and invocation boundary. A WARN record with the message “Checkpoint response contained no CheckpointToken” means the service stopped accepting checkpoints from that invocation. The execution then continues on the next one. If you give the SDK a handler enabled at Debug level, it adds trace records such as invocation suspending, which carries a reason of wait, callback, invoke, retry, condition or combinator. When I built and led Workers observability at Cloudflare, the hard part of serverless debugging was joining one logical run across many short invocations. Here executionArn on every line does that join for you, so index it in your log tool from day one.
Fixing stuck durable executions with Polylane
Polylane detects production issues and fixes them by opening pull requests. It connects to AWS, so when a durable execution fails or stalls it works out why, and the fix arrives ready for your team’s normal review.
Running on AWS? See how Polylane monitors AWS in production.
Common questions.
When did the Lambda Durable Execution SDK for Go become generally available?
AWS announced general availability on 15 October 2026. The announcement says Go developers can now build long-running workflows with Lambda durable functions. You can find the SDK on GitHub at aws/aws-durable-execution-sdk-go.
Can I turn on durable execution for an existing Go Lambda function?
No. The Lambda configuration guide says durable execution can only be enabled when a function is created. Create a new function with a DurableConfig that includes the required ExecutionTimeout, then move traffic over to it.
Which runtime and Go version does the SDK need?
You build a static Linux binary named bootstrap and deploy it on the provided.al2023 runtime. The SDK needs Go 1.24 or later, and a minor release may raise that minimum.
How long can a durable Go function run?
A whole durable execution can run for up to one year. ExecutionTimeout accepts 1 to 31622400 seconds. Each invocation is still limited by the function Timeout, which has a maximum of 15 minutes. A synchronous invoke is also capped at 15 minutes, so start long workflows with --invocation-type Event.
What IAM permissions does a durable function's role need?
Besides permission to write CloudWatch Logs, the execution role needs lambda:CheckpointDurableExecution and lambda:GetDurableExecutionState. The SDK calls both APIs while checkpointing and replaying.
How do I test a durable Go workflow without deploying it?
Use the durabletest package. NewLocalRunner(handler).RunUntilComplete(event) runs the handler in your process with no network or AWS credentials and skips through timers and retries. Add durablelint ./... in CI to catch nondeterminism, goroutine misuse and nested operations.
How is it billed?
AWS lists the billing dimensions as compute duration, requests, durable operations, storage written, storage persisted and data transfer. A suspended execution does not pay for compute, but checkpoints are stored through the wait and the retention period. AWS directs readers to the Lambda pricing page for rates.
Should I use durable functions or Step Functions?
AWS suggests durable functions when the workflow is tightly tied to business logic in a Lambda app and you want to write it in code. It suggests Step Functions for orchestration across AWS services when you want a visual designer and its native integrations with more than 220 services.
Sources (8)
- AWS Lambda Durable Execution SDK for Go is now generally available
- AWS Durable Execution SDK for Go (GitHub)
- durablelint README
- Key Concepts - AWS Durable Execution SDK Developer Guide
- Lambda durable functions - AWS Lambda Developer Guide
- Configure Lambda durable functions
- Polylane documentation
- Polylane full content
Boris Tane is the founder of Polylane. He previously founded Baselime, observability for the future of the cloud, which Cloudflare acquired. At Cloudflare he built and led the Workers observability team.
Related
- How to correlate incidents with deploys using evidence Learn how to correlate incidents with deploys using release markers, metrics, logs and verified GitHub, Vercel, Cloudflare and Kubernetes change events.
- Cloudflare Durable Objects pending I/O keep-alive guide From 2026-10-01, pending I/O keeps Cloudflare Durable Objects in memory after the client leaves. What counts, the 15-minute limit, flags and billing.
- AWS Lambda durable functions in GovCloud: setup and limits Lambda durable functions reached AWS GovCloud (US-West) and (US-East) on 16 October 2026. What they do, how to set one up, and the limits AWS documents.
- CloudWatch log centralisation: GovCloud and 13 new Regions AWS added CloudWatch log centralisation to GovCloud (US) and 13 commercial Regions on 16 October 2026. Setup steps, pricing, limits and gotchas.
- Fix AuthorizerConfigurationException in API Gateway AuthorizerConfigurationException means API Gateway couldn't call your Lambda authorizer or couldn't read its response. Find the cause and fix it.