Crystal lang vs NodeJS vs Golang http benchmark

I like performance and efficiency. That's why I benchmark regularly code, frameworks etc... NodeJS is based on V8, the javascript engine of Chrome, developed by Google, which is fast for an interpreted environment. Crystal is a language that is inspired by Ruby, but compiles into a static binary file, with LLVM. Go is a static language created by Google, inspired by C and Pascal.

Don't Forget to try the Hackmyfortress Challenge !

Test configuration

This is the configuration where tests will run :

  • i7-4702MQ - 4 cores, 8 threads
  • 8Gb SO-DIMM DDR3L

We will test a simple http server with 1 thread, and with 4 threads. Versions of softwares used :

  • Nodejs : v7.2.1
  • Crystal : v0.20.0
  • Go : v1.7.4

Results

Mono Threaded

  • NodeJS mono-threaded :
node --use-strict mono.js

wrk -t3 -c1000 -d10s http://127.0.0.1:8080
Running 10s test @ http://127.0.0.1:8080
  3 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    48.31ms  147.83ms   1.69s    96.97%
    Req/Sec    10.58k     2.41k   20.70k    74.67%
  315881 requests in 10.01s, 38.86MB read
  Socket errors: connect 0, read 220, write 0, timeout 0
Requests/sec:  31558.31
Transfer/sec:      3.88MB
  • Crystal-lang mono-threaded :
crystal build --release mono.cr && ./mono

wrk -t3 -c1000 -d10s http://127.0.0.1:8080
Running 10s test @ http://127.0.0.1:8080
  3 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    11.90ms   27.66ms 836.66ms   99.08%
    Req/Sec    32.59k     2.65k   41.07k    67.33%
  972623 requests in 10.00s, 85.34MB read
Requests/sec:  97222.67
Transfer/sec:      8.53MB
  • Golang mono-threaded :
go build -ldflags "-s -w" mono.go && ./mono

wrk -t3 -c1000 -d10s http://127.0.0.1:8080
Running 10s test @ http://127.0.0.1:8080
  3 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    13.39ms    8.54ms 219.59ms   94.19%
    Req/Sec    23.76k     2.16k   29.46k    75.33%
  709427 requests in 10.00s, 96.07MB read
Requests/sec:  70913.89
Transfer/sec:      9.60MB

Multithreaded

  • NodeJS multithreaded :
node --use-strict multi.js

 wrk -t3 -c1000 -d10s http://127.0.0.1:8080
Running 10s test @ http://127.0.0.1:8080
  3 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    24.02ms   93.68ms   1.28s    96.96%
    Req/Sec    31.83k     4.22k   45.84k    89.00%
  950461 requests in 10.01s, 116.93MB read
Requests/sec:  94996.08
Transfer/sec:     11.69MB
  • Crystal-lang multithreaded :
crystal build --release multi.cr && ./multi

wrk -t3 -c1000 -d10s http://127.0.0.1:8080
Running 10s test @ http://127.0.0.1:8080
  3 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.10ms  827.26us  21.19ms   72.01%
    Req/Sec   107.64k     6.10k  120.57k    91.00%
  3212392 requests in 10.01s, 281.85MB read
Requests/sec: 320763.86
Transfer/sec:     28.14MB
  • Golang multithreaded :

Golang doesn't expose SO_REUSEPORT yet, so we can't really test it with multithreading

Update :

As mentionned by Sergey Lanzman, Golang > 1.5 user all cores, so I dir another benchmark with version 1.7.4 on all cores (> 4):

wrk -t3 -c10000 -d10s http://127.0.0.1:8080
Running 10s test @ http://127.0.0.1:8080
  3 threads and 10000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    53.31ms  125.26ms   1.83s    97.17%
    Req/Sec    52.53k    17.03k  100.42k    63.12%
  1557873 requests in 10.05s, 210.97MB read
  Socket errors: connect 0, read 340, write 0, timeout 319
Requests/sec: 154981.46

Update 2 :

Damon Zhao has mentionned the package fasthttp, here is the result with this package :

wrk -t3 -c1000 -d10s http://127.0.0.1:8080
Running 10s test @ http://127.0.0.1:8080
  3 threads and 1000 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     3.22ms    5.17ms 203.04ms   99.81%
    Req/Sec   106.02k     5.75k  124.38k    75.33%
  3162268 requests in 10.01s, 533.79MB read
