Compatibility
Graphile Worker RS is a Rust implementation of the PostgreSQL-backed job queue used by Node Graphile Worker. It is designed to use the same database schema and can run side by side with Node workers when both runtimes understand the schema revision installed in PostgreSQL.
This page focuses on database and operational compatibility. Rust task handler code is not portable to Node.js, and Node task functions are not portable to Rust; the compatibility boundary is the jobs stored in PostgreSQL.
What Is Shared
Graphile Worker RS manages a PostgreSQL schema, graphile_worker by default.
The schema stores queued jobs, task identifiers, queue locks, migration state,
and optional worker recovery state.
The current migrations install the same public-facing objects in the
graphile_worker schema that applications typically interact with:
add_job(...)schedules one job.add_jobs(...)schedules many jobs.remove_job(...)removes a keyed job.complete_jobs(...),reschedule_jobs(...),permanently_fail_jobs(...), andforce_unlock_workers(...)administer jobs.jobsis a compatibility view over the private job tables.migrationsis the migration ledger.
Internally, newer migrations store queue data in private tables such as
_private_jobs, _private_tasks, and _private_job_queues. Application code
should prefer the public SQL functions and the jobs view instead of depending
on the private tables directly.
Coexisting With Node Workers
Rust and Node workers can process jobs from the same schema when they register handlers for the same task identifiers and agree on the payload shape.
For example, a job scheduled through SQL can be picked up by any compatible
worker that has a handler for send_email:
select graphile_worker.add_job(
identifier => 'send_email',
payload => '{"to":"ada@example.com","subject":"Welcome"}'::json,
queue_name => 'mailers',
run_at => now(),
max_attempts => 25,
job_key => 'welcome:ada@example.com',
priority => 0,
flags => null,
job_key_mode => 'replace'
);
The matching Rust handler must use the same task identifier:
use graphile_worker::{IntoTaskHandlerResult, TaskHandler, WorkerContext};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct SendEmail {
to: String,
subject: String,
}
impl TaskHandler for SendEmail {
const IDENTIFIER: &'static str = "send_email";
async fn run(self, _ctx: WorkerContext) -> impl IntoTaskHandlerResult {
Ok::<(), String>(())
}
}
The job payload is stored as JSON. Keep payloads language-neutral: use stable field names, avoid Rust-only serialization assumptions, and make nullable or optional fields explicit when both Rust and Node workers may consume the same task.
Schema Selection
The default schema is graphile_worker, but Graphile Worker RS can be pointed at
another schema during worker setup:
let worker = graphile_worker::WorkerOptions::default()
.schema("graphile_worker")
.define_job::<SendEmail>()
.pg_pool(pg_pool)
.init()
.await?;
Use the same schema name when you want Rust and Node workers to share a queue. Use separate schemas when you want isolation, for example during a staged migration or when two applications have unrelated task identifiers.
Important Differences
Graphile Worker RS is mostly compatible with Node Graphile Worker, but it is not the same process model or API surface.
The README documents one runtime-level difference: Node Graphile Worker gives
each process its own worker_id; Graphile Worker RS uses one worker id and
processes jobs within the async runtime according to the configured concurrency.
The Rust crate exposes Rust-specific APIs such as WorkerOptions,
TaskHandler, WorkerUtils, runtime feature selection, typed job definitions,
and optional recovery configuration. Those are Rust conveniences around the
shared database model, not Node APIs.
Graphile Worker RS also adds recovery support in the current schema through
_private_workers and SQL functions such as worker_heartbeat,
worker_deregister, list_stale_workers, list_orphan_locked_workers,
recover_dead_worker_jobs, and delete_stale_workers. Recovery is opt-in in
the Rust worker configuration. If you run mixed Rust and Node workers, enable
and test recovery behavior deliberately so that every worker process in the
shared schema is compatible with your operational expectations.
Cron Calendar and Timestamp Selection
Node Graphile Worker matches cron schedules against UTC calendar fields,
including when useNodeTime selects application timestamps. In Rust, the
default use_local_time(false) also matches schedules and backfill in UTC.
Opting into use_local_time(true) selects both application timestamps and
the worker process's local calendar fields for cron matching and backfill.
The recorded timestamps still represent the scheduled instant.
These configuration names therefore do not imply identical cron behavior. Keep Rust's default when schedules must match Node's UTC calendar semantics; use local mode deliberately when schedules should follow the host timezone. Regression tests cover both modes across a date boundary for normal scheduling and backfill. See the cron guide.
Migration Behavior
Graphile Worker RS runs ordered migrations and records them in
graphile_worker.migrations. The migration set is idempotent: tests install a
fresh schema, add a job, rerun migrations multiple times, and verify that the job
remains present.
The migration code can also take over an existing schema that already has a
Graphile Worker-style migrations table. Tests cover a pre-existing migrations
table containing migration 1, then run the Rust migrations, add a job, and
rerun migrations without losing that job.
There are two compatibility checks to plan for:
- If the database contains a breaking migration id newer than this Rust crate knows about, migration aborts instead of downgrading or guessing. Newer non-breaking migration ids log a warning and continue.
- Migration 11 refuses to run while recent jobs are locked. Tests set
locked_atandlocked_byon an existing job and expect the migration to fail with a locked-job error.
For production coexistence, treat schema upgrades as a coordinated operation:
- Check that every Rust and Node worker version you plan to run supports the target database schema revision.
- Drain or stop workers before migrations that rewrite job tables or private structures.
- Confirm there are no active locks before crossing migration 11.
- Start one runtime first, schedule a simple job, and verify the other runtime can see or process jobs with the same task identifier before moving real traffic.
Migration Strategies
For gradual migration from Node to Rust, prefer one of these approaches:
- Shared schema: keep Node workers running, add Rust workers with handlers for a small set of task identifiers, and move task ownership incrementally.
- Separate schema: run Rust workers in a different schema while validating new task implementations, then move scheduling code to the shared schema when the payload and retry behavior are proven.
- SQL scheduling boundary: schedule jobs through
graphile_worker.add_joboradd_jobsfrom application code so the producer does not depend on either runtime's client API.
When sharing a schema, avoid registering different behavior for the same task identifier in both runtimes unless the handlers are intentionally equivalent. PostgreSQL stores the task identifier, not the language runtime that should own the job.
Related pages:
Revision 21 and Node 0.18
Rust and Node migration numbers are not interchangeable checkpoints. Rust revision 19 contains keyed-job concurrency safeguards, and Rust revision 20 adds worker recovery. Node revision 19 is a pool-locking compatibility marker; Node revision 20 avoids consuming existing task and queue identities.
Rust revision 21 installs that identity fix while retaining Rust's advisory key locks and missing-result error. It also creates recovery objects when upgrading from Node revision 20. Existing Rust recovery rows and applied migration ledger entries remain intact. Tests cover both revision-20 upgrade paths with locked jobs and repeat migration. The Node fixture uses its official revision-20 SQL on the compatible revision-18 schema; this is not a full mixed-worker runtime test.
Run the Rust migrator before enabling Rust recovery against a Node-created schema. Do not treat equal migration numbers as proof of identical migration history or rewrite existing ledger entries to make the numbers match.
Revision 22 and replaced jobs
Rust revision 22 adds a private _private_job_retirements table to distinguish a replaced
locked job from an ordinary job on its final attempt. Rust recovery returns both
jobs' locks, but only restores an attempt for the ordinary job. The replacement
retains its original job and queue locks until completion, failure or recovery,
preserving named-queue serialization. Explicit reschedule_jobs calls clear the
marker, allowing intentional administrative retries.
Existing rows are not retroactively classified: replacement events before this revision cannot be inferred from the old schema without confusing legitimate final attempts. Migration history and existing jobs are preserved. This Rust recovery extension is not a claim that Node has identical recovery semantics.
Node v0.18.0's src/sql/returnJobs.ts and older Rust return queries do not
consult this marker. Deploy the updated Rust return paths to obtain this
protection; sharing the new schema alone does not fix an older worker's local
queue return behavior. The shared recover_dead_worker_jobs SQL function does
honor the marker after migration 22.
The marker also records remove_job on a locked job and eligible
permanently_fail_jobs operations. Automatic recovery respects those decisions;
explicit reschedule_jobs remains available to retry them deliberately.