Cameleer

Benchmarks: Apache Camel vs Cameleer

Measured with the same routes on separate cloud machines, Cameleer starts in milliseconds instead of about a second, uses a fraction of Apache Camel's memory, and moves more messages per second on the same CPUs, in routing, REST APIs, Kafka, PostgreSQL and batch jobs.

This is the full report behind the Apache Camel vs Cameleer comparison. It compares three runtimes on the same routes:

  • Cameleer 0.4.0, the native Rust binary;
  • Apache Camel on the JVM: Camel 4.23.0 on Java 21, the way most Camel runs today;
  • Apache Camel on Quarkus, native: Camel 4.22.1 compiled ahead of time with Quarkus 3.40.0 and Mandrel, the fastest-starting way to run Camel.

Every number below is generated from the raw results in benchmarks/results/ by scripts/cloud-bench/summarize.py; none is typed by hand. Each figure is the mean of two runs on two identical servers.

Results

Throughput (higher is better)

scenarioCameleerApache Camel (JVM)Apache Camel (Quarkus native)Cameleer vs Camel JVMCameleer vs Camel native
Direct call through a route2,230,208 msg/s663,701 msg/s243,522 msg/s3.3x9.1x
In-memory queue (seda), one-way1,644,186 msg/s603,426 msg/s316,442 msg/s2.7x5.1x
In-memory queue (seda), request-reply719,433 msg/s117,442 msg/s72,113 msg/s6.1x9.9x
Content-based router1,144,857 msg/s375,212 msg/s119,416 msg/s3.0x9.5x
Split and aggregate a JSON order70,926 orders/s26,622 orders/s9,274 orders/s2.6x7.6x
CSV batch (1,000-row files)998 files/s468 files/s163 files/s2.1x6.1x
Kafka stream processing6,081 records/s698 records/s694 records/s8.7x8.7x
HTTP, fixed reply99,012 req/s56,735 req/s28,584 req/s1.7x3.4x
REST API with JSON80,367 req/s37,814 req/s17,860 req/s2.1x4.4x
HTTP into Kafka17,854 req/s10,052 req/s9,267 req/s1.7x1.9x
HTTP into PostgreSQL11,007 req/s4,710 req/s3,540 req/s2.3x3.1x

Latency, p99 (lower is better)

scenarioCameleerApache Camel (JVM)Apache Camel (Quarkus native)
Direct call through a route1 µs3 µs10 µs
In-memory queue (seda), request-reply38 µs85 µs137 µs
Content-based router2 µs6 µs22 µs
Split and aggregate a JSON order30 µs70 µs382 µs
CSV batch (1,000-row files)1.7 ms3.2 ms12.5 ms
HTTP, fixed reply2.9 ms3.8 ms7.4 ms
REST API with JSON2.6 ms6.3 ms12.3 ms
HTTP into Kafka8.6 ms7.8 ms12.6 ms
HTTP into PostgreSQL10.6 ms19.8 ms27.7 ms

Memory and CPU

scenarioCameleer: peak memoryApache Camel (JVM): peak memoryApache Camel (Quarkus native): peak memoryCameleer: CPUApache Camel (JVM): CPUApache Camel (Quarkus native): CPU
Direct call through a route7 MB399 MB113 MB---
In-memory queue (seda), one-way9 MB440 MB199 MB---
In-memory queue (seda), request-reply9 MB257 MB113 MB---
Content-based router7 MB332 MB113 MB---
Split and aggregate a JSON order8 MB299 MB114 MB---
CSV batch (1,000-row files)8 MB337 MB126 MB---
Kafka stream processing36 MB214 MB130 MB---
HTTP, fixed reply18 MB244 MB121 MB318%315%306%
REST API with JSON19 MB402 MB129 MB318%313%302%
HTTP into Kafka67 MB328 MB129 MB108%148%168%
HTTP into PostgreSQL25 MB273 MB133 MB80%176%208%

Startup

CameleerApache Camel (JVM)Apache Camel (Quarkus native)
time to a started context2.5 ms1106.1 ms30.4 ms
memory at startup8 MB114 MB69 MB

What the results show

  • Startup. Cameleer has its routes running in a few milliseconds. Apache Camel on the JVM takes about a second; compiled natively with Quarkus it takes tens of milliseconds.
  • Memory. Cameleer runs these routes in tens of megabytes. Camel on the JVM uses hundreds; native Camel sits in between.
  • Throughput. Cameleer handles more messages per second than Camel on the JVM in every scenario, and more than native Camel by a wider margin: native Camel has no JIT compiler, so it trades throughput for startup.
  • Latency. Cameleer's p99 latency is lower in every scenario but one. In HTTP into Kafka it is slightly higher than Camel on the JVM (8.6 ms against 7.8 ms) while serving 1.7 times the requests: Cameleer batches the sends of concurrent requests, which raises the slowest requests' wait a little.

The scenarios

The routes are defined once per runtime from one specification: crates/cameleer-bench/src/scenarios.rs and realworld.rs for Cameleer, Bench.java for Apache Camel (the Quarkus build compiles the same class from benchmarks/camel-quarkus).

