Skip to content

Accounting

The internal/accounting package implements the shadow accounting model: request counters are updated asynchronously in the background so that the Envoy response path never blocks on a counter write.

Entity model

A rate-limiting entity is not just an IP address. DRL computes a composite key from:

entity_key = xxHash64(sourceIP + "|" + uriPath + "|" + sortedHeaders)

Headers included in the hash are explicitly enumerated per rule in the configuration:

accounting {
    rules {
        payments-api {
            path-prefix "/api/v1/payments"
            headers "X-API-Key" "X-Tenant-ID"
            limit 500
            per "minute"
        }
    }
}

Only the listed header names are considered; their values are included in the hash. This gives fine-grained control: a single IP can operate under different rate limits depending on which API key it uses.

Ownership via consistent hashing

The entity key is mapped to an owner node using a consistent hash ring (backed by buraksezer/consistent). The owner is the single source of truth for that entity’s counter.

flowchart TD
    R[gRPC Request\nIP + Path + Headers] --> H[xxHash64\nentity key]
    H --> C{Consistent Ring\nGetOwner}
    C -->|This node| L[Increment\nAccountingCache]
    C -->|Remote node| Q[Enqueue to\nFlusher buffer]
    L --> T{Counter ≥ Limit?}
    T -->|Yes| B[Add to BlocklistCache\n+ BroadcastBlock]
    T -->|No| OK[OK]
    Q --> F[Batch flush\nvia UDP to owner]

When the hash ring changes (node join or leave), ownership of some keys shifts to different nodes. The graceful handover mechanism (see Membership) transfers counters to new owners before a node exits.

Batched flushing

Non-owner nodes accumulate increments in a per-owner in-memory buffer. A background goroutine (the Flusher) drains these buffers on a configurable schedule:

accounting {
    settings {
        flush-interval "200ms"   // How often to drain buffers
        max-batch-size 1000      // Immediate flush when buffer reaches this size
    }
}

Batches are serialised as Protobuf CounterBatch messages and sent via memberlist.SendBestEffort (UDP). UDP fits DRL’s “Availability > Consistency” philosophy: if a batch is dropped, the counter on the owner will be slightly low for one flush interval — an acceptable trade-off that keeps the request path clean.

Zero-copy optimization

sync.Pool reuses CounterBatch objects so that high-throughput paths do not generate GC pressure.

Flusher failure handling

If a UDP send to the owner fails (network timeout, node unreachable), the Flusher logs a warning and discards the batch. It does not retry and does not fail the originating Envoy request. The next flush cycle will deliver fresh increments.

Bulk load

The Internal HTTP API exposes a POST /accounting/load endpoint that injects entities directly into the accounting cache for testing and cache warm-up purposes. Entities loaded via bulk load never trigger blocking, even if their counts exceed the configured rule limit.

OutcomeMeaning
accepted_localEntity owned by this node; counter incremented locally
accepted_remoteEntity owned by a remote node; forwarded via the Flusher (requires distributionEnabled=true)
droppedEntity is non-local and distributionEnabled=false, or Flusher not configured
no_matchNo accounting rule matched the entity’s path
invalidJSON parsing failed or required fields missing

Bulk-load outcomes are exported as the drl_accounting_bulk_load_total{result=...} Prometheus counter.

Rule configuration reference

accounting {
    settings {
        algorithm "sliding-window"      // Rate limiting algorithm (only "sliding-window" currently)
        retry-after-type "delay-seconds" // Retry-After header format: "delay-seconds" or "http-date"
        flush-interval "200ms"
        max-batch-size 1000
    }

    rules {
        <rule-name> {
            path-prefix "/api/v1/..."   // URI path prefix to match
            headers "Header-Name"       // Zero or more headers to include in the entity key
            limit 1000                  // Request count threshold
            per "minute"                // Window unit: "second" or "minute"
        }
    }
}

Multiple rules can co-exist. DRL evaluates rules in order and applies the first matching rule to an entity.