Runlane
Reference

Benchmarks

Local PostgreSQL and SQS measurements, a pg-boss comparison, and AWS deployment findings.

This page reports several different tests. Read each result within its listed setup and workload. Local polling, raw SQS transport, the full PostgreSQL/SQS lane, and an AWS deployment each answer different questions. Performance can change with the workload, configuration, and software version.

TestWhat it measuresWhat it does not measure
PostgreSQL pollingThe full Runlane lifecycle for prequeued no-op tasksEnqueue speed or application work
Local SQS transportSQS publish, receive, ordering, and settlement through MinistackAWS service capacity or network latency
Local PostgreSQL/SQS laneThe full lane through Postgres and MinistackAWS Lambda or RDS capacity
AWS deployment checkOne small example deployment and its admission limitsRunlane's maximum AWS throughput

PostgreSQL polling

These tests time prequeued tasks whose handlers do no work. The timer ends after Runlane saves the successful result. Durable observations were off, which is the default. Each task still kept its run and event history.

Processes × slots per processPayloadMeasured tasks per repetitionTasks/sSQL calls/taskWorker CPU ms/taskPostgreSQL CPU ms/taskExecution WAL bytes/task
1 × 1Small1,00063.447.00307.6505.69514,926
1 × 16Small4,000337.740.43832.2971.15313,981
1 × 64Small8,000651.950.21951.5631.03213,966
1 × 168 KiB1,000288.900.44103.0141.42113,736
8 × 16Small12,0002,459.800.43951.7420.64213,571

When tasks run at the same time, Runlane groups their storage work into bounded batches. This is why the rows with more concurrency use fewer SQL calls per task. At 1 × 1, there is no other work to share a batch.

PostgreSQL polling compared with pg-boss

The closest match used eight processes with 16 execution slots per process. In this local no-op test, Runlane reached 94.5% of the pg-boss harness's throughput.

EngineProcesses × slotsCompletions/sSQL calls/completionWorker CPU ms/completionPostgreSQL CPU ms/completionExecution WAL bytes/completion
Runlane8 × 162,459.800.43951.7420.64213,571
pg-boss 12.30.08 × 162,602.992.04300.5741.676879

Single-process results show a larger gap:

EngineProcesses × slotsCompletions/sSQL calls/completion
Runlane1 × 16337.740.4383
pg-boss 12.30.01 × 161,700.582.0160

Runlane ran its full task lifecycle. The pg-boss test used public fetch({ batchSize: 1 }) and complete() loops from pg-boss 12.30.0. Runlane saves more lifecycle history. The test compares the overhead of both engines, but they do not store the same data.

At 8 × 16, Runlane made fewer SQL calls and used less PostgreSQL CPU. It also used about three times the worker CPU and wrote 15 times the WAL. Worker CPU and write volume were the largest measured gaps.

Local SQS transport

A separate test measured the built SQS adapter without the Runlane lifecycle or Postgres. It used Ministack on the same host.

WorkloadTopologyMessages/s
Publish in native batches of 10One processAbout 5,917
Publish one message at a timeOne process, serialAbout 479
Short consume runOne process, 10 slots1,258
Short consume runOne process, 32 slots2,606
Longer consume runsOne process, 10–32 slots549–705
Longer consume runsFour processes, 8–10 slots each1,406–1,496
FIFO consume, one message groupOne process228
FIFO consume, 16 message groupsOne process1,811

The short consume runs used 2,000 messages. The longer runs used 10,000. The short rates fell when Ministack had to sustain the load, so they are burst results. All four-process runs completed without replay. Full native batches needed about 100 receive calls and 100 delete calls per 1,000 messages.

The publish result shows why batch fill matters: one ten-entry request carries ten messages instead of one. The FIFO result shows a different limit. One message group is serial by design, while independent groups can run at the same time.

These numbers show that the adapter uses SQS batching and ordering correctly. They do not predict AWS SQS throughput.

Local PostgreSQL with SQS

The full PostgreSQL/SQS lane was also tested with local Ministack. Ministack accepts the AWS SQS API, but it does not include AWS network latency, quotas, or service limits.

This test enabled a live observer and five durable observations per task. It used one process, 64 worker slots, and 1,000 prequeued no-op tasks. Unlike the polling test, its timer included worker startup and shutdown. The values below are medians of two runs. They describe the measured configuration, which differs from the default profile.

Tasks/sSQL calls/taskWorker CPU ms/taskReceives per 1,000 tasksDeletes per 1,000 tasks
205.263.91705.918137132

Runlane can receive and delete up to ten SQS messages in one request. This is why 1,000 tasks needed far fewer than 1,000 receive and delete requests. The receive count includes empty requests. Both measured runs completed every task once, with no transaction callback replays or SQL rollbacks.