scenariowhere it runswhat the route does
Direct call through a routein processdirect: route that sets a header and counts the message
In-memory queue, one-wayin process4 producers into seda: with 4 concurrent consumers
In-memory queue, request-replyin processthe same queue with a reply for every message
Content-based routerin processchoice on a header, filter on an amount, transform with Simple
Split and aggregate a JSON orderin processsplit an order's 10 items by JSONPath and aggregate them back
CSV batchin processsplit a 1,000-row file into lines, keep the EU rows, reshape, aggregate batches of 100
HTTP, fixed replyover the networknetty-http consumer that answers with a constant JSON document
REST API with JSONover the networkread orderId and customer with JSONPath, answer with a JSON confirmation
HTTP into Kafkaover the networkpublish each order to Kafka keyed by order id, then answer
HTTP into PostgreSQLover the networkinsert each order with the sql component through a pool of 16 connections
Kafka stream processingKafka on the load machineconsume orders with 4 consumers, filter by amount, reshape, publish to another topic
Startupin processstart a context with every in-process route and an HTTP route

How we tested

Machines. Three DigitalOcean CPU-optimized droplets (c-4) in region blr1: 4 dedicated vCPUs (Intel Xeon Platinum 8168 at 2.7 GHz), 8 GB RAM, Ubuntu 24.04, on one private network. Two of them are servers that take turns running each runtime; the third runs the HTTP load generator, Apache Kafka 4.3.2 (KRaft, one broker, 4 partitions per topic) and PostgreSQL 16. All three get the same kernel settings (scripts/cloud-bench/setup-node.sh).

Runtimes. Temurin 21.0.13 for Apache Camel 4.23.0, with no JVM flags. Camel Quarkus 3.40.0 (Apache Camel 4.22.1) compiled with Mandrel 25.0.4.1. Cameleer 0.4.0 built with Rust 1.99.1 in release mode. Every download is pinned and checksum-verified.

Method.

  • Each scenario runs in a fresh process: 15 s of warmup, then five timed 5-second iterations.
  • The two servers swap roles halfway, so every runtime runs on both machines and each figure is the mean of the two. Only one runtime runs at a time.
  • The HTTP scenarios use 64 concurrent keep-alive connections from the load machine; the load generator (loadgen.rs) is the same for every runtime and sends a different order id with every request.
  • Default settings everywhere: no GC or thread-pool tuning on either side, and the same PostgreSQL pool size (16; HikariCP on the JVM, Agroal on Quarkus).
  • Peak memory and CPU of each server process come from /usr/bin/time; startup is the time from spawning the process to its READY line, median of five.

Reproduce it. A run costs about one US dollar of cloud time:

sh
export DO_TOKEN_FILE=~/do-token.txt     # a DigitalOcean API token
scripts/cloud-bench/droplets.sh up      # three c-4 droplets
scripts/cloud-bench/run.sh all          # build, fill Kafka, run every scenario
scripts/cloud-bench/droplets.sh down    # delete the droplets

See scripts/cloud-bench/README.md.

Results that need context

  • Kafka stream processing waits for every record's acknowledgement (acks=all) before the next one, on every runtime: that is the default behaviour of both Camel's and Cameleer's Kafka producer. The ratio is fair; the absolute numbers are far below what Kafka can do with batching or asynchronous sends.
  • HTTP is measured with one load machine of the same size as the server. It drives every runtime the same way, but the absolute requests per second are not a ceiling.
  • Apache Camel on Quarkus runs Camel 4.22.1, one minor version behind the JVM side, because that is what the latest Camel Quarkus release ships.

Bugs this benchmark found and fixed

Running real workloads on separate machines found three problems in Cameleer, fixed before the numbers above were taken. Each fix has a regression test.

  • HTTP keep-alive. The HTTP server had 32 worker threads and closed keep-alive connections when all were busy, so more than 32 concurrent clients reconnected on every request. The default is now 200 workers, started on demand.
  • Kafka producer. Concurrent sends waited in line one record at a time. They are now batched into one produce request per broker, as Kafka's own producer does.
  • PostgreSQL pool. poolSize only limited idle connections, so a burst opened extra connections. It is now a hard maximum, like HikariCP's, with callers waiting up to poolTimeout.

FaaS cold start and size

Startup and size of the cameleer command itself, measured with cameleer-bench coldstart (spawn to the Routes startup log line, 40 runs, medians) on Linux x86-64 (WSL2 Ubuntu 24.04 on a Ryzen 5 5600X):

build (Linux x86-64)binaryHTTP route readyrun-once job donememory, HTTP route
all components, glibc23.9 MB1.17 ms2.15 ms10.4 MB
--features cloud, static musl11.8 MB0.75 ms1.48 ms5.3 MB
--features core, static musl7.5 MB0.69 ms1.41 ms4.2 MB

Container images (linux/amd64, distroless, compressed): about 20 MB with every component, 14.5 MB with FEATURES=cloud, 12.5 MB with FEATURES=core. In AWS's provided:al2023 image with the Lambda Runtime Interface Emulator, init took 3.5–5.2 ms and warm invocations 0.7–1.3 ms. The detailed measurements, the Windows numbers and the tuning history are in benchmarks/local.md and benchmarks/analysis.md; the build options are in build features.

Found a problem?

If a number here doesn't match what you measure, please open an issue with your setup and results, or fork the repository, fix the harness, and open a pull request.

Edit this page on GitHub