ratelimiter

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 21, 2026 License: MIT Imports: 17 Imported by: 0

README

gRPC Fixed-Window Rate Limiter

Lightweight, protobuf-driven fixed-window rate limiter middleware for gRPC.

  • ✅ Fixed-window semantics (no sliding window surprises)
  • ✅ Rules defined directly in .proto
  • ✅ Global + per-method limits
  • ✅ Redis or in-memory backend
  • ✅ Pluggable key strategy and logger

Installation

go get github.com/murouse/rate-limiter

How It Works

The limiter:

  1. Extracts rate_key attributes from request protobuf messages

  2. Builds a deterministic storage key

  3. Applies:

    • Global rules
    • Per-method rules (defined in proto)
  4. Uses atomic increment with TTL set only on first request

TTL is not extended on subsequent increments → strict fixed-window behavior.


Define Rules in Proto

syntax = "proto3";

package rate_limiter;

option go_package = "github.com/murouse/rate-limiter;rate_limiter";

import "google/protobuf/descriptor.proto";
import "google/protobuf/duration.proto";

message Rule {
  string name = 1;
  int32 limit = 2;
  google.protobuf.Duration window = 3;
}

extend google.protobuf.MethodOptions {
  repeated Rule rules = 51234;
}

extend google.protobuf.FieldOptions {
  string rate_key = 51235;
}

Example: Per-Method Rule

service AuthService {
  rpc SendCode(SendCodeRequest) returns (SendCodeResponse) {
    option (rate_limiter.rules) = {
      name: "per_minute"
      limit: 6
      window: { seconds: 60 }
    };
  }
}

message SendCodeRequest {
  string phone = 1 [(rate_limiter.rate_key) = "phone"];
}
What happens

For SendCode:

  • Limit: 6 requests per 60 seconds

  • Key will include:

    • namespace
    • custom rate key (e.g. user ID)
    • method name
    • rule name
    • phone field value

Usage

Basic Setup

rateLimiter := ratelimiter.New(
    ratelimiter.WithNamespace("hookah-culture"),

    ratelimiter.WithCache(
        ratelimiteradapter.NewRedisCache(redisClient),
    ),

    ratelimiter.WithGlobalLimitRules([]ratelimiter.Rule{
        {
            Name:   "global",
            Limit:  5,
            Window: time.Minute,
        },
    }),

    ratelimiter.WithRateKeyExtender(
        func(ctx context.Context, _ interface{}, _ *grpc.UnaryServerInfo) (string, error) {
            user, ok := actor.FromContext(ctx)
            if !ok {
                return "", nil
            }
            return strconv.FormatInt(user.ID, 10), nil
        },
    ),
)

Then attach to gRPC server:

grpc.NewServer(
    grpc.UnaryInterceptor(rateLimiter.UnaryServerInterceptor()),
)

Storage Backends

Uses Lua script for atomic INCR + PEXPIRE.

ratelimiter.WithCache(
    ratelimiteradapter.NewRedisCache(redisClient),
)

Guarantees:

  • Atomic increment
  • TTL set only on first increment

In-Memory

Suitable for:

  • Testing
  • Single-instance services
ratelimiter.WithCache(
    cache.NewInMemoryCache(),
)

Key Strategy

Default storage key format:

rate-limiter:<namespace>:<rateKeyExtension>:<fullMethod>:<ruleName>:<sorted_attrs>

Example:

rate-limiter:hookah-culture:42:/auth.AuthService/SendCode:per_minute:phone=79998887766

You can override formatting:

ratelimiter.WithRateKeyFormatter(customFormatter)

Global Rules

Apply to all methods:

ratelimiter.WithGlobalLimitRules([]ratelimiter.Rule{
    {
        Name:   "global",
        Limit:  100,
        Window: time.Minute,
    },
})

Custom Rate Key

By default, a static value is used.

Override to inject:

  • User ID
  • API key
  • Tenant ID
  • Any context-based identity
ratelimiter.WithRateKeyExtender(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo) (string, error) {
    return "custom-key", nil
})