Eager enqueue was measured on its own at 38.55 tasks/s and 14 SQL calls per task. This test used a live observer and one isolated trigger per task, so every publish contained one message. It did not measure concurrent publish batching. The SQL count is a measured result, not a fixed cost per enqueued task.

Local test environment

SettingValue
CPUIntel Xeon Gold 6132, 8 vCPUs
Memory16 GB
Node.js24.18.0
PostgreSQL16.14 on the same host
SQS serviceLocal Ministack
Connection poolMaximum 10 connections per process
Polling workloadPrequeued no-op handlers; Small and 8 KiB payload cases
PostgreSQL/SQS workloadPrequeued no-op handlers with small payloads
SQS transport workloadDirect publishes or preloaded messages without the Runlane lifecycle
Polling timerExecution only; enqueue, startup, warmup, verification, and shutdown excluded
PostgreSQL/SQS timerWorker startup through shutdown; enqueue measured separately

Each polling value is the median of two runs. The polling matrix completed all 52,000 measured tasks exactly once. It had no transaction callback replays, SQL rollbacks, or throttling.

These tests ran for tens of seconds on one shared host. They compare code paths under fixed conditions. They do not prove production capacity, latency, a service-level objective, an Aurora cost, or AWS SQS performance. SQL calls are not database I/O charges. WAL bytes are not Aurora billed I/O.

AWS deployment check

The AWS deployment used PostgreSQL 17 on db.t4g.micro, a maximum of 79 database connections, Lambda batch size 10, and one small Fargate worker. The cap-2 and cap-4 rows below were separate event-source settings; both used an application pool maximum of two. The load generator ran outside AWS. Producers, consumers, maintenance, and application queries shared the same database.

This setup reached deployment limits before the test found a Runlane or SQS limit. It does not prove that no later limit exists.

DeploymentDurable resultFirst limit observed
Lambda without an event-source concurrency cap, application pool max 10188 of 200 runs succeeded; 12 failed after retries51 concurrent Lambda invocations drove RDS to 77 of 79 connections
Lambda cap 2, application pool max 2640 of 640 runs succeeded, with no retries; median 14.57 completions/sNo failure in the measured run; RDS peaked at 27 connections
Lambda cap 4, application pool max 21,840 of 1,840 runs succeeded, with no retriesRDS peaked at 55 connections; throughput and latency became unstable under higher offered load
Bounded Fargate queue: concurrency 5 and maintenance once per minuteA 40-run warm-up took 489 seconds; handlers took about 0.5–0.7 seconds eachMaintenance admitted five new runs per minute

The Lambda workload read one application row with Prisma, waited 50 milliseconds to simulate an external provider, saved a durable step, and updated the row. It was not the no-op workload used in the local polling table.

The cap-2 rate is not a Runlane, Lambda, or SQS ceiling. It is the result of one small example deployment. A cap of two Lambda invocations can still run up to 20 records at once when both invocations receive full ten-record batches.

The failed uncapped run also had zero Lambda platform errors. The handler returned valid partial batch responses while some durable Runlane runs retried and later failed. Production checks must join AWS metrics with exact run states, attempts, retries, duplicates, and dead-letter queue state.

The Fargate result measures bounded admission. SQS was often empty while runs waited in Postgres for the next maintenance pass. It does not measure Fargate worker speed.

These checks do not establish sustained AWS capacity or cost per million tasks. A capacity test must measure these paths separately:

  1. Ingress with consumers stopped.
  2. Prequeued Lambda drain with producers stopped.
  3. Prequeued long-running worker drain with producers stopped.
  4. Continuous end-to-end traffic.
  5. Bounded queue refill and recovery.

The load generator must run in the same AWS region and send work at a fixed rate. The database must have stable, non-burstable compute for a capacity claim. Each case must account for every accepted run. See Deploy with Postgres and SQS for sizing formulas.

Metric definitions

MetricMeaning
Tasks/s or completions/sRuns that reached the measured success point each second
SQL calls/taskPostgreSQL requests made during the measured window, divided by completed tasks
Worker CPU ms/taskCPU time used by all measured worker processes, divided by completed tasks
PostgreSQL CPU ms/taskDatabase process CPU time during the measured window, divided by completed tasks
Execution WAL bytes/taskPostgreSQL write-ahead log growth during execution, divided by completed tasks

Small means the compact input used by the no-op benchmark. The 8 KiB case adds 8,192 bytes of deterministic text.

WAL measures database change volume. It is not the same as stored bytes or an Aurora I/O charge. SQL calls count client-server requests, which may contain work for many tasks when Runlane batches lifecycle changes.

Other adapters

Measure each storage and transport combination separately. Each system has different atomic operations, batch limits, network costs, and durability rules.

The storage and transport contracts remain the common boundary. Each adapter can use its native batch and atomic features while core keeps ownership of the task lifecycle. Every adapter needs its own single-process, multiprocess, payload, failure, and sustained-load results before making performance claims.

On this page