SQLSpec backend#
Use SQLSpec when the application already uses it or needs SQL queue storage without an object-relational mapper (ORM).
Install and configure#
Install the queue extra and the SQLSpec driver for your database. This SQLite example is suitable for local development:
pip install "litestar-queues[sqlspec]" aiosqlite
from litestar import Litestar
from sqlspec.adapters.aiosqlite import AiosqliteConfig
from litestar_queues import QueueConfig, QueuePlugin
from litestar_queues.backends.sqlspec import SQLSpecBackendConfig
sqlspec_config = AiosqliteConfig(connection_config={"database": "queue.db"})
queue_config = QueueConfig(
queue_backend=SQLSpecBackendConfig(
sqlspec_config=sqlspec_config,
),
execution_backend="local",
)
app = Litestar(plugins=[QueuePlugin(queue_config)])
# Run this from the app's normal SQLSpec migration command or deploy step.
await sqlspec_config.migrate_up(echo=False)
The supplied SQLSpec config selects the adapter. QueuePlugin registers the
queue migration during app and CLI initialization; SQLSpec then runs it through
the same migration command used by the rest of the application. The queue
backend does not migrate the database when it opens. An unsupported adapter
raises a configuration error instead of silently using generic SQL.
Note
Performance
SQLSpec’s sqlspec[performance] extra (msgspec/librt serialization) and
sqlspec[mypyc] extra (compiled SQL parser) speed up statement handling.
These are consumer choices; litestar-queues does not require or install
them on your behalf.
Typical PostgreSQL pickup time#
On a local development machine with an idle worker already running, 95 out of 100 tasks started within about 5.5 ms with asyncpg and 9.5 ms with psycopg. Other SQLSpec adapters use different notification or polling paths, so do not apply these PostgreSQL times to SQLite, MySQL, Oracle, or other databases. Network distance and database load can also increase pickup time.
Schema ownership#
Packaged migrations run through SQLSpec’s extension system. Do not replace the
application’s migration script_location. When migrations run outside the
Litestar app, call configure_queue_migration_extension(sqlspec_config)
before the normal SQLSpec migration command; it registers the packaged revision
because its manage_schema parameter defaults to True.
If the application owns an existing table, pass manage_schema=False to that
same call – configure_queue_migration_extension(sqlspec_config,
manage_schema=False) – so it registers nothing, and set manage_schema=False
on SQLSpecBackendConfig as well. The flag belongs on both; setting it only on
the backend configuration still leaves the standalone call registering the
packaged revision. Map the application’s table with queue_table_name,
column_map, and native_json_columns.
For a small local bootstrap without a migration command, call the backend’s
explicit create_schema() operation after open(). This emits adapter-
specific DDL directly and does not record a migration revision; it is a
development fallback, not a replacement for application migrations.
The default queue table is queue_task. When event history is enabled,
SQLSpec derives its table by adding _event_history to the queue table,
so the default is queue_task_event_history. Set
event_history_table_name only when the application needs a different name.
0001_create_queue_tasks is the only packaged revision. It creates the queue
task table, enabled event history, queue_maintenance for distributed
maintenance coordination, and queue_task_reservation for permanent task
identity reservations. Override the names with maintenance_table_name and
task_reservation_table_name. Schema-qualified custom queue names keep their
schema and add the corresponding suffix to the table part.
See Queue maintenance before scheduling maintenance and
Persistent storage setup before using forever uniqueness.
For a standalone migration command, register the same backend configuration used by the application so custom table names and column mappings are retained:
backend_config = SQLSpecBackendConfig(sqlspec_config=sqlspec_config)
queue_config = QueueConfig(queue_backend=backend_config)
backend_config.configure_migrations(queue_config)
await sqlspec_config.migrate_up(echo=False)
Keep the application’s migration script_location. With
manage_schema=False the backend registers nothing: no extension settings, no
migration directory, and no packaged revision for SQLSpec to discover or apply.
This holds for the Litestar plugin and the standalone migration command alike.
The application owns the queue schema outright and must create every table,
column, and index itself, using the configured physical names. open() does
not create or alter schema, and create_schema() does not advance migration
history.
Schemas created by v0.10.0 and earlier have no packaged forward path to
dispatch_checked_at. 0001_create_queue_tasks uses
CREATE TABLE IF NOT EXISTS, so re-running it cannot retrofit the column.
Add the nullable dispatch_checked_at column and an index over
execution_backend, status, dispatch_checked_at, created_at, and
id in your own migration, using the configured physical column names, or
recreate the schema. Schemas created by v0.11.0 or later already contain both.
Spanner schema provisioning#
Spanner requires its administrative DDL API; do not send queue DDL through the
generic migration runner’s DML execution path. The generic migration runner is
not the Spanner path. Provision the Spanner queue schema with the backend’s
create_schema() operation, which routes through
SpannerQueueStore.create_schema_for_config and submits each statement
through the native DDL operation API. Run it from the deployment process that
owns schema changes. It does not record a migration revision, so track
completion through the application’s own deployment workflow.
Wakeups#
Native worker wakeups are on by default whenever the adapter can push them.
A bare SQLSpecBackendConfig needs no notification settings: the backend
picks the transport from the adapter you configured and provisions everything
it needs. In short, the PostgreSQL drivers (asyncpg, psycopg, psqlpy) get
notify_queue, DuckDB gets poll_queue, and every other adapter –
SQLite, MySQL, CockroachDB, SQL Server, Spanner, and Oracle – falls back to
interval polling. Choose backends carries the full matrix, including how
these compare with the other queue backends.
The durable notify_queue and poll_queue transports ride a SQLSpec events
queue table (sqlspec_event_queue by default). It is provisioned the same way
as the queue table: with manage_schema=True the packaged migration path
registers SQLSpec’s events migration for capable adapters automatically, and the
create_schema() bootstrap emits its DDL directly. A zero-config capable
backend on manage_schema=True therefore works on a fresh database with no
manual step. Set SQLSpecWorkerWakeupConfig.queue_table_name to override the
events queue table name.
With manage_schema=False the backend registers no migrations at all, so the
events queue table is the application’s to create, exactly like the queue table.
The library never creates it implicitly and emits no DDL for it outside
create_schema().
When that table is absent, the first wakeup a worker publishes or waits for
raises QueueConfigurationError naming the table and the transport that needs
it, instead of a raw driver error. The check reads the adapter’s data dictionary
once per backend and creates nothing. Two remedies:
Apply SQLSpec’s packaged events migration to the database from the deployment step that owns schema changes, then start the application again.
Set
worker_wakeups=Noneto run without native wakeups; workers then poll on their configured interval and need no events queue table.
Whether manage_schema is True or False makes no difference to this
requirement: a durable wakeup transport needs the table to exist either way. An
application with manage_schema=False that provisioned the table through its
own migration run passes the check like any other deployment.
To turn native wakeups off and fall back to interval polling, set
worker_wakeups=None. The default SQLSpecWorkerWakeupConfig() selects the
adapter capability; an adapter without a push transport continues interval
polling.
Overrides remain available under worker_wakeups: transport pins a
specific native SQLSpec transport. The canonical names are notify,
notify_queue, poll_queue, aq, and txeventq. channel_name
sets the LISTEN/NOTIFY channel (default litestar_queues_tasks), and
poll_interval controls durable queue polling in seconds. Oracle can opt in
to explicitly provisioned aq or txeventq queues through transport
and settings. The application or database operator owns queue creation,
startup, privileges, payload type, and retention; Litestar Queues only opens
the configured SQLSpec event channel. Oracle stays on interval polling by
default because the backend does not provision Advanced Queuing.
Durable queue transports are competing-consumer queues; do not use them as
multi-process browser fan-out. See Pick up new work faster.
Batch claiming#
SQLSpec stores that advertise a returning claim use one bounded
UPDATE ... RETURNING operation for claim_many. Stores without that
primitive inherit the backend-neutral single-record claim loop. Both paths
preserve the same priority, filtering, ownership, and actual-short-batch
contract; only the number of database operations differs.
Heartbeat isolation#
heartbeat_pool_config can use a dedicated connection pool for heartbeat
writes. It must point to the same database. The backend opens and closes that
pool, and falls back to the main pool if registration fails.
Event history#
SQLSpec event history uses the queue schema, the packaged SQLSpec migration,
and the SQLSpec session lifecycle. Its table naming follows the queue-table
_event_history suffix described above. event_history_table_name customizes the
table.
History batches commit before live delivery is released, including sparse
publications that flush on the configured interval. Query total counts all
matching records before limit and offset; an empty page may still have a
nonzero total. Page and count queries share a session, but consistency during
concurrent writes depends on the configured transaction isolation.
Live SSE/WebSocket delivery still needs a Channels backend. See
Event history.