Lilota, a lightweight solution for long running task
Modern APIs often need to perform work that takes seconds or even minutes to complete. Examples are
Lilota, a lightweight solution for long running task
Modern APIs often need to perform work that takes seconds or even minutes to complete. Examples are
- generating reports
- processing uploaded files
- importing data
- sending emails
Blocking the HTTP request until the work finishes creates a poor user experience and can cause timeouts.
This is exactly where lilota shines. It allows you to schedule long-running tasks and immediately return control to the client while the task runs in the background.
In this tutorial, you’ll learn how to integrate lilota into a FastAPI application and expose endpoints for:
- Creating a background task
- Scheduling a report
- Checking task status
- Retrieving task results
In this example, we do not actually create a report. We simply simulate the process to show how to schedule such tasks using lilota.
Installation
Install FastAPI and lilota using uv:
uv add "fastapi[standard]" lilota
Project structure
A simple project might look like this:
app/
├── main.py
├── tasks.py
└── models.py
Define input and output models
Let’s create a task that generates a report.
models.py
from dataclasses import dataclass
@dataclass
class ReportInput:
customer_id: int
@dataclass
class ReportOutput:
filename: str
The input model contains the information required to generate the report. The output model represents the result (here a filename) that will be stored when the task finishes.
Create a lilota instance
tasks.py
from lilota.worker import LilotaWorker
worker = LilotaWorker(
db_url="sqlite:///tasks.db"
)
Lilota stores all information in a database. In the db_url a connection string is specified (here to a SQLite database named tasks.db). It is also possible and recommended for larger projects to specify a connection string to connect to a PostgreSQL database or any other database that is supported by SQLAlchemy.
Register a background task
Now register the function that performs the work.
tasks.py
import time
from models import ReportInput, ReportOutput
@worker.task
def generate_report(data: ReportInput) -> ReportOutput:
# Simulate a long-running operation
time.sleep(10)
# Return the output
return ReportOutput(
filename = f"report-{data.customer_id}.pdf"
)
def main():
worker.start()
if __name__ == "__main__":
main()
In a real application, this function could
- send emails
- query a database
- generate a PDF
- upload files to cloud storage
- run expensive calculations
Create the FastAPI application
main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from lilota.core import Lilota
from models import ReportInput
from uuid import UUID
Start lilota during application startup
lilota = Lilota(
db_url="sqlite:///tasks.db",
script_path="tasks.py"
)
@asynccontextmanager
async def lifespan(app: FastAPI):
lilota.start()
yield
lilota.stop()
app = FastAPI(lifespan=lifespan)
This ensures the scheduler and worker are started when FastAPI starts and shut down cleanly when the application exits.
Endpoint: Create a report
Add an endpoint that schedules the report generation.
@app.post("/reports")
def create_report(data: ReportInput):
task_id = lilota.schedule("generate_report", data)
return {
"task_id": task_id
}
After the endpoint has been executed, the report generation continues in the background.
Endpoint: Check Task Status
Clients need a way to determine whether the task is still running.
@app.get("/tasks/{task_id}")
def get_task(task_id: UUID):
task = lilota.get_task_by_id(task_id)
return {
"id": task.id,
"status": task.status
}
Endpoint: Retrieve Results
Once the task is finished, the generated output is returned.
@app.get("/tasks/{task_id}/result")
def get_result(task_id: str):
task = lilota.get_task(task_id)
return {
"status": task.status,
"result": task.output,
}
Run the example
Start the FastAPI application
uvicorn main:app --reload
Create a report:
curl -X 'POST' \
'http://127.0.0.1:8000/reports' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"customer_id": 42
}'
Response:
{
"task_id": "b26fb0c8-9299-4e36-9ac3-0914c6141fea"
}
Retrieve task information:
curl -X 'GET' \
'http://127.0.0.1:8000/tasks/b26fb0c8-9299-4e36-9ac3-0914c6141fea' \
-H 'accept: application/json'
Response:
{
"id": "b26fb0c8-9299-4e36-9ac3-0914c6141fea",
"status": "completed"
}
Return the generated output:
curl -X 'GET' \
'http://127.0.0.1:8000/tasks/b26fb0c8-9299-4e36-9ac3-0914c6141fea/result' \
-H 'accept: application/json'
Response:
{
"status": "completed",
"result": {
"filename": "report-42.pdf"
}
}
Why Use lilota Instead of FastAPI BackgroundTasks?
FastAPI includes BackgroundTasks, which is great for very small workloads.
However, BackgroundTasks:
- Run inside the application process
- Do not persist state
- Cannot easily survive restarts
- Provide no built-in task tracking
Lilota adds:
- Persistent task storage
- Task status tracking
- Progress reporting
- Result storage
- Dedicated workers
while remaining much simpler than introducing Celery, RabbitMQ, or Redis.
Conclusion
Lilota integrates naturally with FastAPI:
- Start lilota during application startup
- Register task functions
- Schedule tasks from API endpoints
- Return task IDs immediately
- Expose endpoints for status and results
With only a small amount of code, you gain a reliable background-job system that is lightweight, persistent, and easy to operate.
Links
You can find the complete example on GitHub: https://github.com/tobiasroessler/lilota-fastapi
The full lilota documentation is available here: https://tobiasroessler.github.io/lilota/
메타데이터
- post_id
- 40d43e9ed96a
- slug
- lilota-a-lightweight-solution-for-long-running-task-40d43e9ed96a
- url
- https://medium.com/@tobiasroessler/lilota-a-lightweight-solution-for-long-running-task-40d43e9ed96a
- canonical_url
- https://medium.com/@tobiasroessler/lilota-a-lightweight-solution-for-long-running-task-40d43e9ed96a
- author_url
- https://medium.com/@tobiasroessler
- status
- ok
- fetched_at
- 2026-07-09 20:10:33