Fast and reliable
background jobs.
Enqueue jobs transactionally in your existing database. No added services, built to scale. Get durable workflows, sequences, and global concurrency limits with River Pro.
Get started in Go Ruby Rust TypeScript
Runs on Postgres and SQLite
Transactional enqueueing simplifies everything.
River jobs never run before your transaction completes, and are never lost. If your API's transaction succeeds, your job will be enqueued—period.
Compared to working with external job queues like Redis, this means a simpler development model that lets you build more reliable applications more quickly. For applications built on Postgres or SQLite, this also means no added services to manage.
func createUser(ctx context.Context, email string) (*User, error) {
tx, err := dbPool.Begin(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback(ctx)
// Create the new user record:
user, err := insertUser(ctx, tx, email)
if err != nil {
return nil, err
}
// Enqueue a job to send the user an onboarding email:
if _, err = riverClient.InsertTx(ctx, tx,
SendEmailArgs{UserID: user.ID},
nil,
); err != nil {
return nil, err
}
// Atomically commit both the user record and the job:
if err := tx.Commit(ctx); err != nil {
return nil, err
}
return user, nil
} def create_user(email)
ApplicationRecord.transaction do
# Create the new user record:
user = User.create!(email: email)
# Enqueue on the same Active Record connection:
river_client.insert River::JobArgsHash.new(
:send_email, user_id: user.id
)
# The user and job commit together when the block returns.
user
end
end async fn create_user(
pool: &riverqueue::sqlx::PgPool,
client: &riverqueue::Client,
email: &str,
) -> Result<User, riverqueue::BoxError> {
let mut tx = riverqueue::database::begin_postgres(pool).await?;
// Create the new user record:
let user = insert_user(&mut tx, email).await?;
// Enqueue in the same transaction:
client.insert(SendEmailArgs { user_id: user.id })
.tx(&mut tx)
.await?;
// Atomically commit both the user record and the job:
tx.commit().await?;
Ok(user)
} async function createUser(email: string) {
const tx = await pool.connect();
try {
await tx.query("BEGIN");
// Create the new user record:
const user = await insertUser(tx, email);
// Enqueue in the same transaction:
await riverClient.insert(sendEmail, { user_id: user.id }, { tx });
// Atomically commit both the user record and the job:
await tx.query("COMMIT");
return user;
} catch (error) {
await tx.query("ROLLBACK");
throw error;
} finally {
tx.release();
}
} Batteries included
The features you need.
River is fully-featured so you can focus on building your app instead of chasing down bugs and distributed systems edge cases.
A better workflow with River Pro
River Pro extends River with time-saving features and commercial support. Available through a paid subscription.
Workflows
Break down complex workflows into discrete tasks, including fan-out and fan-in execution.
Concurrency limits
Globally limit concurrent jobs with granular partitioning. Scale your workloads with precision and confidence.
- Job 1
- Job 4
- Job 7
- Job 10
- Job 13
- Job 16
- Job 19
- Job 2
- Job 5
- Job 8
- Job 11
- Job 14
- Job 17
- Job 20
- Job 3
- Job 6
- Job 9
- Job 12
- Job 15
- Job 18
- Job 21
Sequences
Run sequences of jobs in strict order, each running independently in parallel.
No limits
Self-hosted and scalable
No new external services—just use your existing database. No per-workflow pricing.
Keep it going
Support River's development
Help ensure the sustainable maintenance and development of River.
Thoughtful design.
Modern conventions.
River is designed by experienced developers to follow the best known conventions in all supported languages. Use Go, Ruby, Rust, TypeScript, or all at the same time.
// Each worker has a corresponding args type:
type SendEmailArgs struct {
UserID int64 `json:"user_id"`
}
func (SendEmailArgs) Kind() string { return "send_email" }
type SendEmailWorker struct {
river.WorkerDefaults[SendEmailArgs]
}
// Work receives a typed *Job[T] to process:
func (w *SendEmailWorker) Work(ctx context.Context, j *river.Job[SendEmailArgs]) error {
user, err := getUser(ctx, j.Args.UserID)
if err != nil {
return err
}
return sendOnboardingEmail(user.Email)
} # Workers are plain Ruby classes, with no required superclass:
class SendEmailWorker
def self.kind = "send_email"
def work(job)
# Arguments arrive as a hash with string keys:
user = User.find(job.args.fetch("user_id"))
send_onboarding_email(user.email)
end
end
workers = River::Workers.new.add(SendEmailWorker) use riverqueue::{BoxError, Job, JobArgs, WorkContext, WorkOutcome};
use serde::{Deserialize, Serialize};
// Derive job arguments and JSON serialization:
#[derive(Debug, Deserialize, JobArgs, Serialize)]
#[river(kind = "send_email")]
struct SendEmailArgs {
user_id: i64,
}
// An async function receives a strongly typed job:
async fn send_email(
_context: WorkContext,
job: Job<SendEmailArgs>,
) -> Result<WorkOutcome, BoxError> {
let user = get_user(job.args.user_id).await?;
send_onboarding_email(&user.email).await?;
Ok(WorkOutcome::Complete)
} import { defineJob, Workers } from "riverqueue";
import { z } from "zod";
const sendEmail = defineJob({
kind: "send_email",
schema: z.object({ user_id: z.number().int() }),
});
// The handler receives validated, typed arguments.
const workers = new Workers().add(sendEmail, async ({ job }) => {
const user = await getUser(job.args.user_id);
await sendOnboardingEmail(user.email);
}); Built for humans
Easy to operate and develop.
River ships with an optional self-hosted web interface called River UI. Whether you're trying to diagnose an ongoing incident, perusing for details on erroring jobs, or merely trying to see whether your jobs are working correctly in development, a visual interface makes life easier.
- Gain insights.
- Tune your system by keeping an eye on queue depth and throughput.
- Control jobs and queues.
- Cancel, retry, or delete jobs. Pause entire queues to halt job processing, or adjust concurrency limits (Pro only).
- Find bugs.
- Discover slow, stuck, or erroring jobs. Uncover hidden production issues.
- Faster development.
- See whether your jobs are working as you write them. Quickly iterate your way to a working system.
- Open source, self-hosted.
- Keep your data on your systems. Lightweight and stateless, with no added service dependencies.
- Dark mode.
- Because nobody wants to use a bright white website when responding to an outage at 2am.
Stay updated.
We're hard at work on more advanced features including rate limiting, and concurrency controls. Sign up to get updates on our progress.