Error Behavior

When a rule is exceeded:

  • gRPC status: ResourceExhausted
  • Message: rate limit exceeded: rule_name

You can customize:

ratelimiter.WithExceedErrorFormatter(customFormatter)

Design Guarantees

  • Deterministic key construction
  • Atomic counter increment
  • TTL never extended
  • No sliding-window side effects
  • Protobuf-driven configuration
  • Zero reflection at runtime for rules (cached once)

When To Use

Good fit for:

  • Auth flows (OTP, login)
  • Public APIs
  • Multi-tenant systems
  • Internal service protection

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Cache

type Cache interface {
	Increment(ctx context.Context, key string, ttl time.Duration) (int64, error)
}

Cache defines storage behavior for fixed-window rate limiting.

Implementations MUST provide atomic increment semantics.

Increment atomically increments the counter for the given key and MUST ensure that:

  1. The TTL is set only if the key did not previously exist (i.e. on the first increment).
  2. The TTL MUST NOT be extended or modified on subsequent increments.

This behavior guarantees fixed-window semantics.

If TTL is extended on every increment, the algorithm becomes a sliding window, which is NOT the intended behavior of this interface.

Implementations should ensure atomicity (e.g. Redis Lua script).

type Logger

type Logger interface {
	Debugf(msg string, args ...any)
	Infof(msg string, args ...any)
	Warnf(msg string, args ...any)
	Errorf(msg string, args ...any)
}

type Option

type Option func(*RateLimiter)

Option configures RateLimiter.

func WithCache

func WithCache(cache Cache) Option

WithCache sets a custom storage backend.

The provided cache must implement atomic fixed-window semantics.

func WithExceedErrorFormatter

func WithExceedErrorFormatter(exceedErrorFormatter exceedErrorFormatterFunc) Option

WithExceedErrorFormatter overrides the error returned when one or more rate limit rules are exceeded.

func WithGlobalLimitRules

func WithGlobalLimitRules(rules []Rule) Option

WithGlobalLimitRules configures rules that apply to all RPC methods.

func WithLogger

func WithLogger(logger Logger) Option

WithLogger sets a custom logger implementation.

func WithNamespace

func WithNamespace(namespace string) Option

WithNamespace sets a namespace prefix for all generated storage keys.

Useful when sharing the same cache across multiple services.

func WithRateKeyExtender

func WithRateKeyExtender(rateKeyExtender rateKeyExtenderFunc) Option

WithRateKeyExtender overrides the default rate key extension logic.

It allows adding custom identifiers (e.g. user ID) to the rate key.

func WithRateKeyFormatter

func WithRateKeyFormatter(rateKeyFormatter rateKeyFormatterFunc) Option

WithRateKeyFormatter overrides the storage key formatting logic.

Intended for advanced customization of key structure.

type RateLimiter

type RateLimiter struct {
	// contains filtered or unexported fields
}

RateLimiter implements a fixed-window rate limiting middleware for gRPC. The limiter enforces fixed-window semantics.

func New

func New(opts ...Option) *RateLimiter

New creates a new RateLimiter with default configuration.

By default, it uses an in-memory cache, no-op logger, default namespace, and standard key formatting behavior.

func (*RateLimiter) UnaryServerInterceptor

func (rl *RateLimiter) UnaryServerInterceptor() grpc.UnaryServerInterceptor

UnaryServerInterceptor returns a gRPC unary server interceptor that enforces fixed-window rate limiting for incoming requests.

It evaluates global and per-method rules, extracts rate key attributes from protobuf messages, and rejects requests that exceed configured limits.

type Rule

type Rule struct {
	Name   string
	Limit  int
	Window time.Duration
}

Rule describes a single fixed-window rate limiting rule.

func RateLimitRulesToModel

func RateLimitRulesToModel(rs []*ratelimiterpb.Rule) []Rule

RateLimitRulesToModel converts protobuf Rule definitions into internal Rule models used by the rate limiter.

Directories

Path Synopsis
github.com
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL