peerlimit

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 26, 2026 License: MIT Imports: 9 Imported by: 0

README

peerlimit

Go Reference

peerlimit is a distributed rate limiter for Go, implemented as an embedded library rather than a standalone service. Each replica decides immediately from its own in-memory state — no network round trip — and the replicas keep that state approximately consistent by gossiping their counts. There is no shared datastore and no central coordinator, so no single failure can disable limiting.

The cost is accuracy: under partition or gossip lag the cluster may briefly allow slightly more than the limit. That suits abuse protection, noisy-tenant throttling, soft API limits, and shielding internal services — not billing-grade quotas. See DESIGN.md for the full trade-off.

Install

go get github.com/ykoloch/peerlimit

Quick start

ctx := context.Background()

lim, err := peerlimit.New(ctx, peerlimit.Config{
	Node:             "node-1",                       // unique per process
	BindPort:         7946,                           // gossip port
	Discoverer:       peerlimit.NewStaticDiscoverer("10.0.0.2:7946", "10.0.0.3:7946"),
	Rate:             100,                            // tokens added per second
	Burst:            50,                             // bucket capacity
	SyncInterval:     200 * time.Millisecond,         // how often state is gossiped
	DiscoverInterval: 10 * time.Second,               // how often peers are re-discovered
})
if err != nil {
	log.Fatal(err)
}
defer lim.Close()

if lim.Allow(ctx, "user:123") {
	// serve the request
} else {
	// reject: over the limit
}

Allow returns immediately from local state and never blocks on the network. The key sets the scope: "user:123" is an independent bucket per client, "global" one shared limit. Many slow-changing keys converge well; a single hot key shows the largest over-allow (see DESIGN.md).

Configuration

Field Type Meaning
Node string Unique identity for this process (its G-Counter cell)
BindPort int Port memberlist binds for gossip
Discoverer PeerDiscoverer How peers are found (static or DNS)
Rate float64 Tokens added per second
Burst float64 Bucket capacity (maximum burst)
SyncInterval time.Duration How often local state is gossiped and merged
DiscoverInterval time.Duration How often peers are re-discovered
KeyTTL time.Duration Optional; evict a key after this much idle time (requires SweepInterval)
SweepInterval time.Duration Optional; how often the eviction sweep runs (requires KeyTTL)
LogOutput io.Writer Optional; sink for peerlimit's and memberlist's logs (silent by default)

Every field is required except LogOutput and the KeyTTL/SweepInterval eviction pair, which is opt-in and must be set together or left unset.

peerlimit writes its own diagnostics — failed peer joins at startup and on rediscovery — to LogOutput, prefixed [peerlimit], alongside memberlist's internal logs. Leave LogOutput nil to silence both.

Discovery

A node needs one peer address to bootstrap; gossip spreads the rest. Two PeerDiscoverer implementations ship:

peerlimit.NewStaticDiscoverer("10.0.0.2:7946", "10.0.0.3:7946")     // fixed list
peerlimit.NewDNSDiscoverer("peerlimit.default.svc.cluster.local", "7946") // k8s headless service

Discovery re-runs on DiscoverInterval and re-joins, so a node self-heals from cold start, partition, or seed churn. See DESIGN.md.

Design & failure model

peerlimit is an AP system with an eventual-consistency model, and it evicts idle keys through a cluster-coordinated TTL. The architecture, the exact failure modes (over-allow and the eviction resurrection window), and the suitable deployment environments are all in DESIGN.md.

License

MIT — see LICENSE.

Documentation

Overview

Package peerlimit is a distributed rate limiter that runs as an embedded library rather than a separate service. Each process enforces the limit from its own in-memory state and decides without a network round trip; peers keep those states approximately in sync by gossiping their counts. There is no shared datastore and no central coordinator, so no single failure disables limiting.

