This article explores the differences between gRPC and RabbitMQ RPC, two leading frameworks in remote procedure calls (RPC) for microservices architecture.

Comparison overview

Feature gRPC RabbitMQ RPC
Service Definition Yes (.proto file) No
Error Handling Predefined errors list Requires custom implementation
Security TLS, Token TLS, Username/Password
Orchestration Required Not necessary
Performance Fast Slower
Requires direct connection between server and client Yes No

What is RPC?

RPC (Remote Procedure Call) enables program functions to be executed remotely. This article focuses on comparing two popular RPC frameworks in microservices: gRPC and RabbitMQ RPC.

RPC stands for Remote Procedure Call and is a protocol that allows a program to execute a function on a remote machine. It is similar to a function call, but the function is executed on a remote machine.

The major difference between a function call and a remote produce call is that the RPC is usually slower and can fail due to network issues.

What is gRPC?

gRPC is a RPC framework developed by Google. It is based on the HTTP/2 protocol and uses the Protocol Buffers as the default payload format.

An RPC is defined by a service definition, which is written in a .proto file. The service definition defines the methods that can be called remotely.

Such a .proto file can be compiled to a client and server stub, which can be used to implement the service.

Here is an example of such a .proto file. It defines a fibonacci service, which offers one function to calculate the nth fibonacci based on the input.

syntax = "proto3";

package example;

service Fibonacci {
  rpc CalcFib (FibRequest) returns (FibResponse) {}
}

message FibRequest {
  int64 input = 1;
}

message FibResponse {
  int64 output = 1;
}

That proto file generates the stub code, which then requires the implementation of a server and client. Which could look like the following:

// FibonacciServerImpl implements the server interface for our Fibonacci service.
type FibonacciServerImpl struct {
	grpc_comparison.UnimplementedFibonacciServer
}

func (FibonacciServerImpl) CalcFib(ctx context.Context, request *grpc_comparison.FibRequest) (*grpc_comparison.FibResponse, error) {
	n := base.Fib(request.Input)
	result := &grpc_comparison.FibResponse{Output: n}
	return result, nil
}

What is RabbitMQ RPC?

RabbitMQ RPC is a RPC library based on the RabbitMQ message broker. It uses the AMQP protocol to communicate between the client and server. The client sends a message to the server, which then processes the message and sends a response back to the client.

Compared to gRPC, RabbitMQ RPC does not necessary have a server/client distinction. RPC provider and consumer will need to connect to the same RabbitMQ instance.

Besides that, there is no service definition, which means, the RPC provider and consumer need to agree on the payload format.

Comparison

Service definition

gRPC relies on a service definition, which is written in a .proto file. The service definition defines the methods that can be called remotely, as well as the types for parameters and return values.

RabbitMQ offers no service definition. That means, the RPC provider and consumer need to agree on the payload and routing format.

Error handling

gRPC

For gRPC, there is a list of predefined errors , which can either indicate an error regarding gRPC or the actual remote procedure call.

The UNKNOWN error code is used to indicate unknown errors. It is also used if the error code is not specified, such as in case of errors raised by the RPC API.

RabbitMQ

For RabbitMQ RPC, there is also a list of predefined errors . But it is not as extensive as the gRPC error list. It is mainly concerned about channel, connection, and queue thematics, such as no-route or not-found for example. Though, it does not provide API error capabilities. That means, API error reporting need to be implemented as part of the API response.

Security

There are two aspects of security, transport security and authentication.

Transport

Both gRPC and RabbitMQ support transport security via TLS/SSL.

To read more about RabbitMQ TLS support, please refer to the RabbitMQ TLS documentation .

For more about gRPC TLS support, please refer to the gRPC TLS documentation , also this is a good example.

Authentication

RabbitMQ supports authentication via username and password. It is also possible to use TLS client certificates for authentication, although username and password are easier to get started with.

gRPC on the other hand relies by default on TLS client certificates for authentication. Besides that, it also supports token based authentication but please check out here for more information.

Orchestration

RabbitMQ does not need orchestration, as it is a message broker. It is possible to run multiple instances of RabbitMQ and connect to them via a load balancer.

gRPC on the other hand, requires orchestration. It is possible to run multiple instances of the gRPC server and connect to them via a load balancer. But the client needs to know about all the instances.

An example of such an orchestration is Apache ZooKeeper.

That means, gRPC requires more setup than RabbitMQ.

Performance

The performance test involved sending 1000 requests to calculate the 5th Fibonacci number. These tests were both run on a MacBook Pro M1 Max to ensure a fair comparison.

Here are the results:

gRPC

Average request time was 98.27µs.

