I Left Azure Functions Behind. Go Durable Functions Might Bring Me Back.
At Build this week, Microsoft shipped Go support for Azure Functions: first-class, in public preview, on the Flex Consumption plan. That's a solid headline if you've been waiting on the language, but on its own it wouldn't have gotten a post out of me.
The part that pulled me in sits underneath it. Microsoft is building the Durable Task Framework for Go, with a managed backend that works whether you run on Functions or in your own container. I walked away from Functions on purpose, and this is the first thing in a while that's made me look back.
Why I Left, and What It Cost Me
The platform I've been building is a Go backend in a monorepo that breaks out into containers, and the containers drop neatly onto Container Apps or App Service. One language and one build story, which is how I like it.
Functions never fit into that picture. For years, Go's only path onto the platform was Custom Handlers, which is a polite name for running your own web server behind an HTTP shim. So I skipped Functions and kept everything in containers.
Skipping it cost me something, though: I left compute on the table. There's a whole class of work in my system that's event-shaped (email delivery, scheduled jobs, queue-driven tasks that wake up, do one thing, and go back to sleep), and none of it deserves an always-on container. That's the textbook case for serverless. Instead I run it in long-lived containers and hand-roll the scheduling, retries, and back-off that serverless would have handed me for free.
The Real Prize: Durable Task in Go
Most of that async work turns out to be a multi-step process rather than a single event. Email done right means sending, waiting on the delivery or bounce webhook, retrying after a blip, escalating after a few failed tries, and recording the result — sometimes hours after the first send. If the process dies halfway through, it has to pick up where it left off instead of starting over and double-sending. That's an orchestration, and orchestrations are exactly what hand-rolled plumbing gets wrong.
durabletask-go is Microsoft's Go take on the Durable Task Framework, the same engine behind .NET Durable Functions. You write the workflow as plain Go: call an activity, await it, fan out and back in, or wait for an external event. The framework persists state as it goes, so the whole thing survives a restart.
Here's that email flow written as an orchestration, adapted from Microsoft's own samples. All the retrying and waiting and resuming after a crash is the framework's job now:
func EmailDeliveryOrchestrator(ctx *task.OrchestrationContext) (any, error) {
var msg EmailRequest
if err := ctx.GetInput(&msg); err != nil {
return nil, err
}
// Send it. Transient failures get retried for me.
if err := ctx.CallActivity(SendEmail, task.WithActivityInput(msg)).Await(nil); err != nil {
return nil, err
}
// Park here — maybe for hours — until the provider's webhook reports back.
var receipt DeliveryReceipt
if err := ctx.WaitForSingleEvent("DeliveryReceipt", 24*time.Hour).Await(&receipt); err != nil {
// Nothing came back in time: escalate instead of silently dropping it.
return nil, ctx.CallActivity(Escalate, task.WithActivityInput(msg)).Await(nil)
}
return receipt, nil
}The activities it calls are just normal Go functions, the plain retriable units of work:
func SendEmail(ctx task.ActivityContext) (any, error) {
var msg EmailRequest
if err := ctx.GetInput(&msg); err != nil {
return nil, err
}
return nil, provider.Send(msg)
}That WaitForSingleEvent call is the part I care about. The orchestration sleeps with nothing running, survives a process restart, and resumes exactly where it left off when the webhook fires. I keep rebuilding that exact sleep-then-resume behavior by hand, and mine has never come out this short.
The Durable Task Scheduler is what made me sit up, though: a managed backend that works with Durable Functions and with the standalone Durable Task SDKs. The engine isn't welded to the Functions host anymore, which means I could get real durable-workflow semantics in my own Go container on Container Apps, backed by a managed scheduler. No C# rewrite, no surrendering my deployment to the Functions host, and the engine underneath would still be the same one .NET Durable Functions runs on.
Why I'm Not Migrating Anything Yet
I'm not moving anything critical onto this today. durabletask-go is upfront that it's a work-in-progress that shouldn't run production workloads, and the Go SDK is still experimental. They're right, and I'll take them at their word.
Durable isn't free, either. Every orchestration is state that gets persisted and replayed, and somebody has to reason about that state when a run goes sideways. The orchestrator code also has to stay deterministic, because the framework rebuilds state by replaying your code against its saved history. Before any of this touches production, I want to kill an orchestration mid-wait on purpose and watch it come back without double-sending a thing.
When the SDK stabilizes, the email pipeline goes first. That's the orchestration sketched above; it already lives in a container the scheduler could back, and it's the job I'm most tired of maintaining by hand. Until durabletask-go takes down its own work-in-progress warning, the hand-rolled version keeps that job.


Comments (0)
No comments yet. Be the first to share your thoughts!
Leave a Comment
Sign in with Google, Microsoft, or email to leave a comment.