Raise the default per-client API limit to 600 a minute (#232)

Opening Map and Nodes on a region of a few thousand nodes could spend the
whole 300-request minute and leave the map half loaded. 600 still sits
under the fail2ban flood jail's 900, so the server's 429 answers first.
This commit is contained in:
MrAlders0n authored and GitHub committed 2026-10-18 23:12:57 -04:00
1 parent 202acc6cc8
commit 127fc76092
8 files changed
+14 -14

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ var version = "dev"
// @title MeshCore Beacon API
// @version 2.0.7
// @description MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.
// @description REST requests share a configurable per-client rate limit (default 300/minute and a 300-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.
// @description REST requests share a configurable per-client rate limit (default 600/minute and a 600-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.
// @description WebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.
// @termsOfService https://github.com/MeshCore-Beacon/beacon-server
+2 -2
View File
@@ -40,11 +40,11 @@ server:
# the proxy's budget. IPv6 clients share a /64 budget to prevent address rotation.
ratelimit:
enabled: true
requests_per_minute: 300
requests_per_minute: 600
# Maximum requests in a one-second sliding window, not token-bucket capacity.
# Omitted/zero defaults to requests_per_minute. Negative limits prevent startup.
# A 429 returns error.code=rate_limited and Retry-After (1 or 60 seconds).
burst: 300
burst: 600
# MeshMapper integrations (optional, require a regional or grouped-region API key).
# Your local MeshMapper regional or grouped-region admin can generate the key.
+1 -1
View File
@@ -6276,7 +6276,7 @@ var SwaggerInfo = &swag.Spec{
BasePath: "/api/v1",
Schemes: []string{"http", "https"},
Title: "MeshCore Beacon API",
Description: "MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.\nREST requests share a configurable per-client rate limit (default 300/minute and a 300-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.\nWebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.",
Description: "MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.\nREST requests share a configurable per-client rate limit (default 600/minute and a 600-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.\nWebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
+1 -1
View File
@@ -5,7 +5,7 @@
],
"swagger": "2.0",
"info": {
"description": "MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.\nREST requests share a configurable per-client rate limit (default 300/minute and a 300-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.\nWebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.",
"description": "MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.\nREST requests share a configurable per-client rate limit (default 600/minute and a 600-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.\nWebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.",
"title": "MeshCore Beacon API",
"termsOfService": "https://github.com/MeshCore-Beacon/beacon-server",
"contact": {
+1 -1
View File
@@ -1819,7 +1819,7 @@ info:
url: https://github.com/MeshCore-Beacon/beacon-server
description: |-
MeshCore network observation backend. Ingests LoRa packets from MQTT brokers, stores in PostgreSQL, and streams live events via WebSocket.
REST requests share a configurable per-client rate limit (default 300/minute and a 300-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.
REST requests share a configurable per-client rate limit (default 600/minute and a 600-request one-second burst cap). Exceeded limits return HTTP 429 with error.code=rate_limited and a Retry-After header in seconds. CORS preflights and WebSocket upgrades do not consume this API budget.
WebSocket upgrade attempts at /ws have a configurable per-client limit (default 10/minute, including failed handshakes). Rate exhaustion returns HTTP 429 with Retry-After before upgrade. An accepted socket exceeding the concurrent cap closes with code 1013 before hello; established connections remain open.
license:
name: AGPL-3-or-later
+2 -2
View File
@@ -23,13 +23,13 @@ import (
func TestDefaultAPIRateLimit(t *testing.T) {
handler := New(nil, nil, nil, Options{MaxConnsPerIP: 5, MaxConnectsPerMinute: 1000, RateLimit: config.Resolve(&config.Config{}).RateLimit})
for i := 0; i <= 300; i++ {
for i := 0; i <= 600; i++ {
request := httptest.NewRequest(http.MethodGet, "/api/v1/brokers", nil)
request.RemoteAddr = "198.51.100.1:1234"
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
want := http.StatusOK
if i == 300 {
if i == 600 {
want = http.StatusTooManyRequests
}
if response.Code != want {
+1 -1
View File
@@ -600,7 +600,7 @@ func Resolve(cfg *Config) ResolvedConfig {
ObserverDeleteAfter: cfg.Observers.DeleteAfter.Duration,
}
if r.RateLimit.RequestsPerMinute == 0 {
r.RateLimit.RequestsPerMinute = 300
r.RateLimit.RequestsPerMinute = 600
}
if r.RateLimit.Burst == 0 {
r.RateLimit.Burst = r.RateLimit.RequestsPerMinute
+5 -5
View File
@@ -15,11 +15,11 @@ func TestRateLimitConfig(t *testing.T) {
want ResolvedRateLimitConfig
wantError bool
}{
{"omitted", "{}", ResolvedRateLimitConfig{true, 300, 300}, false},
{"disabled", "ratelimit: {enabled: false}", ResolvedRateLimitConfig{false, 300, 300}, false},
{"omitted", "{}", ResolvedRateLimitConfig{true, 600, 600}, false},
{"disabled", "ratelimit: {enabled: false}", ResolvedRateLimitConfig{false, 600, 600}, false},
{"custom", "ratelimit: {enabled: true, requests_per_minute: 900, burst: 100}", ResolvedRateLimitConfig{true, 900, 100}, false},
{"burst default follows budget", "ratelimit: {requests_per_minute: 600}", ResolvedRateLimitConfig{true, 600, 600}, false},
{"zero defaults", "ratelimit: {requests_per_minute: 0, burst: 0}", ResolvedRateLimitConfig{true, 300, 300}, false},
{"burst default follows budget", "ratelimit: {requests_per_minute: 1200}", ResolvedRateLimitConfig{true, 1200, 1200}, false},
{"zero defaults", "ratelimit: {requests_per_minute: 0, burst: 0}", ResolvedRateLimitConfig{true, 600, 600}, false},
{"negative minute", "ratelimit: {requests_per_minute: -1}", ResolvedRateLimitConfig{}, true},
{"negative burst", "ratelimit: {burst: -1}", ResolvedRateLimitConfig{}, true},
{"invalid disabled config", "ratelimit: {enabled: false, burst: -1}", ResolvedRateLimitConfig{}, true},
@@ -39,7 +39,7 @@ func TestRateLimitConfig(t *testing.T) {
})
}
cfg, err := Load(filepath.Join(t.TempDir(), "missing.yaml"))
if err != nil || Resolve(cfg).RateLimit != (ResolvedRateLimitConfig{true, 300, 300}) {
if err != nil || Resolve(cfg).RateLimit != (ResolvedRateLimitConfig{true, 600, 600}) {
t.Fatalf("missing config did not enable defaults: %v", err)
}
}