Skip to content

πŸ›‘οΈ An GEO/SEO-friendly rate limiter for Go. Protects your server from abuse/DDoS while safely whitelisting verified Search Engines & AI Bots (Google, Bing, GPTBot, etc.) via IP/RDNS verification.

License

Notifications You must be signed in to change notification settings

cnlangzi/botrate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

15 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

botrate

High-performance, SEO-friendly rate limiter for Go applications

Go Reference Go Report Card codecov License: MIT


Overview

BotRate is a high-performance rate limiter designed specifically for modern web applications. Unlike traditional rate limiters that blindly block high-frequency IPs, botrate intelligently distinguishes between malicious scrapers and verified bots (Search Engines, AI Crawlers, etc.).

This ensures your site remains protected from abuse without sacrificing your SEO rankings or AI knowledge base presence.

Features

  • πŸ›‘οΈ Smart Bot Detection - Uses knownbots library for verified bot identification (Googlebot, Bingbot, GPTBot, ClaudeBot, etc.)
  • πŸ”’ Behavior Analysis - Asynchronous IP+URL pattern detection to identify scrapers
  • ⚑ High Performance - <2ΞΌs hot path latency, only rate limits blacklisted IPs
  • πŸ’Ύ Memory Efficient - Only creates token buckets for blacklisted IPs
  • 🎯 Flexible - HTTP callback handling is left to caller for maximum compatibility
  • πŸ”„ Graceful Shutdown - Proper resource cleanup with Close() method

Installation

go get github.com/cnlangzi/botrate

Quick Start

Basic Usage

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/cnlangzi/botrate"
	"golang.org/x/time/rate"
)

func main() {
	limiter, err := botrate.New(
		// Rate limiting for blacklisted IPs only
		botrate.WithLimit(rate.Every(10*time.Minute)),

		// Behavior analysis
		botrate.WithAnalyzerWindow(time.Minute),
		botrate.WithAnalyzerPageThreshold(50),
		botrate.WithAnalyzerQueueCap(10000),
	)
	if err != nil {
		log.Fatalf("Failed to create limiter: %v", err)
	}
	defer limiter.Close()

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ua := r.UserAgent()
		ip := extractIP(r)

		if !limiter.Allow(ua, ip) {
			http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
			return
		}

		w.Write([]byte("Hello, World!"))
	})

	fmt.Println("Server started on :8080")
	http.Handle("/", handler)
	http.ListenAndServe(":8080", nil)
}

// extractIP extracts the real client IP from the request.
func extractIP(r *http.Request) string {
	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
		return strings.TrimSpace(strings.Split(xff, ",")[0])
	}
	if xri := r.Header.Get("X-Real-IP"); xri != "" {
		return xri
	}
	return r.RemoteAddr
}

Using Wait Method (Blocking)

For scenarios where you want to wait instead of immediately rejecting:

handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	if err := limiter.Wait(r.Context(), ua, ip); err != nil {
		http.Error(w, err.Error(), http.StatusTooManyRequests)
		return
	}
	w.Write([]byte("Hello!"))
})

Middleware Pattern

