Load-Bearing · No. 03 · 13 October 2026

Hatchet

Multi-step AI jobs that survive a crash without paying twice for the steps that already worked, and retry the flaky ones on their own, on the Postgres you already run.

Load-Bearing is a weekly note for people who fund and run AI products: one open-source repository, run hands-on, and the adopt, build or buy decision behind it. All issues

Repo
hatchet-dev/hatchet
Licence
MIT
Age
2 yr 9 mo
Contributors
89
Releases, 6 mo
100
Installs / month
1.2M pypi:hatchet-sdk
Who owns it
Platform engineering, with the AI lead deciding which steps must be safe to repeat
First value
2 weeks
Verdict
Adopt
Measured 2026-09-26 from GitHub, PyPI and npm

The decision

Your AI pipelines chain a paid model call to steps that fail every now and then, like a render, an upload or an API with a rate limit, so when one of them falls over halfway, what does recovery cost and who finds out? This one is about whether to put those pipelines on a durable workflow engine, who should own it, and what it saves you in model spend.

The problem it removes

Most AI pipelines start life as a loop in a script, and that works fine until a step times out after the expensive part has already run, or a machine restarts in the middle of a batch. Then the output is lost, the batch runs again from the top and every model call gets paid for twice, and nobody sees the waste because nobody is measuring it.

What I ran

My job-search pipeline tailors a CV with a model call, renders it to PDF in Chrome and then checks the PDF, and the render times out every now and then with no retry around it in my code.

I ran twelve jobs through the plain loop and through Hatchet with the same injected render failures, and then again with the process killed halfway.

The plain loop paid for twelve model calls and delivered ten CVs, and killed and restarted it paid for eighteen and still delivered ten.

Hatchet delivered all twelve both times. After the crash it paid for fifteen, because it kept the finished steps and only the three calls running at the moment of the kill ran again, with waiting work moving to a new worker in four seconds and the interrupted steps following after twenty-nine.

The model step was a stand-in, so I counted calls instead of euros.

Where it sits in the stack

Around the pipeline, between the code that does the work and the machines that run it. Each step becomes a task with retries, timeouts and a stored result, a small server on Postgres keeps track of every attempt, and any worker can pick up where another one stopped. It fits agents too, since a long wait for a human approval becomes a paused step and no process has to sit around waiting.

The first 30 days

Week 1. Pick the pipeline with the most expensive step and the flakiest neighbour, run the server next to your existing Postgres and wrap the steps you already have. My workflow code was about forty lines and left the step code untouched.

Weeks 2 to 4. Set retries and timeouts per step, make the paid steps safe to repeat, and put the dashboard in front of whoever is on call.

Skills. An engineer who is comfortable with queues and Postgres, and no new data store.

Running cost. One small service and the database you already have.

Working at day 30 means a failed step retries on its own, a restart only costs what was running at the time, and someone can see every attempt of every job.

Risk

Durable means at-least-once. A step that was running when a worker died runs again, so if that step charges a customer or sends an email, make it idempotent or split it so the part that must not repeat is as small as possible.

Defaults built for short tasks. Steps time out after sixty seconds unless you say otherwise, and long model calls take longer than that, so set timeouts per step on day one.

Pace of change. There were a hundred releases in six months across the server and SDKs, and I ran into a deprecation on the very first day, so pin your versions and read the release notes before upgrading.

Maintainers. Four people carry most of the work and the company behind it sells a hosted version, but the self-hosted server is MIT and complete for this use, and your way out is the step code, which stays plain functions.

The organisational risk is that the pipeline turns into a platform nobody owns, so give it an owner in platform engineering and give the AI lead the list of steps that must be safe to repeat.

Build, adopt or buy

Build. You can make a loop skip finished jobs and retry a flaky step, and that covers the simple crash, but the months go into the rest of it: steps in flight on a dead machine, backoff, concurrency limits, human approvals and a record of every attempt.

Buy. Hosted workflow services, including this project's own, take the server off your plate, which is worth it when nobody wants to run one or when the volume grows.

Adopt it self-hosted wherever Postgres is already run well, and keep the step functions plain so you can swap the engine later.

Verdict

I'd adopt it. On my pipeline it turned lost work into delivered work and cut the cost of a crash down to the steps that were in flight, and all it asks for is one service and a database you probably already have. Before you roll it out, list every step that must not run twice and make each one safe to repeat, because a durable engine will happily charge a customer twice if you let it.

Engineering notes
  • uv pip install hatchet-sdk: 8 s, 87 MB (v1.41.1). Server: the Hatchet Lite compose file from the docs. Pin the image (hatchet-lite:v0.107.2) and bind ports to 127.0.0.1. Pull 113 s, ready 8 s.
  • Worker token without the dashboard: docker compose exec hatchet-lite /hatchet-admin token create --config /config --tenant-id <id>. The id is in the Tenant table. Set HATCHET_CLIENT_TOKEN and HATCHET_CLIENT_TLS_STRATEGY=none for local use.
  • Steps: @wf.task(parents=[previous], retries=3, backoff_factor=2.0, backoff_max_seconds=30, execution_timeout=timedelta(minutes=5)). Read a parent's result with ctx.task_output(previous), and ctx.attempt_number tells a step which try it is on.
  • hatchet.worker(name, slots=3, workflows=[wf]).start(). Slots cap concurrency per worker, and Chrome was happy with three.
  • run_many_no_wait() is deprecated. Use run_many(..., wait_for_result=False).
  • A killed worker's running steps were requeued after about 29 s, and queued steps moved in about 4 s.