Absurd: пять месяцев durable execution на Postgres — отчёт Армина Ронахера

Один SQL-файл, SDK на 1400–1900 строк и Postgres вместо отдельного сервиса. Отчёт автора Flask о пяти месяцах эксплуатации Absurd в реальной нагрузке.

Обложка: Absurd: пять месяцев durable execution на Postgres — отчёт Армина Ронахера

Когда воркер падает на шаге 6 из 10, обычно проще всё начать заново или городить свою retry-логику поверх очереди. Durable execution даёт третий путь: каждый шаг — отдельный чекпоинт, при рестарте задача продолжается ровно с того места, где упала. Если эта идея вам близка, но Temporal с его 170 тысячами строк SDK на Python кажется перебором — посмотрите на Absurd. Армин Ронахер, автор Flask, пять месяцев крутит в продакшене систему, которая помещается в один SQL-файл и SDK на 1400–1900 строк (TypeScript и Python соответственно). Ниже — его свежий разбор того, что сработало, а что нет.

Absurd — это система durable execution, целиком живущая внутри Postgres. Ядро — один SQL-файл absurd.sql с хранимыми процедурами для управления задачами, чекпоинтами, событиями и claim-based-планированием (claim — это резервирование задачи воркером, чтобы её не подхватил соседний). Поверх — тонкие SDK на TypeScript, Python и экспериментальный на Go. Никаких отдельных сервисов, плагинов компилятора и собственных рантаймов — только база и тонкая обёртка над ней.

Армин впервые написал про Absurd пять месяцев назад. С тех пор библиотека прошла десяток релизов и реальную нагрузку. Свежий пост — отчёт о том, какие куски архитектуры дожили без изменений, какие пришлось доделать и где остались дыры.

Главное
Что нужно знать про Absurd сегодня
Краткая выжимка из отчёта Армина Ронахера о пяти месяцах эксплуатации

Архитектура из ноября 2025 устояла без переделок: задачи, шаги, чекпоинты, события, suspend — те же абстракции, что и на старте.

Главное обновление — beginStep / completeStep: теперь шаг можно «начать», проверить состояние и только потом завершить. Закрывает кейсы before/after-call хуков.

Появился absurdctl — CLI для миграций, очередей, ретраев. Делает дебаг прод-инцидентов человечным: видишь, на каком шаге задача застряла.

TypeScript SDK — 1400 строк, Python — 1900. Для сравнения, Temporal-SDK на Python — около 170 000.

Главная нерешённая проблема — партицирование таблиц. Detach Partition Concurrently не запускается из pg_cron из-за транзакций.

Перевод выполнен по условиям лицензии оригинала CC BY-NC 4.0. Сама библиотека Absurd распространяется отдельно — под Apache 2.0.

Что такое Absurd, если коротко

Absurd — система durable execution, которая живёт целиком в Postgres. Ядро — один SQL-файл absurd.sql с хранимыми процедурами для управления задачами, чекпоинтами, событиями и claim-based-планированием. Сверху — тонкие SDK, чтобы было удобно вызывать всё это из TypeScript, Python и Go.

Модель простая: вы регистрируете задачу, разбиваете её на шаги, и каждый шаг становится чекпоинтом. Если что-то падает, задача рестартует с последнего успешного шага. Задачи могут спать, ждать внешних событий и приостанавливаться на дни и недели. Всё состояние хранится в Postgres.

Если хотите полное введение — оригинальный пост разбирает фундаменты. Здесь — то, что мы узнали с тех пор.

Что изменилось

За пять месяцев вышло несколько релизов. Большинство правок — то, что и ожидаешь от системы, на которую начали реально рассчитывать: ужесточили обработку claim-ов (резервирование задач воркерами), добавили watchdog-и, которые убивают сломанных воркеров и возвращают их задачи в очередь, защиту от deadlock-ов на уровне Postgres, нормальный lease management (продление «аренды» задачи воркером, который её взял), race-условия на событиях и кучу edge-cases, которые вылезают только под реальной нагрузкой.

Декомпозированные шаги

Изначально было только ctx.step(): передаёшь функцию — получаешь её закешированный результат. Это покрывает большинство кейсов, но не все. Иногда нужно знать, выполнялся ли шаг раньше, прежде чем решить, что делать дальше. Поэтому добавили beginStep() / completeStep(): возвращают handle, который можно проверить до коммита результата. Это очень полезно для моделирования намеренных падений и условной логики.

Особенно нужно при работе с before-call / after-call хук-API.

Результаты задач

Теперь можно стартовать задачу, заняться другими делами и потом вернуться, чтобы получить или дождаться её результата. Звучит очевидно задним числом, но изначально система была чисто fire-and-forget. С нормальным доступом к результатам стало возможно использовать Absurd, например, для спавна child-задач из родительского workflow и ожидания их завершения. Особенно полезно при дебаге с агентами.