func BotRateMiddleware(l *botrate.Limiter) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if !l.Allow(r.UserAgent(), extractIP(r)) {
				http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

// Usage
http.Handle("/", BotRateMiddleware(limiter)(myHandler))

API Reference

Options

Option Description Default
WithLimit(rate.Limit) Requests per second for blocked IPs rate.Every(10*time.Minute)
WithAnalyzerWindow(time.Duration) Analysis window duration 5*time.Minute
WithAnalyzerPageThreshold(int) Max distinct pages threshold 50
WithAnalyzerQueueCap(int) Event queue capacity 10000
WithKnownbots(*knownbots.Validator) Custom knownbots validator nil (use default)

Methods

Allow(ua, ip string) bool

Non-blocking check if the request should proceed. Returns true if allowed, false if blocked.

Bot Detection Logic:

  • Verified bot (StatusVerified): βœ… Allow immediately
  • RDNS lookup failed (StatusPending): βœ… Allow, retry verification next time
  • Fake bot (StatusFailed): ❌ Block immediately
  • Normal user: Continue to analyzer and blocklist check
allowed := limiter.Allow(ua, ip)
if !allowed {
    // Request was blocked (fake bot or blacklisted IP)
}

result := limiter.Allow(ua, ip) if !result.Allowed { // Handle denial }


#### `Wait(ctx context.Context, ua, ip string) error`

Blocks until the request is allowed or the context ends. Returns `nil` if allowed, `ErrLimit` if blocked.

**Bot Detection Logic:**
- **Verified bot** (StatusVerified): βœ… Allow immediately
- **RDNS lookup failed** (StatusPending): βœ… Allow, retry verification next time
- **Fake bot** (StatusFailed): ❌ Block immediately
- **Normal user**: Continue to analyzer and blocklist check

```go
err := limiter.Wait(ctx, ua, ip)
if err != nil {
    // Handle denial (ErrLimit) or context cancellation
}

Close()

Gracefully shuts down the limiter and releases resources. Always call this when the limiter is no longer needed.

limiter, err := botrate.New(...)
if err != nil {
    log.Fatalf("Failed to create limiter: %v", err)
}
defer limiter.Close()

How It Works

Request
  β”‚
  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 1. KnownBots Verification           β”‚  Hot path: <1ΞΌs
β”‚    - Check if UA matches known bot  β”‚
β”‚    - Verified β†’ Allow immediately   β”‚
β”‚    - RDNS failed β†’ Allow, retry     β”‚
β”‚    - Fake bot β†’ Block immediately   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β”‚
  β–Ό (only for normal users)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 2. Blocklist Check                  β”‚  Atomic read: <100ns
β”‚    - Check if IP is blacklisted     β”‚
β”‚    - Not blocked β†’ Record + Allow   β”‚
β”‚    - Blocked β†’ Rate limit           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  β”‚
  β–Ό (async)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ 3. Behavior Analysis                β”‚  Background worker
β”‚    - Record IP+URL combination      β”‚
β”‚    - Bloom filter deduplication     β”‚
β”‚    - Visit counter increment        β”‚
β”‚    - Threshold exceeded β†’ Block     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Design Decisions

  1. Fake bots blocked immediately - Known bot UAs with failed verification are blocked without rate limiting
  2. RDNS lookup failures are tolerated - Failed DNS lookups allow the request (will retry next time)
  3. Verified bots bypass everything - Googlebot, Bingbot, etc. are allowed without rate limiting
  4. Normal users go through analyzer - Behavior analysis only applies to regular users
  5. Async behavior analysis - Request processing is never blocked by analysis

Performance

Scenario Latency Memory
Normal user <1.5ΞΌs 0 bytes
Verified bot <1ΞΌs 0 bytes
Blacklisted IP <2ΞΌs ~200 bytes/IP
Fake bot <1ΞΌs 0 bytes

Total memory budget: <5MB (Bloom: 1MB + Counter: 1MB + Blacklisted IPs: variable)

Benchmark Results

$ go test -run=^$ -bench=. -benchmem -cpu=1,4,8

Key metrics to monitor:

  • ns/op - Nanoseconds per operation (lower is better)
  • B/op - Bytes allocated per operation (should be 0 for hot path)
  • allocs/op - Allocations per operation (should be 0 for hot path)

Error Handling

Errors

var ErrLimit = context.DeadlineExceeded

Check errors with:

if errors.Is(err, botrate.ErrLimit) {
    // Request was denied (fake bot or blacklisted IP)
}

Denial Reasons

Allow() returns false when:

  1. Fake bot - Known bot UA (e.g., "GPTBot") but IP verification failed
  2. Blacklisted IP - IP was flagged by behavior analysis

Wait() returns ErrLimit when:

  1. Fake bot - Blocked immediately
  2. Rate limited - Normal user on blocklist hitting rate limit
allowed := limiter.Allow(ua, ip)

if !allowed {
    // Request was denied
    // - Fake bot: blocked immediately
    // - Blacklisted IP: rate limited
}

Configuration Examples

Strict Rate Limiting

limiter, err := botrate.New(
	botrate.WithLimit(rate.Every(10*time.Minute)),
)
if err != nil {
    log.Fatalf("Failed to create limiter: %v", err)
}

Aggressive Bot Detection

limiter, err := botrate.New(
	botrate.WithAnalyzerWindow(30*time.Second),
	botrate.WithAnalyzerPageThreshold(20),
)
if err != nil {
    log.Fatalf("Failed to create limiter: %v", err)
}

High-Throughput Configuration

limiter, err := botrate.New(
	botrate.WithAnalyzerWindow(10*time.Minute),
	botrate.WithAnalyzerPageThreshold(100),
	botrate.WithAnalyzerQueueCap(50000),
)
if err != nil {
    log.Fatalf("Failed to create limiter: %v", err)
}

Custom KnownBots Validator

// Create custom validator with specific configuration
customKB, err := knownbots.New(
	knownbots.WithRoot("./custom-bots"),
	knownbots.WithSchedulerInterval(12*time.Hour),
)
if err != nil {
    log.Fatalf("Failed to create validator: %v", err)
}

// Use custom validator
limiter, err := botrate.New(
	botrate.WithKnownbots(customKB),
	botrate.WithLimit(rate.Every(5*time.Minute)),
)
if err != nil {
    log.Fatalf("Failed to create limiter: %v", err)
}

Architecture

botrate/
β”œβ”€β”€ limiter.go          # Main Limiter type and API
β”œβ”€β”€ botrate.go          # Error definitions
β”œβ”€β”€ config.go           # Configuration struct
β”œβ”€β”€ options.go          # Functional options
β”œβ”€β”€ analyzer/           # Behavior analysis engine
β”‚   β”œβ”€β”€ analyzer.go    # Core analyzer with worker
β”‚   β”œβ”€β”€ bloom.go       # Double-buffered Bloom filter
β”‚   └── counter.go     # LRU visit counter (O(1))
└── example/
    └── main.go        # Working example

Development

Makefile Commands

A Makefile is provided for common development tasks:

make help          # Show available commands
make test          # Run all tests (short + race)
make test-short    # Run short tests (fast)
make test-race     # Run tests with race detector
make test-coverage # Run tests with coverage report
make bench         # Run benchmarks (1 and 4 CPUs)
make bench-all     # Run all benchmarks (1, 4, 8 CPUs)
make build         # Build the project
make clean         # Clean build artifacts

Examples

# Run all tests
make test

# Run benchmarks
make bench

# Generate coverage report
make test-coverage

# Run benchmarks with detailed output
make bench-all

Contributing

Contributions are welcome! Please read our contributing guidelines before submitting PRs.

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for your changes
  4. Ensure all tests pass: make test
  5. Run benchmarks: make bench
  6. Submit a pull request

License

MIT License - see LICENSE for details.

Acknowledgments

About

πŸ›‘οΈ An GEO/SEO-friendly rate limiter for Go. Protects your server from abuse/DDoS while safely whitelisting verified Search Engines & AI Bots (Google, Bing, GPTBot, etc.) via IP/RDNS verification.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published