limit

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package limit is one rate limit for every replica.

A counter in a process's memory is three limits when three pods are running, and none after a deploy: an attacker gets the limit multiplied by the replica count and gets it back whenever anything restarts. That is why the auth module's lockout said so in its own comment and left the real thing for later. This is the real thing — one row per key per window in Postgres, one statement to record an event, one to read a count — so the answer does not depend on which pod the request landed on.

It is a fixed window rather than a token bucket: a limit stated the way a person understands it ("ten in a quarter of an hour"), one INSERT ... ON CONFLICT to record it, and the worst the edge between two windows gives an attacker is twice the limit for one instant.

Index

Constants

This section is empty.

Variables

View Source
var ErrNoConnection = errors.New("limit: no database connection on this context")

ErrNoConnection is what every method answers when Connections finds none. It is an error rather than a silent allowance because the caller decides: auth fails open and logs, which is right for a lockout and would be wrong for a paywall.

Functions

func Purge

func Purge(ctx context.Context, conn *db.Conn) error

Purge deletes the rows whose window closed a day ago. It is the caller's hourly job that runs it, because the table is written by whoever uses the limiter and by nothing else; the moment a second module adopts this, the purge belongs beside the outbox's in kit/app.

Types

type Connections

type Connections func(context.Context) (*db.Conn, bool)

Connections is where a limiter finds the pool, and httpx.ConnFrom is what the application passes: the kernel puts the connection on every request's context.

It is a parameter rather than an import for two reasons. A composition builds its modules before kit/app opens the pool, so a limiter cannot be handed one at construction; and a package that counts rows has no business linking a web server to find out where they go — modules/auth's contracts package holds a Limiter, and a contracts package is the entity and the interfaces.

type Limiter

type Limiter interface {
	// Allow records one event under key and reports whether the window is
	// still within limit: the limit-th event is allowed and the one after it is
	// not. retryAfter is what is left of the window, and it is zero when the
	// answer is yes.
	Allow(ctx context.Context, key string, limit int, window time.Duration) (ok bool, retryAfter time.Duration, err error)

	// Count reports how many events key has in the window that is open now, and
	// how long that window has left. It records nothing, because a limit with
	// three answers rather than two — allow, delay, refuse — has to be read
	// before the attempt it is about, and a read that counted would make every
	// successful sign-in an attempt against the lockout.
	Count(ctx context.Context, key string, window time.Duration) (n int, retryAfter time.Duration, err error)

	// Forget drops a key: the caller has been proved right about whoever they
	// were counting.
	Forget(ctx context.Context, key string) error
}

Limiter counts events under a key and answers whether one more is within a limit.

A key is whatever the caller counts by — an address, an account, the two together — and it is scoped to the tenant of the context before it is stored, so two customers never share a counter and no caller has to remember to say which tenant it is counting in.

func Memory

func Memory() Limiter

Memory returns a limiter that counts in this process's memory.

func Postgres

func Postgres(conns Connections) Limiter

Postgres returns the limiter every replica shares.

Jump to

Keyboard shortcuts

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