The trade-off is accuracy: under partition or gossip lag a limit may be briefly exceeded. peerlimit suits abuse protection, noisy-tenant throttling and soft API limits — not billing-grade quotas.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Node is this process's identity in the cluster. It must be unique and
	// stable for the process's lifetime: it names this node's own G-Counter cell.
	Node nodeID
	// BindPort is the port memberlist listens on for gossip.
	BindPort int
	// Discoverer supplies the peer addresses to join, at startup and on every
	// DiscoverInterval tick.
	Discoverer PeerDiscoverer

	// Rate is the sustained refill rate, in tokens per second.
	Rate float64
	// Burst is the bucket capacity: the most tokens available at once.
	Burst float64

	// KeyTTL is how long a key may sit idle before it is evicted from local
	// state. Zero disables eviction. Must be set together with SweepInterval.
	KeyTTL time.Duration
	// SweepInterval is how often idle keys are checked against KeyTTL. Zero
	// disables eviction. Must be set together with KeyTTL.
	SweepInterval time.Duration

	// SyncInterval is how often this node gossips its state to peers and merges
	// theirs. Must be positive.
	SyncInterval time.Duration
	// DiscoverInterval is how often Discoverer is polled for peers. Must be positive.
	DiscoverInterval time.Duration

	// LogOutput receives peerlimit's own diagnostics (prefixed [peerlimit]) and
	// memberlist's internal logs. Nil discards both.
	LogOutput io.Writer
}

Config holds the parameters for a Limiter and is passed to New.

type Limiter

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

Limiter is a distributed rate limiter. Build one with New and release it with Close. It is safe for concurrent use.

Example

This example runs a single-node limiter — 100 tokens per second with a burst of 50, scoped per client by the key. In a real deployment several processes find each other through a Discoverer and keep their counts in sync over gossip; the calling code stays exactly the same.

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/ykoloch/peerlimit"
)

func main() {
	ctx := context.Background()

	lim, err := peerlimit.New(ctx, peerlimit.Config{
		Node:             "node-1",                        // unique per process
		BindPort:         7946,                            // gossip port
		Discoverer:       peerlimit.NewStaticDiscoverer(), // no peers: lone node
		Rate:             100,                             // tokens per second
		Burst:            50,                              // bucket capacity
		SyncInterval:     time.Second,
		DiscoverInterval: 5 * time.Second,
	})
	if err != nil {
		log.Fatal(err)
	}
	defer lim.Close()

	// The key decides what the limit is scoped to: "user:123" per client,
	// "global" for one shared limit.
	if lim.Allow(ctx, "user:123") {
		fmt.Println("request allowed")
	}
}

func New

func New(ctx context.Context, conf Config) (*Limiter, error)

New creates a Limiter, joins the gossip cluster via conf.Discoverer, and starts the background discover loop (and, when KeyTTL is set, the eviction sweep loop). The loops run until Close is called or ctx is cancelled.

func (*Limiter) Allow

func (l *Limiter) Allow(_ context.Context, k string) bool

Allow reports whether an event for key k is permitted now, recording it when it is. The decision is local, from current state, with no network round trip. Each key is an independent bucket, so use k to scope the limit — "user:123" per client, "global" for one shared limit.

func (*Limiter) Close

func (l *Limiter) Close()

Close stops the background loops and leaves the gossip cluster.

type PeerDiscoverer

type PeerDiscoverer interface {
	Discover(context.Context) ([]string, error)
}

PeerDiscoverer returns the current peer addresses ("host:port") to join. It is called once at startup and then polled periodically, so the set may change as peers come and go.

func NewDNSDiscoverer

func NewDNSDiscoverer(host, port string) PeerDiscoverer

NewDNSDiscoverer returns a PeerDiscoverer that resolves host to its addresses and appends port to each — e.g. a Kubernetes headless service. A name that does not resolve yields no peers rather than an error.

func NewStaticDiscoverer

func NewStaticDiscoverer(addrs ...string) PeerDiscoverer

NewStaticDiscoverer returns a PeerDiscoverer that always yields addrs. Use it for a fixed peer list known at startup.

Directories

Path Synopsis
examples
http command
Command http demonstrates using peerlimit as net/http middleware.
Command http demonstrates using peerlimit as net/http middleware.
Package httpmw adapts a peerlimit.Limiter to standard net/http middleware.
Package httpmw adapts a peerlimit.Limiter to standard net/http middleware.

Jump to

Keyboard shortcuts

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