absurdctl

Сделали из этого нормальный CLI. Через него инициализируются схемы, гоняются миграции, создаются очереди, спавнятся задачи, эмитятся события, ретраятся падения. Ставится через uvx или как standalone-бинарь.

Для дебага продакшен-инцидентов это незаменимо. Когда что-то застряло, возможность просто написать absurdctl dump-task --task-id=<id> и сразу увидеть, где именно остановилось — это совсем другой опыт, чем копаться в логах.

Habitat

Маленькое приложение на Go, которое поднимает веб-дашборд для мониторинга задач, прогонов, чекпоинтов и событий. Подключается прямо к Postgres и даёт живое представление о происходящем. Простое, но именно такие вещи делают систему приятной для людей.

Интеграция с агентами

Раз Absurd изначально строился под workload-ы агентов, добавили bundled-скилл, который кодинг-агенты могут обнаружить и использовать для дебага состояния workflow через absurdctl. Также описан паттерн, как сделать turn-ы агента pi (это один из coding-агентов) durable: каждое сообщение логируется как чекпоинт.

Что устояло без переделок

Что меня радует больше всего — фундамент проекта почти не поменялся. Базовая модель из задач, шагов, чекпоинтов, событий и suspend-ов — ровно та же, что была в начале. Мы добавили вокруг фичи, но ничего не заставило пересмотреть сами абстракции.

Ставка на «вся сложность в SQL, SDK тонкие» оказалась реально хорошей. TypeScript-SDK — около 1400 строк. Python-SDK — около 1900, и большая часть объёма из-за поддержки «цветных» функций (sync/async). Сравните с Temporal Python SDK на ~170 000 строк. Это значит, что SDK легко понять, легко дебажить и легко портировать. Если что-то идёт не так, можно прочитать весь SDK за полдня и понять, что он делает.

Модель replay по чекпоинтам тоже себя оправдала. В отличие от систем, которые требуют детерминированного replay всей workflow-функции, Absurd просто загружает закешированные результаты шагов и пропускает выполненную работу. Это значит, что код не обязан быть детерминированным вне шагов. Между шагами можно вызывать Math.random() или datetime.now() — всё работает, потому что важны только границы шагов. На практике это сильно упрощает рассуждения о том, что безопасно, а что нет.

Pull-based планирование тоже оказалось правильным выбором. Воркеры сами забирают задачи из Postgres по мере появления свободных ресурсов. Никакого координатора, никакого push-механизма, никаких HTTP-callback-ов. Это делает систему тривиально self-hostable и снимает необходимость думать о load management на уровне инфраструктуры.

Что осталось нерешённым архитектурно

Открытый вопрос — стоило ли строить всё вокруг durable promise (долгоживущего обещания результата, который выживет рестарт процесса). В теории это более мощная абстракция: вместо явных шагов вы получаете объект-обещание, на котором можно ждать или вешать колбэки, а система сама обеспечивает его доставку. На практике — заметно сложнее в реализации. Я делал попытки прикинуть, как выглядел бы Absurd на durable promises, но ни к чему не пришёл. Эксперимент, который было бы интересно довести до конца.

Для чего мы это используем

Основной use-case по-прежнему — workflow-ы агентов. Агент — это, по сути, цикл, который зовёт LLM, обрабатывает результаты тулзов и повторяет, пока не решит, что закончил. Каждая итерация становится шагом, результат каждого шага чекпоинтится. Если процесс умирает на 7-й итерации, он стартует, replay-ит итерации 1–6 из стора и продолжает с 7-й.

Но мы нашли применение и для другого. Все наши cron-задачи теперь просто диспатчат Absurd-таски с заранее сгенерированным ключом дедупликации (формула: ключ = имя крона + текущий слот времени). Поэтому даже два cron-процесса, запущенные параллельно, триггернут только один Absurd-таск — это всё ещё pull-модель, просто триггер живёт снаружи. Также используем для фоновой обработки, которая должна переживать деплои. По сути, везде, где иначе пришлось бы строить свою retry-and-resume-логику поверх очереди.

Чего не хватает

Absurd намеренно минималистичен, но кое-что хотелось бы видеть.

Нет встроенного шедулера. Если хочется cron-подобного поведения — поднимаешь свой scheduler-цикл и используешь idempotency-ключи для дедупликации. Это работает, и у нас есть описанный паттерн, но было бы приятно иметь что-то более интегрированное.

Нет push-модели. Всё на pull. Если нужен HTTP-эндпоинт, чтобы принимать вебхуки и будить задачи — это пишется руками. Я считаю, что это правильный дефолт: push-системы сложнее эксплуатировать, и их легче перегрузить. Но иногда было бы удобно. В частности, для агентных систем было бы здорово иметь нативно встроенные вебхуки (wake on incoming POST). В ядро это совсем не хочется тащить — но звучит как задача для смежной библиотеки поверх Absurd.

