Learn how to use Kong as an API gateway for Go microservices with JWT authentication, rate limiting, and gRPC internal communication.
In this article, we show how to use Kong, a lightweight and scalable API gateway, to manage traffic between microservices written in Go. This demonstration has two services, a Users API and a Payments API. All external traffic is routed through Kong with JWT authentication and rate limiting. We will also demonstrate how the services communicate internally via gRPC while keeping the public surface secure and predictable.
Link to the GitHub repo here.
Kong is an open-source API gateway that sits in front of your services to handle routing, authentication, rate limiting, and observability. In a microservice architecture, it acts as a protective layer between external clients and your internal services, freeing individual services from concern about security and traffic control. By centralising these concerns in Kong, each microservice can focus purely on business logic. Kong handles authenticating requests, throttling traffic, and directing calls to the right upstream service.
Kong can be set up in two ways: with a database (DB mode) or without one (DB-less mode).
In DB mode, Kong stores its configuration in PostgreSQL or Cassandra and provides a RESTful Admin API for dynamic updates. In DB-less mode, the entire gateway state is defined in a single declarative file and loaded at startup.
For this project, we chose DB-less mode. There is no database to manage, which keeps the local setup simple.
The entire gateway is defined in _kong/kong.yaml. This file declares our two upstream services the users-api and payments-api along with their routes, plugins, and consumers.
services:
- name: payments-api
url: http://payments-api:8080
...
- name: users-api
url: http://users-api:8080
...Routes define how Kong directs incoming requests to the correct service. In our configuration, this can be handled at the route level or more globally at the service level.
For the Users API, we have two routes on the same path.
The users-create-route matches POST /api/v1/users and does not require authentication, to mimic account signup.
The users-route matches the same path for all other methods and requires authentication.
routes:
- name: users-create-route
paths:
- /api/v1/users
methods:
- POST
strip_path: false
- name: users-route
paths:
- /api/v1/users
strip_path: falseFor the Payments API, we take a more global approach. The JWT plugin is applied at the service level, so every route under payments-api inherits it automatically.
services:
- name: payments-api
url: http://payments-api:8080
plugins:
- name: jwt
...
routes:
- name: payments-route
paths:
- /api/v1/payments
- ~/api/v1/users/([^/]+)/payments
strip_path: falseThis flexibility lets you enforce policies broadly or carve out exceptions for specific routes.
Rate limiting protects your services from spam and accidental overload. By capping the number of requests a client can make in a given window, you ensure fair usage and prevent any single source from degrading performance for everyone else.
In our kong.yaml, rate limiting is configured per service under each service’s plugins section.
Payments API — 50 per minute, 500 per hour, 2,000 per day:
- name: rate-limiting
config:
minute: 50
hour: 500
day: 2000
policy: local
limit_by: ipUsers API — 100 per minute, 1,000 per hour, 5,000 per day:
- name: rate-limiting
config:
minute: 100
hour: 1000
day: 5000
policy: local
limit_by: ipThe local policy stores counters in memory, which is sufficient for a single-node local setup. Later, we will validate these limits with a K6 load test.
Authentication ensures that only known clients can access protected resources.
In our setup, JWT is applied selectively. The POST /api/v1/users route remains open so new clients can register. All other routes require a valid JWT signed with the configured secret.
The JWT plugin is configured in kong.yaml:
plugins:
- name: jwt
config:
uri_param_names: []
cookie_names: []
key_claim_name: iss
secret_is_base64: false
claims_to_verify:
- exp
maximum_expiration: 86400At the heart of this setup is a consumer. In Kong, a consumer represents a client or application that can consume your APIs. We define a single consumer with a JWT secret:
consumers:
- username: $JWT_CONSUMER_USERNAME
jwt_secrets:
- key: $JWT_KEY
secret: $JWT_SECRET
algorithm: HS256NOTE: This is a simplified setup for demonstration purposes. It does not adhere to proper authorization standards — the JWT is used for authentication only, and the user ID is neither verified nor embedded in the token claims.
Our two services can communicate internally through gRPC. When we want to create a payment for a user, the Payments API fetches the user from the Users API over gRPC to confirm they exist and are over 18 before accepting the payment. This avoids an internal HTTP call, and gives lower latency and stricter contracts.
The users.proto file defines a single RPC method:
service UserService {
rpc GetUserByID (GetUserByIDRequest) returns (GetUserByIDResponse);
}The Users API registers a gRPC server alongside its HTTP server:
server := http.NewServer("8080", services)
go server.Start()
grpcServer := grpc.NewServer(os.Getenv("GRPC_PORT"), services)
grpcServer.Start()The Payments API dials the Users API over an internal address:
userGRPC := grpc.NewUserGRPC(os.Getenv("USERS_API_GRPC_ADDR"))This pattern keeps service-to-service communication fast and type-safe, while Kong manages all inbound traffic from the outside world.
We use load testing to ensure the rate limiting we configured is actually being applied.
A K6 script in _k6/load.js sends repeated POST requests to the public POST /api/v1/users endpoint through Kong.
The test runs 10 virtual users for 10 seconds, intentionally exceeding the 100 requests-per-minute limit on the Users API.
make run-load
The results highlight Kong’s rate limiting in action.
Requests are either accepted (201 Created) or rejected with 429 Too Many Requests once the limit is exceeded, confirming the gateway is protecting the upstream service as intended.
For a quick, easy end-to-end test we used Hurl.
The test file in _hurl/flow.hurl exercises the entire system.
Creating users, authenticating via JWT, making payments, and verifying business rules.
It defines two scenarios.
First, an adult user (> 18) is created, retrieved, and used to make a valid payment:
POST {{BASE_URL}}/api/v1/users
Content-Type: application/json
{
"name": "Alice",
"age": 25
}
HTTP 201Then a child user (< 18) is created, and a payment attempt is correctly rejected:
POST {{BASE_URL}}/api/v1/users/{{child_user_id}}/payments
Authorization: Bearer {{JWT_TOKEN}}
Content-Type: application/json
{
"amount": 50
}
HTTP 400You can run this yourself with the following command:
make run-hurlThis gives us confidence that Kong, the microservices, and the business logic all work together correctly.
In this article, we showed how Kong can serve as a centralised API gateway for Go microservices, handling routing, JWT authentication, and rate limiting through a declarative configuration. By keeping the gateway stateless and DB-less, the setup remains simple to run locally while mirroring patterns that scale to production. Combined with gRPC for internal service communication, K6 for load validation, and Hurl for end-to-end testing, this architecture provides a solid foundation for building secure, observable, and performant APIs in Go.