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. Nice headline. Not the part that got me.
The part that got me is 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. For someone who walked away from Functions on purpose, that's 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. It breaks out into containers that drop neatly onto Container Apps or App Service. One language, one build story. I like it that way.
Functions never fit that. For years Go's only path there was Custom Handlers — a polite name for running your own web server behind an HTTP shim. So I skipped it and kept everything in containers.
The honest cost: I left compute on the table. There's a whole class of work that's event-shaped and doesn't deserve an always-on container — email delivery, scheduled jobs, queue-driven tasks that wake up, do one thing, and go back to sleep. 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 isn't one event — it's a process. Take email done right: send, wait for the delivery or bounce webhook, retry on a blip, escalate after a few tries, record the result — maybe over hours. If it dies halfway, it has to resume where it left off, not start over and double-send. That's not a function. 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. It persists state so the whole thing survives a restart.
Here's that email flow as an orchestration, adapted from Microsoft's own samples. The retrying, the waiting, the resume-after-a-crash — that's the framework's job now, not mine:
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 is the whole point. The orchestration sleeps with nothing running, survives a process restart, and resumes exactly where it left off when the webhook fires. That's the part I keep rebuilding by hand.
The piece that made me sit up is the Durable Task Scheduler — the managed backend that works with Durable Functions and the standalone Durable Task SDKs. The engine isn't welded to the Functions host anymore. So 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 to the Functions host. Same language, same lineage, my choice of where it runs.
Where I Draw the Line
I'm not moving anything critical onto this today. durabletask-go says plainly 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 you persist, replay, and have to reason about when it goes sideways — and the code has to be deterministic, because the framework replays it from history to rebuild state. You don't own the workflow until you've watched it crash and recover.
So this isn't a migration. It's a door I closed on purpose, and Go finally gave me a reason to put a handle back on it. When the SDK graduates, I already know which hand-rolled job walks through first.


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.