Transactional enqueue
Save the row and queue the work — in one COMMIT. Kills the dual-write problem outright; the one thing no external queue or broker can do.
Every twelve seconds, worker-2 is kill -9’d mid-message.
Watch what doesn’t happen. Then run the attacks yourself →
You don’t need a message platform today. You need to send a welcome email after signup, process a Stripe webhook, resize an upload — reliably. That usually means adding Redis or SQS to your stack before lunch. With Vulkan it means adding nothing:
queue := vulkan.Queue[WelcomeEmail](client, "emails")
// In your signup handler — same transaction as the user INSERT,// so the email job exists if and only if the user does:client.Tx(ctx, func(tx vulkan.Tx) error { if err := users.Insert(ctx, tx, user); err != nil { return err } return queue.EnqueueTx(ctx, tx, &WelcomeEmail{UserID: user.ID})})
// Anywhere in your app (or a separate binary) — the worker:queue.Work(ctx, func(ctx context.Context, job vulkan.Message[WelcomeEmail]) error { return mailer.SendWelcome(ctx, job.Payload)}, vulkan.WithRetries(5), vulkan.WithBackoff(vulkan.Exponential(time.Second)))That’s the whole setup. One library, one migration, your existing
DATABASE_URL. Retries, exponential backoff, and a dead-letter queue you can
SELECT — and because the job is enqueued in your transaction, the email
can never fire for a signup that rolled back, and never get lost for one that
committed. That last guarantee is structurally impossible with Redis, SQS, or
any other external queue. Why →
Here’s the thing about every async tool you could pick today: it’s a fork in the road you can’t see yet. The job queue can’t fan out. The queue service can’t replay. Eighteen months in, each new need means bolting on another system. Vulkan’s bet: a queue is just a stream with one consumer group — so growing never means migrating.
Day 1 — background jobs. The code above. One queue, one worker, retries and dead letters included. As capable as any job queue, with one guarantee none of them have.
Month 3 — a second consumer. Product wants an in-app activity feed fed by the same signups. With a job queue, the message is deleted the moment the email worker finishes — there’s nothing left to consume. In Vulkan, your queue was a durable stream all along:
vulkan.Subscribe(client, "emails", "activity-feed", recordSignup)One line. Independent cursor, independent retries, independent dead letters. The email worker never notices.
Month 9 — a new service needs history. You’re building search. Point it at the beginning of retained history and it bootstraps itself — no hand-written backfill, no consistency scramble:
vulkan.Subscribe(client, "orders", "search-indexer", indexOrder, vulkan.FromOffset(0))Year 1 — routing and ordering. Multiple event types, regional consumers, a per-account ordering requirement. Routing keys, bindings, and per-key FIFO are already in the table:
stream.Publish(ctx, event, vulkan.WithRoutingKey("orders.eu.created"), vulkan.WithPartitionKey(order.AccountID))
vulkan.Bind(client, "eu-compliance", "orders.eu.>")Same table the whole way. Each step is additive — a line of code, not a migration project, not a second system, not a third. This is the point where other tools hand you a Kafka cluster and a sympathetic look.
Transactional enqueue
Save the row and queue the work — in one COMMIT. Kills the dual-write problem outright; the one thing no external queue or broker can do.
Per-message lifecycle
Retries with exponential backoff, leases that survive dead workers, and
a dead-letter queue you can inspect with SELECT.
Retention & replay
Messages are facts, not ephemeral work items. New consumers can rebuild from history — the superpower that usually costs you a Kafka cluster.
Fan-out without ceremony
Any number of consumer groups on one stream, each at its own pace with its own retries and dead letters. Adding one is a line, not a topology.
Routing
Publish with a routing key; bindings decide who receives —
orders.*.created wildcards and header matching, stored as rows.
FIFO where you need it
Per-key ordering on demand: same key processes in order, everything else runs fully parallel. Pay for ordering only where you opt in.
The engine is open source — run it against any Postgres: yours, RDS, Neon, Railway, Supabase. And because the entire state is rows, your messaging layer answers questions like a database — because it is one:
-- 2am, something's off. "What happened to order ord_8431?" is a-- query, not an archaeology dig through a broker console:SELECT d.consumer_group, d.status, d.attempts, d.last_errorFROM vulkan.events eJOIN vulkan.deliveries d ON d.event_offset = e."offset"WHERE e.payload->>'order_id' = 'ord_8431';Vulkan Cloud is those same queries as a product — a live dashboard, dead-letter triage with one-click redrive, replay dry-runs, and alerts that know queues:
Start in five minutes
One migration, one import, your existing DATABASE_URL.
Quickstart →
Skeptical? Good.
See how Vulkan stacks up against job queues, Kafka, and RabbitMQ & SQS.