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.
| Test | What it measures | What it does not measure |
|---|---|---|
| PostgreSQL polling | The full Runlane lifecycle for prequeued no-op tasks | Enqueue speed or application work |
| Local SQS transport | SQS publish, receive, ordering, and settlement through Ministack | AWS service capacity or network latency |
| Local PostgreSQL/SQS lane | The full lane through Postgres and Ministack | AWS Lambda or RDS capacity |
| AWS deployment check | One small example deployment and its admission limits | Runlane'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 process | Payload | Measured tasks per repetition | Tasks/s | SQL calls/task | Worker CPU ms/task | PostgreSQL CPU ms/task | Execution WAL bytes/task |
|---|---|---|---|---|---|---|---|
| 1 × 1 | Small | 1,000 | 63.44 | 7.0030 | 7.650 | 5.695 | 14,926 |
| 1 × 16 | Small | 4,000 | 337.74 | 0.4383 | 2.297 | 1.153 | 13,981 |
| 1 × 64 | Small | 8,000 | 651.95 | 0.2195 | 1.563 | 1.032 | 13,966 |
| 1 × 16 | 8 KiB | 1,000 | 288.90 | 0.4410 | 3.014 | 1.421 | 13,736 |
| 8 × 16 | Small | 12,000 | 2,459.80 | 0.4395 | 1.742 | 0.642 | 13,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.
| Engine | Processes × slots | Completions/s | SQL calls/completion | Worker CPU ms/completion | PostgreSQL CPU ms/completion | Execution WAL bytes/completion |
|---|---|---|---|---|---|---|
| Runlane | 8 × 16 | 2,459.80 | 0.4395 | 1.742 | 0.642 | 13,571 |
| pg-boss 12.30.0 | 8 × 16 | 2,602.99 | 2.0430 | 0.574 | 1.676 | 879 |
Single-process results show a larger gap:
| Engine | Processes × slots | Completions/s | SQL calls/completion |
|---|---|---|---|
| Runlane | 1 × 16 | 337.74 | 0.4383 |
| pg-boss 12.30.0 | 1 × 16 | 1,700.58 | 2.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.
| Workload | Topology | Messages/s |
|---|---|---|
| Publish in native batches of 10 | One process | About 5,917 |
| Publish one message at a time | One process, serial | About 479 |
| Short consume run | One process, 10 slots | 1,258 |
| Short consume run | One process, 32 slots | 2,606 |
| Longer consume runs | One process, 10–32 slots | 549–705 |
| Longer consume runs | Four processes, 8–10 slots each | 1,406–1,496 |
| FIFO consume, one message group | One process | 228 |
| FIFO consume, 16 message groups | One process | 1,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/s | SQL calls/task | Worker CPU ms/task | Receives per 1,000 tasks | Deletes per 1,000 tasks |
|---|---|---|---|---|
| 205.26 | 3.9170 | 5.918 | 137 | 132 |
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
| Setting | Value |
|---|---|
| CPU | Intel Xeon Gold 6132, 8 vCPUs |
| Memory | 16 GB |
| Node.js | 24.18.0 |
| PostgreSQL | 16.14 on the same host |
| SQS service | Local Ministack |
| Connection pool | Maximum 10 connections per process |
| Polling workload | Prequeued no-op handlers; Small and 8 KiB payload cases |
| PostgreSQL/SQS workload | Prequeued no-op handlers with small payloads |
| SQS transport workload | Direct publishes or preloaded messages without the Runlane lifecycle |
| Polling timer | Execution only; enqueue, startup, warmup, verification, and shutdown excluded |
| PostgreSQL/SQS timer | Worker 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.
| Deployment | Durable result | First limit observed |
|---|---|---|
| Lambda without an event-source concurrency cap, application pool max 10 | 188 of 200 runs succeeded; 12 failed after retries | 51 concurrent Lambda invocations drove RDS to 77 of 79 connections |
| Lambda cap 2, application pool max 2 | 640 of 640 runs succeeded, with no retries; median 14.57 completions/s | No failure in the measured run; RDS peaked at 27 connections |
| Lambda cap 4, application pool max 2 | 1,840 of 1,840 runs succeeded, with no retries | RDS peaked at 55 connections; throughput and latency became unstable under higher offered load |
| Bounded Fargate queue: concurrency 5 and maintenance once per minute | A 40-run warm-up took 489 seconds; handlers took about 0.5–0.7 seconds each | Maintenance 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:
- Ingress with consumers stopped.
- Prequeued Lambda drain with producers stopped.
- Prequeued long-running worker drain with producers stopped.
- Continuous end-to-end traffic.
- 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
| Metric | Meaning |
|---|---|
| Tasks/s or completions/s | Runs that reached the measured success point each second |
| SQL calls/task | PostgreSQL requests made during the measured window, divided by completed tasks |
| Worker CPU ms/task | CPU time used by all measured worker processes, divided by completed tasks |
| PostgreSQL CPU ms/task | Database process CPU time during the measured window, divided by completed tasks |
| Execution WAL bytes/task | PostgreSQL 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.