I’ve been running some simple benchmarks to compare web servers written in JS, F# and OCaml:
environments:
JS
- JS: fastify https://www.fastify.io + pg + knex (not entirely fare with the other ones since they are more bare bone than knex)
- OCaml: opium on master (httpaf) https://github.com/rgrinberg/opium + caqti
- F#: giraffe https://giraffe.wiki + dapper https://dapper-tutorial.net
I started with @shonfeder’s excellent tutorial: https://shonfeder.gitlab.io/ocaml_webapp/
Seriously if there were more tutorials like this the OCaml community would have been times bigger, they are so helpful when starting out and you don’t want to solve the same problem everyone else has already solved.
And then implemented parts of it in JS and F#
the tests:
1st just renders html, with no DB calls whatsoever
2nd:
- queries a DB
- adds an additional item to the result
- orders the list
- returns the result as JSON
I ran each test 3 times and posted the best result
This is not a scientific test using a controlled environment, this is a quick and dirty test, so take it with a bit of salt (but in many ways the results are not so unexpected). The tests are also not comparing the languages themselves but a combination of some of the more popular libraries of them, which is how you would normally use them.
EDIT: adding a link to the code: https://gitlab.com/mudrz/ocaml-web-benchmarks/-/tree/master
The tests were made with wrk -t8 -c400 -d30s
- 8 threads, 400 connections, 30 seconds;
HTML endpoint, no DB access
JS - single process
Thread Stats Avg Stdev Max +/- Stdev
Latency 44.16ms 4.73ms 59.22ms 88.44%
Req/Sec 1.10k 89.00 1.50k 82.83%
262651 requests in 30.05s, 179.60MB read
Socket errors: connect 0, read 264, write 0, timeout 0
Requests/sec: 8739.98
Transfer/sec: 5.98MB
JS - multiple processes
Thread Stats Avg Stdev Max +/- Stdev
Latency 43.37ms 56.91ms 316.27ms 83.49%
Req/Sec 2.43k 459.80 3.81k 73.25%
580742 requests in 30.05s, 397.10MB read
Socket errors: connect 0, read 239, write 0, timeout 0
Requests/sec: 19325.95
Transfer/sec: 13.21MB
OCaml - single process
Thread Stats Avg Stdev Max +/- Stdev
Latency 15.01ms 2.28ms 39.86ms 89.00%
Req/Sec 3.27k 240.86 4.53k 89.83%
781313 requests in 30.01s, 489.54MB read
Socket errors: connect 0, read 308, write 0, timeout 0
Requests/sec: 26031.65
Transfer/sec: 16.31MB
F#
Thread Stats Avg Stdev Max +/- Stdev
Latency 5.46ms 840.10us 20.34ms 83.20%
Req/Sec 8.91k 659.89 13.27k 73.25%
2130565 requests in 30.04s, 1.45GB read
Socket errors: connect 0, read 272, write 0, timeout 0
Requests/sec: 70923.15
Transfer/sec: 49.31MB
Results:
- OCaml: 1.34x more requests/s than JS, with 2.8x less latency
- F#: 3.66x more than JS, 2.7x more requests/s than OCaml, with 2.7x less latency
JSON endpoint, DB access
The JSON response for JS and OCaml was:
{"excerpts":[{"author":"kan","excerpt":"Another excerpt","source":"My source2","page":"another page"},{"author":"kan","excerpt":"My excerpt","source":"my source","page":"23"}]}
for F# it was slightly longer since option types are serialized by default with a Some/None variant (it can be changed):
{"excerpts":[{"author":"kan","excerpt":"Another excerpt","source":"My source2","page":{"case":"Some","fields":["another page"]}},{"author":"kan","excerpt":"My excerpt","source":"my source","page":{"case":"Some","fields":["23"]}},{"author":"a","excerpt":"b","source":"c","page":{"case":"Some","fields":["d"]}}]}
The DB was Postgres with 10 max connections
JS - single process
Thread Stats Avg Stdev Max +/- Stdev
Latency 57.79ms 5.68ms 88.02ms 85.24%
Req/Sec 848.20 109.95 1.37k 72.24%
202885 requests in 30.09s, 62.69MB read
Socket errors: connect 0, read 237, write 0, timeout 0
Requests/sec: 6742.09
Transfer/sec: 2.08MB
JS - multiple processes
Thread Stats Avg Stdev Max +/- Stdev
Latency 57.48ms 61.84ms 774.03ms 83.36%
Req/Sec 1.20k 260.84 2.29k 71.38%
287101 requests in 30.04s, 88.71MB read
Socket errors: connect 0, read 286, write 38, timeout 0
Requests/sec: 9558.04
Transfer/sec: 2.95MB
OCaml
Thread Stats Avg Stdev Max +/- Stdev
Latency 6.69ms 38.78ms 1.07s 98.17%
Req/Sec 1.78k 842.50 3.62k 56.33%
424454 requests in 30.02s, 100.39MB read
Socket errors: connect 0, read 253, write 0, timeout 13
Requests/sec: 14139.42
Transfer/sec: 3.34MB
F#
Thread Stats Avg Stdev Max +/- Stdev
Latency 19.27ms 3.92ms 107.53ms 82.60%
Req/Sec 2.54k 165.82 3.26k 79.21%
606868 requests in 30.02s, 261.02MB read
Socket errors: connect 0, read 259, write 0, timeout 0
Requests/sec: 20214.71
Transfer/sec: 8.69MB
Results:
- OCaml: 1.48x more requests/s than JS (up from 1.34x before), with 8.6x less latency (before: 2.7x)
- F#: 2.1x more than JS (down from 3.66x before), 1.43x more requests/s than OCaml (down from 2.7x before), with 2.88x MORE latency than OCaml (before: 2.7x LESS than OCaml)
Observations:
- JS is performing unexpectedly good compared to compiled languages
- F# (or ASP.NET Core) is really fast out of the box, with no tweaking necessary
- OCaml is running on a single process and has had Max request time of
1.07s
and Stdev 10x that of F#; in some tests it spiked to 2seconds for some requests, is this the GC? how can I troubleshoot that?
Is there a good tutorial on running OCaml with multiple processes and generally commonly faced use cases for web servers?
There are countless articles for the other ecosystems, but it is a bit difficult to find ones for OCaml, making it a bit time consuming to try to figure each thing out