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.
- π‘οΈ Smart Bot Detection - Uses
knownbotslibrary 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
go get github.com/cnlangzi/botratepackage 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
}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!"))
})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))| 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) |
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
}
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()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 β
βββββββββββββββββββββββββββββββββββββββ
- Fake bots blocked immediately - Known bot UAs with failed verification are blocked without rate limiting
- RDNS lookup failures are tolerated - Failed DNS lookups allow the request (will retry next time)
- Verified bots bypass everything - Googlebot, Bingbot, etc. are allowed without rate limiting
- Normal users go through analyzer - Behavior analysis only applies to regular users
- Async behavior analysis - Request processing is never blocked by analysis
| 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)
$ go test -run=^$ -bench=. -benchmem -cpu=1,4,8Key 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)
var ErrLimit = context.DeadlineExceededCheck errors with:
if errors.Is(err, botrate.ErrLimit) {
// Request was denied (fake bot or blacklisted IP)
}Allow() returns false when:
- Fake bot - Known bot UA (e.g., "GPTBot") but IP verification failed
- Blacklisted IP - IP was flagged by behavior analysis
Wait() returns ErrLimit when:
- Fake bot - Blocked immediately
- 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
}limiter, err := botrate.New(
botrate.WithLimit(rate.Every(10*time.Minute)),
)
if err != nil {
log.Fatalf("Failed to create limiter: %v", err)
}limiter, err := botrate.New(
botrate.WithAnalyzerWindow(30*time.Second),
botrate.WithAnalyzerPageThreshold(20),
)
if err != nil {
log.Fatalf("Failed to create limiter: %v", err)
}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)
}// 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)
}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
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# Run all tests
make test
# Run benchmarks
make bench
# Generate coverage report
make test-coverage
# Run benchmarks with detailed output
make bench-allContributions are welcome! Please read our contributing guidelines before submitting PRs.
- Fork the repository
- Create a feature branch
- Add tests for your changes
- Ensure all tests pass:
make test - Run benchmarks:
make bench - Submit a pull request
MIT License - see LICENSE for details.
- knownbots - Bot detection library
- bloom - Bloom filter implementation
- golang/x/time - Token bucket rate limiter