=== RUN   TestGRPCClient
2023/09/04 00:15:11  [x] Requesting  run: 0
2023/09/04 00:15:11  [x] Requesting  run: 100
2023/09/04 00:15:11  [x] Requesting  run: 200
2023/09/04 00:15:11  [x] Requesting  run: 300
2023/09/04 00:15:11  [x] Requesting  run: 400
2023/09/04 00:15:11  [x] Requesting  run: 500
2023/09/04 00:15:11  [x] Requesting  run: 600
2023/09/04 00:15:11  [x] Requesting  run: 700
2023/09/04 00:15:11  [x] Requesting  run: 800
2023/09/04 00:15:11  [x] Requesting  run: 900
Execution time: 98.270333ms  avg:  98.27µs

RabbitMQ RPC

Average request time was 6.00ms.

=== RUN   TestRabbitRPCClient
2023/09/04 00:17:41  [x] Requesting run: 0
2023/09/04 00:17:42  [x] Requesting run: 100
2023/09/04 00:17:42  [x] Requesting run: 200
2023/09/04 00:17:43  [x] Requesting run: 300
2023/09/04 00:17:44  [x] Requesting run: 400
2023/09/04 00:17:44  [x] Requesting run: 500
2023/09/04 00:17:45  [x] Requesting run: 600
2023/09/04 00:17:46  [x] Requesting run: 700
2023/09/04 00:17:46  [x] Requesting run: 800
2023/09/04 00:17:47  [x] Requesting run: 900
Execution time: 6.002085917s  avg:  6.002085ms

Test Definition

package go_rpc_comparison

import (
	"fmt"
	"go-rpc-comparison/grpcimpl"
	"go-rpc-comparison/rabbitimpl"
	"go-rpc-comparison/utils"
	"log"
	"testing"
	"time"
)

func measureRunTime(f func()) {
	// Start measuring time
	start := time.Now()

	for i := 0; i < utils.TestRuns; i++ {
		if i%100 == 0 {
			log.Printf(" [x] Requesting  run: %d", i)
		}
		f()
	}

	// Stop measuring time
	duration := time.Since(start)

	average := duration / utils.TestRuns

	// Print the execution time
	fmt.Println("Execution time:", duration, " avg: ", average)
}

func TestGRPCClient(t *testing.T) {
	grpcimpl.InitClient()

	measureRunTime(func() {
		err := grpcimpl.RpcFib(utils.TestFibN)
		utils.FailOnError(err, "Failed to handle RPC request")
	})
}

func TestRabbitRPCClient(t *testing.T) {

	rabbitimpl.InitClient()

	measureRunTime(func() {
		_, err := rabbitimpl.FibonacciRPC(utils.TestFibN)
		utils.FailOnError(err, "Failed to handle RPC request")
	})
}

RabbitMQ RPC Server

The RabbitMQ RPC server implementation requires a queue definition and consumption.

package go_rpc_comparison

import (
	"context"
	amqp "github.com/rabbitmq/amqp091-go"
	"go-rpc-comparison/base"
	"go-rpc-comparison/utils"
	"log"
	"strconv"
	"testing"
	"time"
)

func TestRabbitRPCServer(t *testing.T) {
	conn, err := amqp.Dial("amqp://rabbitmq:rabbitmq@localhost:5672/")
	utils.FailOnError(err, "Failed to connect to RabbitMQ")
	defer conn.Close()

	ch, err := conn.Channel()
	utils.FailOnError(err, "Failed to open a channel")
	defer ch.Close()

	q, err := ch.QueueDeclare(
		rabbitimpl.RABBIT_QUEUE,    // name
		false,                      // durable
		false,                      // delete when unused
		false,                      // exclusive
		false,                      // no-wait
		nil,                        // arguments
	)
	utils.FailOnError(err, "Failed to declare a queue")

	err = ch.Qos(
		1,     // prefetch count
		0,     // prefetch size
		false, // global
	)
	utils.FailOnError(err, "Failed to set QoS")

	msgs, err := ch.Consume(
		q.Name, // queue
		"",     // consumer
		false,  // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	utils.FailOnError(err, "Failed to register a consumer")

	var forever chan struct{}

	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		for d := range msgs {
			n, err := strconv.Atoi(string(d.Body))
			utils.FailOnError(err, "Failed to convert body to integer")

			log.Printf(" [.] fib(%d)", n)
			response := base.Fib(int64(n))

			err = ch.PublishWithContext(ctx,
				"",        // exchange
				d.ReplyTo, // routing key
				false,     // mandatory
				false,     // immediate
				amqp.Publishing{
					ContentType:   "text/plain",
					CorrelationId: d.CorrelationId,
					Body:          []byte(strconv.Itoa(int(response))),
				})
			utils.FailOnError(err, "Failed to publish a message")

			d.Ack(false)
		}
	}()

	log.Printf(" [*] Awaiting RPC requests")
	<-forever
}

RabbitMQ RPC Client

The RabbitMQ RPC client implementation requires, for each request, a new queue definition and consumption.

package rabbitimpl

