Introducing Taskturbine
Through most of 2025, I had been working on Sentry’s task platform. That system operates with pretty high throughput (~200,000 tasks/second), so I’m familiar with the challenges in operating big systems in this space. In November 2025, Armin published a post on Absurd and I was intrigued by the operational simplicity that ‘just Postgres’ could provide. After seeing Armin’s work, I wanted to explore this space further. I thought it would be a good opportunity to learn more Rust, get experience using Py03 to build a Python package, optimize some SQL queries, and potentially also build extensions for PHP and JavaScript with a common Rust library. Using the schema, and general API from Absurd as a starting point, I wanted to build a ‘core’ library in Rust, and then extend that core library with SDKs for a variety of languages starting with Rust and Python. The end result is taskturbine
Durable tasks
Durable tasks (or workflows) are a powerful tool for building reliable application logic that can survive crashes, application restarts, and network failures without losing or duplicating work. Durable tasks are composed of ‘steps’. As steps are completed ‘checkpoints’ are stored with the result of each step. If a task fails or is interrupted, it can continue where it left off on the next attempt by skipping any steps that already checkpoint state stored. By thoughtfully structuring your application logic you can build fault-tolerant applications with greater reliability and consistency than traditional fire-and-forget task systems.
Getting started with Taskturbine in Python
Once you’ve installed the taskturbine package, you can create an application. The `app` needs to be an importable from a module as the Python worker uses multiprocessing:
- from taskturbine import Config, TaskturbineApp
- config = Config(
- app_module="demoapp.tasks:app",
- database_url="postgres://app:password@localhost:5432/demoapp",
- usecase="demoappp"
- )
- # Build an app that can have tasks attached.
- app = TaskturbineApp(config=config)
- # Install/update schema with migrations
- app.update_schema()
The update_schema() method will create a Postgres schema where taskturbine will store all of its tables. With an application created, you can define your first task:
- from taskturbine import TaskContext
- @app.register_task("hello-world")
- def hello_world(ctx: TaskContext) -> None:
- print('Hello {ctx.params["name"]}')
We can spawn tasks using our app as well:
- app.spawn_task("hello-world", {"name": "Mark"}, retry_seconds=30)
To run our tasks, we need to create a Worker and run it:
- worker = app.worker("worker-1")
- worker.run()
A hello-world example isn’t overly interesting, but if we were building a workflow like a user deletion or account cancellation we run into more interesting problems like coordinating cleanup operations across multiple systems:
- @app.register_task("account-cancellation")
- def account_cancellation(ctx: TaskContext) -> None:
- # Define all the steps for our cleanup operation
- @ctx.step("unsubscribe-mailing-list")
- def unsubscribe_mailing_list(user_id: int) -> dict[str, str]:
- logger.info("starting unsubscribe")
- subscription_id = mailing_list_service.unsubscribe_user(user_id)
- return {"subscription_id": subscription_id}
- @ctx.step("remove-data-from-blob-storage")
- def cleanup_blob_storage(user_id: int) -> dict[str, list[str]]:
- logger.info("starting blob cleanup")
- removed = []
- for blob_id in blob_storage.list_for_user(user_id):
- blob_storage.remove(blob_id)
- removed.append(blob_id)
- return {"blobs": removed}
- @ctx.step("remove-database-data")
- def cleanup_database(user_id: int) -> bool:
- logger.info("starting db cleanup")
- cleanup_service.remove_user(user_id)
- return true
- # Run the steps in the order you want
- user_id = ctx.params["user_id"]
- subscription = unsubscribe_mailing_list(user_id)
- logger.info(f"removed mailing list data {subscription}")
- blob_ids = cleanup_blob_storage(user_id)
- logger.info(f"removed blobs {blob_ids}")
- cleanup_database(user_id)
- logger.info("completed user cleanup")
- return None
Each step will store its results as a ‘checkpoint’. Should the task or one of its steps encounter an error and crash, taskturbine can resume from the most recently completed step on the next attempt. Logic outside of steps is not durable, and will be re-run each time the task executes.
External events and waits
Like many other durable workflow systems, taskturbine also includes support for external events, and sleeps. External events are a great way to incorporate webhooks. For example, in a checkout flow you may need to wait for a payment intent to be completed before you mark the order as paid and make update the order status.
- @app.register_task("process-checkout-order")
- def process_checkout_order(ctx: TaskContext) -> None:
- @ctx.step(name="reserve-inventory")
- def reserve_inventory(order: Order) -> str:
- reservation_id = inventory_service.reserve_for_order(order)
- return {"inventory_reservation": reservation_id}
- @ctx.step(name="release-inventory")
- def release_inventory(reservation_id: str) -> dict[str, Any]:
- reservation_id = inventory_service.release_reservation(reservation_id)
- return {"released": True}
- @ctx.step(name="cancel-order")
- def cancel_order(order: Order) -> dict[str, Any]:
- order_service.cancel(order)
- return {"cancelled": True}
- @ctx.step(name="process-order")
- def process_order(order: Order) -> str:
- status = update_order_status(order, OrderStatus.PAID)
- return {"status": status}
- order = order_service.get(ctx.params["order_id"])
- reservation = reserve_inventory(order)
- # Wait for the payment to be completed (via async webhook)
- # Each event must have a unique id
- payment = ctx.await_event(f"order:payment:{order_id}", timeout=timedelta(minutes=10))
- # Once our payment event has been received, the task will re-run with the data
- # captured by the event.
- if payment["state"] == PaymentStatus.COMPLETE:
- process_order(order)
- else:
- release_inventory(reservation["reservation"])
- cancel_order(order)
In the HTTP endpoint that handles our payment provider webhooks we could capture a task event with:
- payment_state = PaymentStatus.from_webhook(webhook_data["payment"])
- app.emit_event(f"order:payment:{order_id}", {"state": payment_state})
When events are captured, any tasks waiting on the event will attempt another run.
What’s next?
This project is currently in a development preview stage. I still have to finish off writing documentation, and doing more extensive testing for performance and failure mode scenarios. I’m also planning on adding support for PHP and Javascript as there are good Rust bindings for each of these languages. The CLI tool is also very early on and could use a more effort to polish it off and extend the number of usage scenarios that it can cover. Lastly, I’d love to make a simple monitoring dashboard that gives a web-based GUI that lets an operator view the current state of tasks, and cancel/retry/start tasks. If this project is something that you’re interested in contributing to, open a discussion or issue on GitHub.
There are no comments, be the first!