Главная дыра — поддержки партицирования пока нет. Это обидно, потому что чистка данных получается дороже, чем могла бы. В теории партицирование добавляется довольно просто: weekly-партиции, которые detach-ишь и удаляешь по мере истечения. Единственная загвоздка — у Postgres нет удобного способа это делать.

Сложная часть — не само партицирование, а управление жизненным циклом партиций под реальной нагрузкой. Если воркер вставляет строку, у которой expires_at попадает в месяц без партиции — insert падает, и workflow рушится. Поэтому нужен отдельный maintenance-loop, который всегда создаёт будущие партиции достаточно далеко вперёд для sleep-ов и retry-ев, и делает это для каждой очереди.

На стороне удаления безопасный путь — DETACH PARTITION CONCURRENTLY, но запустить его из pg_cron не получается, потому что эту команду нельзя выполнять внутри транзакции, а pg_cron всё гоняет в одной.

Не думаю, что это нерешаемая проблема, но решения у меня пока нет, и я был бы рад обратной связи в issues.

А open source ещё имеет смысл?

Это подводит к мета-вопросу: какой смысл в open-source библиотеках в эпоху агентной разработки. Durable execution сейчас продают десятки стартапов. С другой стороны, это то, что вам мог бы написать агент, и люди могут даже не искать готовых решений. Странно немного, да?

Я не верю, что библиотека для durable execution может прокормить компанию, реально не верю. С другой стороны, эта задача ровно настолько сложная, что может стать хорошим open-source проектом без коммерческих интересов. Нужна экосистема вокруг — особенно UI и нормальный DX для дебага — а это сложно собрать одноразовой реализацией.

Не уверен, что мы ответили на этот вопрос окончательно. Но Absurd сейчас уже сильно лучше, чем был несколько месяцев назад.

Часто задаваемые вопросы
1
Чем Absurd отличается от Temporal?

Главное отличие — масштаб. Temporal — большая система с собственным сервером, требующим координатор и отдельную инфраструктуру; Python-SDK Temporal весит около 170 000 строк. Absurd — это один SQL-файл и тонкий SDK на 1400–1900 строк, всё работает поверх обычного Postgres без отдельных сервисов. Absurd проще понять и self-host-ить, но у Temporal больше готовых интеграций и зрелая экосистема.

2
Можно ли использовать Absurd с уже работающим Postgres-приложением?

Да. Absurd подгружается в существующую базу через миграции от absurdctl и не требует отдельного инстанса. Но имейте в виду нагрузку: workflow-операции и чекпоинты создают регулярный поток writes, поэтому для high-traffic-приложений лучше иметь отдельный инстанс или схему. Партицирования пока нет, поэтому большие таблицы со временем потребуют ручной чистки.

3
На каких языках есть SDK?

Стабильные SDK — TypeScript и Python. Есть экспериментальный SDK на Go. Ядро — SQL, поэтому теоретически SDK можно написать под любой язык, у которого есть Postgres-драйвер. Из-за тонкости SDK портировать его — задача на дни, а не на месяцы.

4
Зачем разбивать workflow на шаги, если можно просто обернуть всё в транзакцию?

Шаги — это не транзакции, а долгоживущие чекпоинты, переживающие падение процесса, рестарт сервера и деплои. Транзакция длиной в часы или дни заблокирует ресурсы Postgres и упадёт при первом разрыве соединения. Absurd пишет результат каждого шага отдельно и при рестарте просто пропускает выполненные.

5
Где посмотреть код и попробовать?

Репозиторий — github.com/earendil-works/absurd, лицензия Apache 2.0. Документация и примеры там же. Стартовать проще всего через absurdctl: устанавливаете через uvx, делаете absurdctl init и получаете готовую схему в своей Postgres-базе.

Выводы

Если у вас уже есть Postgres и нужно что-то из перечисленного — workflow-ы для агентов с retry, cron с дедупликацией, фоновая обработка, переживающая деплои, или просто долгоживущая задача с шагами — Absurd подойдёт. Полтора SDK на 1400–1900 строк читаются за вечер, и вы сразу поймёте, как это работает и где ваши данные лежат. Это редкое ощущение для durable-execution систем.

Сам Армин в финале оригинала пишет: «Если вы используете Absurd, думаете о нём или строите что-то рядом — буду рад вашей обратной связи. Баг-репорты, неудобные углы, критика дизайна, контрибьюшены — всё приветствуется. Этот проект становится лучше каждый раз, когда кто-то ковыряется в нём с другой стороны».

Источник: lucumr.pocoo.org — Absurd In Production, 4 апреля 2026.