Requests/sec: 315919.73
Transfer/sec:     53.33MB

Conclusions

Crystal is fast, faster than go and 3x than nodejs, and scales very well. NodeJS is the slower, but it could be multithreaded, so it can handle more requests thant Go. Go doesn't use SO_REUSEPORT, so we can't realy test it with multithreading. Even with all cores, Golang is not as fast as Crystal, but faster than NodeJS. The 3 languages could be used for net/web applications. Crystal is inspired by Ruby. If you like Ruby and want performances, you should try Crystal.

Update :

Go package fasthttp is as fast as Crystal default http module. You need to install the fast http package to use it : go get -u github.com/valyala/fasthttp. If we only consider the default modules, Crystal wins.

The code

NodeJS mono-threaded code

var http = require('http');
const PORT=8080;

function handleRequest(req, res)
{
    res.end('Welcome on Hackmyfortress.com');
}

var server = http.createServer(handleRequest);
server.listen(PORT, function()
{
  console.log("Server listening on: http://localhost:%s", PORT);
});

NodeJS multi-threaded code

var cluster = require("cluster");
var http = require("http");
const PORT=8080;
var NUM = 1;

var numThread = 4;

if (cluster.isMaster)
{
  var i = 0;

  while (i < numThread)
  {
    cluster.fork();
    i++;
  }

  cluster.on("exit", function(worker, code, signal)
  {
    console.log("Worker " + worker.process.pid + " died");
  });
}
else
{
  function handleRequest(req, res)
  {
      res.end('Welcome on Hackmyfortress.com');
  }

  var server = http.createServer(handleRequest);
  server.listen(PORT, function()
  {
    console.log("Server listening on: http://localhost:%s", PORT);
  });
}

Crystal lang mono-threaded code

require "http/server"

server = HTTP::Server.new(8080) do |context|
  context.response.print "Welcome on Hackmyfortress.com"
end

puts "Listening on http://127.0.0.1:8080"
server.listen

Crystal lang multi-threaded code

SO_REUSEPORT must be added to crystal to enable multi-threading capacities.

require "http/server"

class Cluster

    def self.fork (env : Hash)
        env["FORKED"] = "1"
        return Process.fork { Process.run(PROGRAM_NAME, nil, env, true, false, true, true, true, nil ) }
    end

    def self.isMaster
        (ENV["FORKED"] ||= "0") == "0"
    end

    def self.isSlave
        (ENV["FORKED"] ||= "0") == "1"
    end

    def self.getEnv (env : String)
        ENV[env] ||= ""
    end

end

numThread = 4;

if(Cluster.isMaster())
  numThread.times do |i|
        Cluster.fork({"i" => i.to_s})
  end
  sleep
else
  server = HTTP::Server.new(8080) do |context|
  context.response.print "Welcome on Hackmyfortress.com"
  end

  puts "Listening on http://127.0.0.1:8080"
  server.listen
end

Golang mono-threaded code

package main

import (
        "net/http"
)

type Hello struct{}

func (h *Hello) ServeHTTP(w http.ResponseWriter, r *http.Request)  {
        w.Write([]byte("Welcome on Hackmyfortress"));
}

func main() {
        http.ListenAndServe(":8080", &Hello{})
}

Golang multi-threaded code

Unfortunately, Golang doesn't include SO_REUSEPORT yet, and goroutines aren't thread, so, no result yet for multithreaded Go example.

Update :

As mentionned by Sergey Lanzman, no need to use SO_REUSEPORT in Golang > 1.5, GOMAXPROCS is set to all. So the same code in "Monothreaded" section is used here.

Update 2 :

Damon Zhao has mentionned the package fasthttp for Golang, here is the code used with fasthttp :

package main
import(
    "log"
    "fmt"
    "github.com/valyala/fasthttp"
)
func main()
{
  requestHandler := func(ctx *fasthttp.RequestCtx)
  {
      fmt.Fprintf(ctx, "Welcome on Hackmyfortress.com")
  }

  s := &fasthttp.Server
  {
    Handler: requestHandler,
    Name: "Hackmyfortress Server",
  }

  if err := s.ListenAndServe("127.0.0.1:8080"); err != nil
  {
    log.Fatalf("error in ListenAndServe: %s", err)
  }
}

Adrien