Skip to content

Coming from arq

arq and gylo share a temperament — async-first, small API, no ceremony. The move is mostly mechanical: tasks stop being strings, Redis becomes Postgres, and a few of arq's per-call knobs become policy.

The structural difference

arq enqueues by name into Redis: await redis.enqueue_job("send_report", 42) — a typo in the string is discovered at run time, and the enqueue happens outside any transaction. gylo enqueues through the task object, on a database connection you pass:

import gylo

app = gylo.Gylo()


@app.task
async def send_report(user_id: int) -> None: ...


async def nightly(conn) -> None:
    await send_report.enqueue(conn, 42)

The task is an object with the function's real signature, so a wrong argument fails the type check, and the insert joins whatever transaction the connection holds. Workers keep their own registry the same way arq's WorkerSettings.functions does — a name no worker recognises dead-letters rather than retrying forever.

Concept map

arq gylo
WorkerSettings.functions = […] tasks register by decoration on the app
await redis.enqueue_job("name", x) await task.enqueue(conn, x) — object, not string
_defer_by=60 / _defer_until=dt options(delay=60) — seconds from now only
_queue_name options(queue=…)
_job_id for deduplication options(unique=True) on arguments, or unique="your-key"
ctx as mandatory first parameter context=True opts in, gylo.JobContext
ctx["job_try"] ctx.attempt, with ctx.final for the last one
raise Retry(defer=…) just raise — policy retries with exponential backoff
max_tries options(max_attempts=…)
job_timeout timeout — on by default at 300s
keep_result=3600 store_result=True; kept for the retention window
await job.result(timeout=…) await gylo.outcome(conn, job_id) — poll, no blocking wait
abort() gylo.cancel(conn, *ids) — not-yet-started jobs only
cron(func, hour=9, minute=0) in settings @app.cron("0 9 * * *") on the task itself
Redis Postgres

What gylo will not do the same way

The task cannot choose its own retry delay. arq's Retry(defer=10) lets a failure pick when to come back — useful for honouring a Retry-After header. In gylo backoff is policy: exponential from --retry-base to --retry-cap with jitter, computed in the database. A task that must wait a specific interval re-enqueues itself with options(delay=…) and raises gylo.NoRetryError — explicit, but a real difference.

No on_startup / on_shutdown hooks. arq gives the worker a lifespan for building HTTP sessions and closing them. gylo's children import your module and go; per-process setup happens at import time or lazily in tasks. A worker lifespan hook is a known gap.

No absolute scheduling at enqueue. _defer_until with a datetime has no equivalent — delay is seconds from now, and recurring absolute times are cron's job.

Results expire on the retention clock, not per job. arq's keep_result is per-call; gylo's window is the completed-job retention setting (24h default) for everything. Results that must outlive it belong in your own tables, written by the task — ideally as a durable step.

More machinery is available when you want it. Workflows (chain, group, chord), durable steps, keyed concurrency and priorities have no arq counterpart — nothing to migrate, but worth knowing they exist before building them by hand. Start at Workflows.