import (
	"context"
	amqp "github.com/rabbitmq/amqp091-go"
	"go-rpc-comparison/utils"

	"math/rand"
	"strconv"
	"time"
)

var conn *amqp.Connection
var channel *amqp.Channel

func InitClient() {
	connection, err := amqp.Dial("amqp://rabbitmq:rabbitmq@localhost:5672/")
	utils.FailOnError(err, "Failed to connect to RabbitMQ")
	conn = connection

	ch, err := conn.Channel()
	utils.FailOnError(err, "Failed to open a channel")
	channel = ch
}

func randInt(min int, max int) int {
	return min + rand.Intn(max-min)
}

func randomString(l int) string {
	bytes := make([]byte, l)
	for i := 0; i < l; i++ {
		bytes[i] = byte(randInt(65, 90))
	}
	return string(bytes)
}

func FibonacciRPC(n int) (res int, err error) {

	// Declare a receiver queue per request
	q, err := channel.QueueDeclare(
		"",    // name
		false, // durable
		false, // delete when unused
		true,  // exclusive
		false, // noWait
		nil,   // arguments
	)
	utils.FailOnError(err, "Failed to declare a queue")

	// Consume messages for that queue
	msgs, err := channel.Consume(
		q.Name, // queue
		"",     // consumer
		true,   // auto-ack
		false,  // exclusive
		false,  // no-local
		false,  // no-wait
		nil,    // args
	)
	utils.FailOnError(err, "Failed to register a consumer")

	// Define a random correlation ID for the request
	corrId := randomString(32)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	// Publish the request to the queue, pushing n as the body
	err = channel.PublishWithContext(ctx,
		"",            // exchange
		RABBIT_QUEUE, 			// routing key
		false,        // mandatory
		false,        // immediate
		amqp.Publishing{
			ContentType:   "text/plain",
			CorrelationId: corrId,
			ReplyTo:       q.Name,
			Body:          []byte(strconv.Itoa(n)),
		})
	utils.FailOnError(err, "Failed to publish a message")

	// Listen to messages on the queue and wait for the response with the same correlation ID
	for d := range msgs {
		if corrId == d.CorrelationId {
			res, err = strconv.Atoi(string(d.Body))
			utils.FailOnError(err, "Failed to convert body to integer")
			break
		}
	}

	return
}

gRPC Server

package go_rpc_comparison

import (
	"flag"
	"fmt"
	grpc_comparison "go-rpc-comparison/ecostack.dev/project/grpc-comparison"
	"go-rpc-comparison/grpcimpl"
	"google.golang.org/grpc"
	"log"
	"net"
	"testing"
)

type FibonacciServerImpl struct {
	grpc_comparison.UnimplementedFibonacciServer
}

func (FibonacciServerImpl) CalcFib(ctx context.Context, request *grpc_comparison.FibRequest) (*grpc_comparison.FibResponse, error) {
	n := base.Fib(request.Input)
	result := &grpc_comparison.FibResponse{Output: n}
	return result, nil
}


func TestGRPCServer(t *testing.T) {
	port := grpcimpl.RpcPort
	flag.Parse()
	lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	var opts []grpc.ServerOption
	grpcServer := grpc.NewServer(opts...)

	server := &grpcimpl.FibonacciServerImpl{}

	grpc_comparison.RegisterFibonacciServer(grpcServer, server)
	grpcServer.Serve(lis)
}

gRPC Client

package grpcimpl

import (
	"context"
	grpc_comparison "go-rpc-comparison/ecostack.dev/project/grpc-comparison"
	"google.golang.org/grpc"
	"log"
	"strconv"
	"time"
)

var conn *grpc.ClientConn

const RpcPort = 44553

func InitClient() {
	serverAddr := "localhost:" + strconv.Itoa(RpcPort)

	connection, err := grpc.Dial(serverAddr, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("Failed to connect: %v", err)
	}
	conn = connection
}

func RpcFib(n int64) error {
	client := grpc_comparison.NewFibonacciClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	_, err := client.CalcFib(ctx, &grpc_comparison.FibRequest{Input: n})
	if err != nil {
		log.Fatalf("Error calling SayHello: %v", err)
	}
	return err
}

Conclusion

In conclusion, choose RabbitMQ RPC for simpler setups and when ease of use is a priority. Opt for gRPC for more robust, scalable solutions, especially in environments where performance is critical.

Feature gRPC RabbitMQ RPC
Service Definition Yes (.proto file) No
Error Handling Predefined errors list Requires custom implementation
Security TLS, Token TLS, Username/Password
Orchestration Required Not necessary
Performance Fast Slower
Requires direct connection between server and client Yes No

Further reading

In case you want to learn more about RabbitMQ RPC, please refer to the RabbitMQ RPC tutorial .

If you are interested in gRPC, please refer to the gRPC documentation .

References