# Lambda Durable Execution SDK for Go is GA: a practical guide

> AWS made the Lambda Durable Execution SDK for Go generally available on 15 October 2026. What it does, how to ship one, and the limits that bite.

By Boris Tane, Founder of Polylane · Published October 17, 2026 · 15 min read
Canonical: https://polylane.com/learn/reliability/aws-lambda-durable-execution-sdk-go-generally-available/

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.

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.Step`** runs a function, checkpoints its result and retries on error.
- **`durable.Wait`** suspends the execution for a set time with no compute running.
- **`durable.CreateCallback` and `durable.WaitForCallback`** pause until an outside system sends a result. This is how human approvals and agent-in-the-loop flows work.
- **`durable.Invoke`** calls another durable function and waits for its result. This is the announcement's "durable invocation for reliable function chaining".
- **`durable.Map`, `durable.Parallel` and `durable.Go`** fan work out concurrently in a way replay can repeat.
- **`durable.WaitForCondition`** polls 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.

```mermaid alt="How a durable Go handler checkpoints, suspends, and replays from the top until it finishes"
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.

1. **Create a module and add the SDK.**

```bash
mkdir rollout && cd rollout
go mod init example.com/rollout
go get github.com/aws/aws-durable-execution-sdk-go
```

2. **Write the handler in `main.go`.** Every side effect goes inside a step body. The approval uses `WaitForCallback`, which runs the submitter as a step and then blocks until someone sends a result.

```go
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)
}
```

3. **Build a static Linux binary named `bootstrap`.** That is the file the `provided.al2023` runtime runs.

```bash
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o dist/bootstrap .
```

4. **Create the function with a `DurableConfig`.** `ExecutionTimeout` is required whenever you set `DurableConfig`. Without it, creation fails with `InvalidParameterValueException`. Size it to cover the longest path through your workflow, which here is the bake time plus a full day of waiting for approval.

```yaml
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
```

5. **Give the execution role the two durable API actions.** Besides permission to write CloudWatch Logs, the role needs `lambda:CheckpointDurableExecution` and `lambda:GetDurableExecutionState`. The SDK calls these on every checkpoint and every replay.

6. **Deploy.**

```bash
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
```

7. **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 the `DurableExecutionArn`.

```bash
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
```

8. **Watch it, then approve it.** The execution reports `RUNNING` while it waits. The callback ID is in the step's log line and in the `CallbackStarted` event of the execution history.

```bash
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.

```go
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.

```bash
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, `NewFileSystemSerdes` writes values to files on a durable mount such as Amazon EFS. The ephemeral `/tmp` is not durable, so don't point it there.
- **Version stability.** The core SDK follows semantic versioning. The plugin API is marked experimental, and the `insight` and `durablelint` modules 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](https://polylane.com/for/aws/).

## 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

- [AWS Lambda Durable Execution SDK for Go is now generally available](https://aws.amazon.com/about-aws/whats-new/2026/10/aws-lambda-durable-go-sdk-support/)
- [AWS Durable Execution SDK for Go (GitHub)](https://github.com/aws/aws-durable-execution-sdk-go)
- [durablelint README](https://github.com/aws/aws-durable-execution-sdk-go/blob/main/analysis/README.md)
- [Key Concepts - AWS Durable Execution SDK Developer Guide](https://docs.aws.amazon.com/durable-execution/getting-started/key-concepts/)
- [Lambda durable functions - AWS Lambda Developer Guide](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html)
- [Configure Lambda durable functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-configuration.html)
- [Polylane documentation](https://docs.polylane.com/llms-full.txt)
- [Polylane full content](https://polylane.com/llms-full.txt)

## About the author

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](https://polylane.com/learn/deployment-safety/how-to-correlate-incidents-with-deploys/): 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](https://polylane.com/learn/reliability/cloudflare-durable-objects-pending-i-o-keep-alive/): 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](https://polylane.com/learn/reliability/aws-lambda-durable-functions-govcloud-availability/): 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](https://polylane.com/learn/monitoring-coverage/amazon-cloudwatch-log-centralization-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](https://polylane.com/learn/troubleshooting/aws-authorizerconfigurationexception/): AuthorizerConfigurationException means API Gateway couldn't call your Lambda authorizer or couldn't read its response. Find the cause and fix it.

Get started with one command: `curl -fsSL https://polylane.com/setup | bash` installs the CLI, connects your coding agents, and creates the account.
