Background jobs on PostgreSQL in 2026: revisit River, Graphile Worker and Oban before you add Redis just for a job queue
Most web applications need background jobs. Send an SMS after a payment, build a PDF, call a slow partner API, recalculate a report at night. The usual reflex is to add Redis or RabbitMQ and a job library on top. That works, but it also adds a second system that has to agree with your database, and the place where the two disagree is exactly where the painful bugs live.
This post looks at three libraries that keep the job queue inside PostgreSQL itself: River for Go, Graphile Worker for Node.js and Oban for Elixir. Solid Queue from the Rails world and a plain SELECT ... FOR UPDATE SKIP LOCKED query are covered as context. The point is not that PostgreSQL beats every broker. The point is that for application jobs, when your data already lives in PostgreSQL, you may not need the broker at all, and you gain something a broker cannot give you: enqueueing the job in the same transaction as the business write.
If you are choosing a message broker for events between services, that is a different question, and our earlier post on RabbitMQ, NATS and Kafka queues covers it. This one is about job libraries that live inside your application.
A scenario to keep us honest
Take an invented Kolhapur company that builds milk collection and farmer payment software for dairy cooperatives in south Maharashtra. Village societies record milk twice a day, fat and SNF readings come from the collection centre machines, and farmers are paid on a fixed cycle. About 25 developers. The collection and payout core is in Go. The cooperative admin portal and its notification service are in Node.js. A small team built the live collection dashboard in Elixir with Phoenix. Everything stores its data in one managed PostgreSQL cluster, with PgBouncer in front for the web tier.
Background jobs today go through Redis with a Go job library. Two incidents in one payment cycle made the team rethink it:
- A payout batch transaction rolled back because of a validation error, but the “payment credited” SMS job had already gone into Redis. About 300 farmers got an SMS for money that never moved.
- On another day Redis restarted between the payout commit and the enqueue call. The payouts were saved, the SMS jobs were never created, and nobody noticed until farmers called the society secretary.
Both are the same bug: two systems, two writes, no shared transaction. The question is whether moving the jobs into PostgreSQL fixes it, and what it costs.
The short version
- The real win is the transactional enqueue. If the job row is inserted in the same transaction as the payout rows, the job exists if and only if the payout committed. Both Kolhapur incidents become impossible by design.
- River (Go, with newer clients for other languages) is the natural fit for the Go core. It is MPL-2.0, uses
SKIP LOCKEDto claim jobs andLISTEN/NOTIFYto wake workers quickly, and ships a separate open-source web UI. River Pro is a paid add-on. - Graphile Worker (Node.js) is MIT, very PostgreSQL-centric, and lets you enqueue from plain SQL with
graphile_worker.add_job(), even from triggers. It deletes jobs once they succeed. It has no official dashboard. Worker Pro is a paid, source-available add-on. - Oban (Elixir) is Apache 2.0, mature, keeps job history for inspection, and its web dashboard Oban Web has been open source since January 2025. Oban Pro is a paid add-on.
- Solid Queue is the Rails 8 default and works on PostgreSQL, but it polls rather than listens, and by default it recommends a separate queue database, partly to stop you relying on transactional enqueue.
- A real broker is still the right answer when you need very high sustained throughput, fan-out to many services, replayable event streams, or when the database is already your bottleneck.
For the Kolhapur team, a sensible answer is River for the Go payout core, Graphile Worker for the Node.js notification service, and Oban for the Elixir dashboard’s few jobs, all on the same PostgreSQL cluster, with Redis removed from the job path.
Where things stand on 26 October 2026
Versions and dates below are from GitHub release pages and package registries, checked on 26 October 2026. Times are in IST.
- River (Go) v0.49.0, released 5 October 2026 at 9:20 PM. The repository was created in November 2023; its two main contributors by commit count are Brandur Leach and Blake Gentry. The same repository now also tags other language clients: a Ruby client 0.13.0 (8 October 2026), a Rust client 0.3.0 (7 October 2026), a JavaScript client published on npm as 0.2.0 (7 October 2026) and a Java client as 0.1.0-alpha.1 (25 October 2026). The README says jobs inserted in one language can be worked in another. Licence: MPL-2.0 (checked in the LICENSE file).
- River UI v0.22.0, released 25 October 2026 at 5:35 AM. Licence: MPL-2.0.
- Graphile Worker v0.18.0, released 8 September 2026 at 9:35 PM. It is now published as pure ESM and needs Node.js 22.18 or later and PostgreSQL 12 or later. The repository dates from March 2019 and is maintained by Benjie Gillam’s Graphile. The project still uses 0.x numbering; its project status page calls it production ready and warns that minor versions may need small code changes. Licence: MIT.
- Oban 2.24.2 on Hex, published 14 October 2026 at 1:11 PM. The latest GitHub release entry is v2.24.0 from 26 August 2026 at 12:39 AM. Oban dates from February 2019, needs PostgreSQL 14 or later (it also has MySQL and SQLite engines), and is Apache 2.0. Version 2.24 renamed “plugins” such as the pruner and lifeline into top-level services and backported per-entry cron time zones from Pro. A Python sibling, oban-py, reached v0.7.0 on 22 October 2026.
- Oban Web 2.13.1 on Hex, published 8 October 2026 at 7:58 PM. Apache 2.0 since v2.11.0 on 16 January 2025; earlier versions were commercial.
- Solid Queue v1.7.0, released 21 August 2026 at 5:07 PM. MIT, maintained under the Rails organisation, configured by default in new Rails 8 applications.
All four are actively maintained. None of them is a weekend project you need to worry about.
The first question: where does your data live
Before comparing features, check one thing. Are the rows that a job depends on in the same PostgreSQL database that would hold the job?
- If yes, a PostgreSQL job library gives you the transactional enqueue for free. This is the strongest reason to use one.
- If your data is spread over several databases or services, the job table can only share a transaction with one of them. You still save a system, but the big win shrinks.
- If you already run a broker for events between services, keep it for that. Job libraries are not a replacement for an event backbone.
What each tool is
Plain SELECT … FOR UPDATE SKIP LOCKED
PostgreSQL added SKIP LOCKED in version 9.5. With it, a query that locks rows simply skips rows another transaction has already locked, instead of waiting. The PostgreSQL documentation is clear that this gives an inconsistent view of the data and is not meant for general queries, but that it suits queue-like tables with many consumers. Every library in this post uses it, or something close to it, to let many workers claim different jobs without blocking each other.
You can build a small queue yourself with one table and this clause. Many teams do. What you then have to write yourself is everything around it: retries with backoff, rescuing jobs from crashed workers, cleaning old rows, scheduling, uniqueness, cron, shutdown and a way to see what is going on. That list is the reason the libraries exist.
River
River is a Go library. You define job args as a struct with a Kind(), a worker with a Work() method, and start a client with a pgx pool. Inserting in a transaction is one call, InsertTx. River’s README states the guarantee plainly: a job is enqueued if its transaction commits, removed if it rolls back, and not visible to workers until commit. It also offers transactional job completion, where marking the job complete commits together with the worker’s own database changes.
Maintenance work (cleaning old jobs, rescuing stuck ones, scheduling, reindexing) runs on one elected leader client. River UI is a separate binary or container, or an embeddable Go handler, that reads the River tables.
Graphile Worker
Graphile Worker runs tasks in Node.js and stores jobs in its own graphile_worker schema. Its stated aim is to keep infrastructure simple for small teams. The distinctive part is that adding a job is a SQL function, graphile_worker.add_job(), so you can enqueue from any SQL client, a stored procedure or a trigger, not only from JavaScript. Jobs that succeed are deleted; jobs that fail permanently stay for you to inspect. A stable graphile_worker.jobs view exists since 0.16, with a warning not to read it often because reads can slow the workers.
Oban
Oban is the oldest of the three in production use and the most feature-rich in its open-source form. Jobs live in an oban_jobs table, each job runs in its own Elixir process, queues have their own concurrency limits, and queues can be paused, resumed and scaled at runtime across nodes. Unlike Graphile Worker, Oban keeps completed jobs for a while for metrics and inspection, and its pruner removes them later. Oban Web is a Phoenix LiveView dashboard you mount inside your own app.
Solid Queue, for context
Solid Queue is the Active Job backend that ships with Rails 8. It works on PostgreSQL, MySQL and SQLite, uses FOR UPDATE SKIP LOCKED where available, and polls: workers default to a 0.1 second polling interval and dispatchers to 1 second. Retries come from Active Job, not from Solid Queue itself. Its README is unusually honest about transactional enqueue: it calls it a powerful but sharp tool, and by default configures a separate queue database so that your code does not quietly depend on it. Rails 8 also offers enqueue_after_transaction_commit, which delays the enqueue until the commit instead of joining the transaction. That fixes the rolled-back SMS case, but not the “committed, then crashed before enqueue” case.
Side by side, without a table
Licence and paid tiers
- River: MPL-2.0 core and UI. River Pro adds workflows, batching, sequences, concurrency limits, a dead letter queue, durable periodic jobs, encrypted jobs and ephemeral jobs. Its pricing page shows a paid tier at 125 US dollars with monthly or annual subscriptions, and a custom tier through sales.
- Graphile Worker: MIT. Worker Pro is proprietary but source available, and adds live migration (no need to scale to zero for upgrades) and crashed worker recovery. Its docs list it at 100 US dollars a month, included for sponsors at that level, with a cheaper early access offer for sponsors. The docs also say plainly that Pro is not needed for production.
- Oban: Apache 2.0, and Oban Web is Apache 2.0 too. Oban Pro adds workflows, global concurrency and rate limits, queue partitioning, batches and more. The pricing page lists 150 US dollars a month billed monthly or 135 US dollars a month billed yearly, per application, with unlimited nodes, and no free trial.
- Solid Queue: MIT, no paid tier. The Rails team points to Mission Control Jobs for a dashboard.
Prices are as shown on the vendors’ pages on 26 October 2026 and will change; check before you budget.
How workers find jobs
- River: claims jobs with
FOR UPDATE SKIP LOCKED, wakes onLISTEN/NOTIFY, and polls every second by default as a fallback. APollOnlymode skipsLISTENentirely, at the cost of slower pickup. - Graphile Worker:
SKIP LOCKEDto fetch,LISTEN/NOTIFYfor low latency on new jobs, and polling every 2 seconds by default for jobs scheduled in the future, since nothing sends a notification when time passes. Each concurrent slot asks for the next job as soon as it finishes the previous one. - Oban: the basic engine uses
FOR UPDATE SKIP LOCKED. Insert notifications go through a notifier, which by default is PostgreSQLLISTEN/NOTIFY, and a stager moves scheduled jobs to available on an interval. - Solid Queue: polling only, with separate tables for ready and scheduled executions.
Retries and backoff
- River: 25 attempts by default, backoff of attempt to the power four seconds with about 10 percent jitter, so the last retry lands roughly three weeks after the first run. Per-worker overrides are possible.
- Graphile Worker: 25 attempts by default, delay of
exp(least(10, attempt))seconds, which caps at about 6 hours between attempts; the docs describe it as about three days in total. - Oban: 20 attempts by default, exponential backoff with 15 seconds of padding and jitter; the docs say the 20th attempt comes about 13 days and 8 hours after the first. A
backoff/1callback lets you change it, even based on the error. - Solid Queue: whatever you configure with Active Job’s
retry_on.
Uniqueness and deduplication
- River: unique by args (or a tagged subset of fields), period, queue and state, enforced by a partial unique index. Insert results tell you when a duplicate was skipped.
- Graphile Worker: a
job_keythat replaces or updates a pending job, with modesreplace,preserve_run_atandunsafe_dedupe. Useful for debouncing and throttling. Its docs list caveats, including a race under very high contention on one key whereadd_jobcan return null. - Oban: unique by fields such as args, queue, worker and meta, with a period and a set of states.
- All three run jobs at least once. Unique insert does not mean exactly-once execution, so the job itself must be safe to repeat. Our post on idempotency keys goes deeper.
Scheduling, cron and time zones
- River: scheduled jobs and periodic jobs run by the leader. Durable periodic jobs, with next run times stored in the database, are a Pro feature.
- Graphile Worker: a crontab file with optional backfill of missed runs. Schedules are in UTC only, which matters for an Indian team: 6:00 AM IST is
30 0 * * *. - Oban: a cron service with a configurable time zone, and since 2.24, a time zone per crontab entry.
Priorities and queues
- River: named queues with their own worker counts; priority 1 (highest) to 4 (lowest), default 1.
- Graphile Worker: jobs run in parallel by default; jobs sharing a
queue_namerun one at a time. Priority is an integer where smaller runs first, default 0. The docs warn against high-cardinality queue names. - Oban: isolated queues with their own concurrency; priority 0 (highest) to 9 (lowest).
Rescuing stuck jobs
- River: the rescuer treats a running job as stuck after one hour by default (or longer if you set a longer job timeout) and retries or discards it. The docs warn this is effectively an upper bound on job duration, because a still-running job may be started again.
- Graphile Worker: after a hard crash, jobs stay locked for at least 4 hours; workers sweep every 8 to 10 minutes and release jobs locked longer than that. Worker Pro adds faster crashed worker recovery.
- Oban: the lifeline service rescues jobs still executing after 1 hour by default, purely by time, and Oban’s docs note this can cause duplicate execution of a job that is genuinely still running.
Observability
- River: River UI, open source, as a binary, container or embedded handler; subscriptions for metrics hooks.
- Graphile Worker: events you can hook into for logging and metrics, the
jobsview, and admin functions; no official UI. - Oban: Oban Web (open source) plus Telemetry events for every job lifecycle step.
How a PostgreSQL job queue actually works
It helps to see the mechanism once, because every operational issue later comes from it.
- Enqueue. Your code inserts a row into the job table, ideally inside the same transaction as the business change. Nothing is visible to workers until commit.
- Wake up. On commit, a
NOTIFY(if used) tells listening workers that new work exists. PostgreSQL delivers notifications only when the sending transaction commits, so a rolled-back job never wakes anyone. WithoutLISTEN, workers find the job on their next poll. - Claim. A worker runs an
UPDATE ... WHERE id IN (SELECT ... FOR UPDATE SKIP LOCKED LIMIT n)style query, marks the rows as running with its own identity, and commits quickly. The job is not worked inside that claiming transaction. - Work. The worker runs your code outside the claim transaction.
- Finish. It marks the job completed, deletes it, or schedules a retry with backoff.
- Maintain. Background services clean finished rows, move scheduled jobs to available, enqueue cron jobs and rescue jobs whose worker vanished.
Step 3 is where hand-written queues often go wrong. Holding the FOR UPDATE lock for the whole job looks simpler, but it keeps a transaction and a connection open for as long as the job runs, which hurts both connection limits and vacuum.
A worked plan for the Kolhapur team
Step 1: understand the bare pattern once
Even if you will use a library, write the plain version on a scratch database so the team understands what the library does:
BEGIN;
INSERT INTO payout (id, farmer_id, amount_paise, cycle_id)
VALUES (981, 4411, 125000, 77);
INSERT INTO job (kind, args, run_at)
VALUES ('payout_sms', '{"payout_id": 981}', now());
COMMIT;
-- a worker claims up to 10 jobs and commits at once
UPDATE job SET state = 'running', locked_by = 'worker-a', locked_at = now()
WHERE id IN (
SELECT id FROM job
WHERE state = 'available' AND run_at <= now()
ORDER BY id
FOR UPDATE SKIP LOCKED
LIMIT 10
)
RETURNING id, kind, args;
If the payout insert fails, the job row disappears with it. That alone removes the first incident. Then list what is missing: retries, rescue, cleanup, cron, a dashboard. That list is why Step 2 uses a library.
Step 2: River in the Go payout core
tx, err := dbPool.Begin(ctx)
if err != nil {
return err
}
defer tx.Rollback(ctx)
if err := savePayoutBatch(ctx, tx, batch); err != nil {
return err
}
for _, p := range batch.Payouts {
if _, err := riverClient.InsertTx(ctx, tx, PayoutSMSArgs{PayoutID: p.ID}, nil); err != nil {
return err
}
}
return tx.Commit(ctx)
For large batches, River’s batch insert (InsertManyTx) uses COPY FROM. Give PayoutSMSArgs unique options by PayoutID so a retried batch handler does not queue a second SMS for the same payout. Note the River 0.49.0 change: transactional helpers now reuse your transaction instead of creating savepoints, so roll back your transaction on any River error.
Step 3: Graphile Worker in the Node.js notification service
The admin portal writes a cycle closing record and queues the society report in one transaction, using plain SQL:
const client = await pool.connect();
try {
await client.query("BEGIN");
const { rows } = await client.query(
"INSERT INTO cycle_close (society_id, cycle_id) VALUES ($1, $2) RETURNING id",
[societyId, cycleId]
);
await client.query(
"SELECT graphile_worker.add_job('society_report', json_build_object('closeId', $1::int), job_key := $2)",
[rows[0].id, `society_report:${rows[0].id}`]
);
await client.query("COMMIT");
} catch (e) {
await client.query("ROLLBACK");
throw e;
} finally {
client.release();
}
The morning reminder to societies runs from the crontab file. Remember UTC: 30 0 * * * collection_reminder fires at 6:00 AM IST.
Step 4: Oban in the Elixir dashboard
The dashboard team has only a few jobs, such as rebuilding a centre’s daily summary after a correction. With Ecto.Multi, the correction and the job commit together:
Ecto.Multi.new()
|> Ecto.Multi.update(:reading, correction_changeset)
|> Oban.insert(:summary_job, fn %{reading: r} ->
CentreSummaryWorker.new(%{centre_id: r.centre_id, date: r.date})
end)
|> Repo.transaction()
In configuration, set the cron time zone to Asia/Kolkata, keep the pruner and lifeline on, and set rescue_after above the longest job you expect.
Step 5: plan connections before the cut-over
The web tier talks to PgBouncer in transaction mode. LISTEN does not work through transaction pooling, because the server connection is handed to someone else after each transaction. Our PgBouncer and alternatives post explains why.
- Enqueueing through PgBouncer in transaction mode is fine for all three, since an insert is just a transaction. River documents insert-only clients for exactly this.
- River’s working client should use a direct or session-pooled connection. River’s docs suggest a few coordinators with high worker counts, rather than many small ones.
PollOnlyexists if you must go through transaction pooling, but the docs ask you to verify the full setup. - Graphile Worker’s FAQ says pgBouncer may cause problems unless it runs in session (connection) mode, and that there is currently no polling-only option. Point its workers directly at PostgreSQL. Each worker process has its own pool, 10 connections by default.
- Oban’s PostgreSQL notifier does not work through PgBouncer in transaction or statement mode; its docs suggest the
Oban.Notifiers.PGnotifier, which uses distributed Erlang, instead. - Add up the direct connections: every worker process, every leader, every dashboard. Keep that number well below
max_connections.
Step 6: what changes next payment cycle
A failed payout batch now takes its SMS jobs down with it. A committed batch always has its SMS jobs, because they were in the same commit. Redis can be removed from the job path. The team still has to make the SMS job idempotent, because any of these libraries can run a job twice after a crash.
Table bloat, vacuum and long transactions
A job table is the busiest table in the database for its size: every job is inserted, updated a few times and then deleted. In PostgreSQL, updates and deletes leave dead row versions that only vacuum can reclaim.
- Keep the table small. River’s cleaner deletes completed and cancelled jobs after 24 hours and discarded jobs after 7 days by default. Oban’s pruner keeps jobs for 60 seconds by default. Graphile Worker deletes on success and asks you to clean permanently failed jobs.
- Let autovacuum keep up. For a high-churn job table, per-table settings such as a lower
autovacuum_vacuum_scale_factormake vacuum run sooner. Watchn_dead_tupinpg_stat_user_tablesfor the job table. - Indexes bloat too. River and Oban both include a reindexer that rebuilds key indexes with
REINDEX CONCURRENTLY; River’s runs daily at midnight UTC by default, which is 5:30 AM IST. - Long transactions are the hidden enemy. Vacuum cannot remove dead rows that an old open transaction might still see. One forgotten reporting session or a stuck analytics query can make the job table grow and every fetch slow down. Set
idle_in_transaction_session_timeoutand watch for old transactions.
Operational costs people forget
- Database load. Jobs now add writes, WAL and vacuum work to your primary. Size it for that, and watch replication lag if you stream to replicas.
- Backups include jobs. Oban lists this as an advantage. It is, but a restore also brings back pending jobs, which might send old SMS messages again unless they are idempotent.
- Migrations. Each library ships schema migrations. Graphile Worker 0.16 and later shut down old workers when a newer version migrates the schema, so plan deploys; Worker Pro’s live migration addresses this.
- Mixed versions across services. With three libraries you have three schemas and three upgrade paths. That is acceptable, but write down who owns each.
- People time. Someone has to watch the dashboard, the discarded jobs and the table size. A broker would need the same attention.
When a real broker is still the right answer
- Sustained very high throughput. Graphile Worker’s own scaling page reports its benchmark at over 180,000 trivial jobs per second, which is the vendor’s own run on its own hardware, and the same page suggests thinking about another queue around 5,000 jobs per second sustained. Your limit will be lower if the job table shares the database with heavy business traffic.
- Fan-out and many consumers. If one event must reach many independent services, use a broker or a log. A job row is meant to be worked once.
- Replay and long retention. Kafka-style replay of history is not what job tables do.
- The database is already the bottleneck. Moving jobs into it makes that worse.
- Data not in PostgreSQL. Without a shared transaction, the main benefit is gone.
If you still need to publish events to a broker reliably, the transactional outbox pattern is the related idea: write the event in the same transaction, relay it later. It is a topic of its own and we will come back to it separately. And if your jobs are turning into multi-step processes with waits and compensations, read our post on durable execution with Temporal, Restate and DBOS before building workflows out of chained jobs.
Decision guide
- Go service, data in PostgreSQL: River.
- Node.js service, or you want to enqueue from SQL, triggers or stored procedures: Graphile Worker.
- Elixir application: Oban; it is the default choice in that community for good reason.
- Rails 8 application: Solid Queue is already there. Decide on purpose whether it shares your main database.
- Several languages sharing one queue: River’s cross-language clients are worth a look, but the non-Go clients are young; test them first.
- You need workflows, global rate limits or partitioned concurrency: compare River Pro and Oban Pro honestly against building it or using a durable execution engine.
- Throughput in the thousands of jobs per second, or many consuming services: a broker.
A short checklist
- Jobs enqueued in the same transaction as the business write.
- Every job idempotent, because delivery is at least once.
- Unique options or job keys for jobs that must not be duplicated.
- Retry counts and backoff reviewed per job kind, not left at defaults blindly.
- Rescue timeout longer than your longest job.
- Cron schedules checked for time zone, especially UTC-only crontabs.
- Worker connections direct or session pooled; enqueue connections can use transaction pooling.
- Cleaner or pruner on, autovacuum watched, long transactions limited.
- A dashboard or alert on discarded jobs and queue depth.
Common mistakes
- Doing the work inside the claim transaction. It ties up a connection and blocks vacuum for the whole job.
- Enqueueing on a different connection. If the job insert does not use the same transaction object as the business write, you have recreated the dual-write problem inside one database.
- Running workers through PgBouncer transaction mode and wondering why jobs are picked up late or not at all.
- Assuming unique means exactly once. It only means one row.
- Letting discarded jobs pile up forever. Graphile Worker’s docs specifically ask you to clear permanently failed jobs.
- Forgetting the rescuer’s side effect. A long job that outlives the rescue timeout can run twice at the same time.
About a hands-on drill
No hands-on drill was run for this post. The code and SQL snippets above are written from each project’s documentation and are meant as starting points; try them on a scratch database before using them in production.
Unlearn, re-learn
Unlearn the idea that background jobs need their own infrastructure from day one. For many applications, the database you already back up, monitor and understand can carry the job queue too, and it gives you a guarantee no separate broker can: the job and the data commit together or not at all. Re-learn the costs that come with it: a busy table to keep small, vacuum to respect, connections to plan, and jobs that must be safe to run twice. Add a broker when your traffic or your architecture truly needs one, not because it is the habit.
Sources
- River repository and README, releases and changelog
- River docs: transactional enqueueing, job retries, unique jobs, maintenance services, PgBouncer
- River UI and River Pro
- Graphile Worker repository and releases
- Graphile Worker docs: introduction, exponential backoff, job key, crontab, error handling, scaling, FAQ, Worker Pro
- Oban repository, changelog and Hex package
- Oban docs: Oban.Worker, Oban.Lifeline, Oban.Pruner, Oban.Notifiers.Postgres
- Oban Web repository, Oban Web v2.11 changelog and Oban Pro pricing
- Solid Queue README and releases
- PostgreSQL docs: SELECT locking clause, NOTIFY, routine vacuuming
- PgBouncer features and pooling modes
Releted Posts
SQLite in production in 2026: revisit Litestream, rqlite and LiteFS before you add another database server for a small app
Picture a small software company in Madurai that builds a job card app for about 60 two-wheeler service centres across southern Tamil Nadu (an invented example, not a real client).
Read moreIn-process analytics in 2026: revisit DuckDB, Polars and chDB before you spin up another warehouse for a small question
Think of a knitwear exporter in Tiruppur that makes T-shirts, polos and kids’ wear for buyers in Europe, the US, the Gulf and Japan (an invented example, not a real client).
Read moreTime series databases in 2026: revisit TimescaleDB, QuestDB and InfluxDB 3 Core before you store another sensor reading
Take a cold-chain company in Nashik that runs pre-cooling and cold storage chambers for export grapes and onions at four sites: MIDC Ambad, Pimpalgaon Baswant, Dindori and Niphad (an invented example, not a real client).
Read more