Skip to main content
Benchmarking a Go Gin API This guide shows how to benchmark a Gin-based HTTP API using Go’s testing package and CodSpeed. We’ll create a minimal API, design clean benchmarks measuring what matters, and run them in CI with consistent results.
Prerequisites
  • Basic knowledge of Gin and HTTP
  • Go 1.24+ (for using b.Loop() with testing.B)
  • A GitHub repository (to run CI examples)

Creating a base Gin API to work with

Let’s start with an API from the official Gin tutorial. If you never have used Gin before, following this tutorial is a great way to get started before we start benchmarking. We’ll organize the project so benchmarks target a library package while you still have a runnable server for manual testing.
api.go
The only difference here with the original code is that we’re now using the api package name instead of main for compatibility reasons.
As a small recap, this small HTTP API handles music albums by storing them in memory and has three routes:
  • GET /albums: Returns all albums
  • GET /albums/:id: Returns a specific album by ID
  • POST /albums: Creates a new album
Let’s run it to make sure it works:
terminal
And now a few to make sure it works:

GET `/albums`

terminal
Response
terminal
Response
terminal
Response
Use := for numbers in HTTPie to send them as JSON numbers rather than strings.

Adding benchmarks to the API

Now, let’s get started writing performance tests to actually measure the performance of each route of this API. First, we need to do a bit of refactoring to make it easier to write benchmarks.

Isolating the router

In the initial code, the router is created and configured in the main function. This is not ideal for any tests or benchmarks because it’s impossible to reuse the router configuration. Let’s isolate the router creation and configuration in a separate function:
api.go

Writing the first benchmark

Now, let’s write the first benchmark for the GET /albums route, strongly inspired by the Gin documentation on writing tests:
api/api_test.go
This benchmark creates a router, creates a request and a response recorder, and then loops over the code to benchmark using b.Loop(), measuring the time it takes for each iteration. Let’s run it:
terminal
It works! The first benchmark is running, and the results are displayed. Let’s dive in the numbers:
  • First we can see in the [GIN] logs that our request takes roughly 2µs on average. This is an interesting reference point but actually not the source of truth we’ll use for our benchmark results.
  • The benchmark name BenchmarkGetAlbums-10 has the -10 suffix, which means it ran on 10 CPU cores.
  • It ran 54,296 times, taking an average of 21,328 ns per operation (which translates to 21.328 µs per request in our case).
  • Overall, benchmarking this module took 1.460 seconds.
However, seeing the output of this first run, we can note a few things that are not ideal:
  • There is a significant overhead in our measurement: we end up measuring ~21µs per request, but the reported timing of a single request by the router is ~2 µs. That means 90% of what we measure in not what’s happening in the router.
  • As mentioned in the logs, Gin is running in debug mode here: since we want to measure something as closely related to what happens in prod, we should run the router in release mode to measure realistic performance.
  • The output of the router is very verbose, while it’s very convenient for integration or unit tests, it’s harmful to have all those logs in the benchmark since we’re also measuring STDOUT performance here.

Configuring the router for benchmarking

To fix those issues, we can create a helper function to set up the router specifically for benchmarking:
And here we go:
terminal
And now, we can first see that the output is way cleaner and simpler to understand what’s going on during the benchmarking process! Also, we can see that the overhead is way lower, and the reported timing (3.060µs) is much closer to the actual time spent in the router, putting us in a much better position to make decisions about performance improvements. Still, there’s one last thing we can do to improve the benchmark results.

Optimizing the Response Writer

Since the beginning, we reused the httptest.NewRecorder() inspired by the Gin testing example to get a response writer. This is necessary because Gin’s ServeHTTP method requires an http.ResponseWriter to handle the HTTP response, and httptest.NewRecorder() provides a concrete implementation that captures the response for inspection. This is convenient but introduces some extra costs not really worth measuring in our case:
  • Extra useless allocations
  • JSON buffering to capture the responses
  • JSON processing to handle the responses
Let’s replace it with a dummy writer that discards all data:
And now we can use it instead of the httptest.NewRecorder():
And voilà, this implementation:
  • Discards all data instead of storing it
  • No allocations - just returns success without buffering
  • Minimal CPU overhead - simple length calculation for Write()

The benchmark factory

Now, we can combine all those changes into a single helper function to create a benchmark for a given request:
And then use it, making our benchmarks way cleaner and simpler to understand:

Scaling up the benchmarking suite

Now, let’s add some more benchmarks for the other routes and scenarios:
api_test.go
And here’s the output:
terminal
We’re using the -benchtime=5s flag to run the benchmarks for 5 seconds each, making sure we get enough samples to get a good estimate of the performance.
And now, almost all branches of the API are covered by this set of benchmarks!

Running the benchmarks in CI

Local benchmarks are excellent for development iteration, but running benchmarks in CI provides consistency and automation that local benchmarking can’t match:
  • Consistent hardware: CI runners eliminate the “works on my machine” problem. Your laptop’s thermal throttling, background processes, and varying load create noise that masks real performance changes.
  • Automated detection: Catch performance regressions before they reach production. Every PR gets benchmarked automatically, making performance a first-class concern like tests.
  • Historical tracking: Build a performance timeline across commits. Spot trends, identify when regressions were introduced, and validate that optimizations actually worked.
The CodSpeed GitHub Action will automatically run the benchmarks with instrumentation and upload the results to CodSpeed. It mostly boils down to this: Two important things to note:
  • We’re using the codspeed-macro runners to run the benchmarks on an optimized and isolated CI machine, removing noise from virtualization and shared resources. Check out the Macro Runners page for more details.
  • We’re again using the -benchtime=5s flag to run the benchmarks for 5 seconds each, making sure we get enough samples to get a good estimate of the performance. Feel free to change it to your needs.
This example is for GitHub Actions, but you can use CodSpeed with any other CI providers. Check out the CI integration docs for more details on the CI integration.
And now each pull request will automatically run the benchmarks, and you’ll be able to see the results in the CodSpeed dashboard. For example, let’s use SQLite instead of an in-memory database to see the performance impact (check out the code here):
Github Comment with the benchmark results

Github Comment on the with the benchmark results

This also emits a status check on the pull request:
Status check on the PR which can be used to prevent merging regressions

Status check on the pull request which can be used to prevent merging regressions

Now, we can analyze the performance report to see the details of the performance regression:
Performance report

Performance report with the Differential Flamegraph

Here, we clearly see the dramatic performance regression in getAlbums (in red) introduced by the new (in blue) database/sql usage. However, we can see that there is almost no difference in the postAlbum benchmark:
Impact on the BenchmarkPostAlbumsValid benchmark: -2%
Diving deeper, we can see that actually the changes on the postAlbum function are important but only impact 0.1% of the total time:
Flamegraph inspector for the postAlbum function

The function is getting 31% slower but only impacts 0.1% of the total time

In the end, using SQLite would impact primarily the read operations without any impact on the write operations. Check out the pull request and the CodSpeed performance report for more details.

Next steps

Now that you’ve seen how to benchmark a Go Gin API, you can start benchmarking your own code! Here are some useful resources:

Source code

The example GitHub repository for this guide with all the code and Pull Requests.

Go integration documentation

All the details about CodSpeed’s Go integration.

Walltime instrument

Learn more about the Walltime instrument and how to use it.

Dive in performance changes

Learn more about profiling and how to read flame graphs.

Benchmarking Cookbook

Gin benchmark utilities:
gin_benchmark_utils.go
Sample usage:
api_test.go
See Go integration notes and compatibility in the Go benchmarks guide.