# How Dragonboat Implements At-Most-Once Processing for Client Requests

> Dragonboat implements at-most-once processing for client requests using session tracking and response caching. Learn how it prevents duplicate proposals from impacting the Raft state machine.

- Repository: [lni/dragonboat](https://github.com/lni/dragonboat)
- Tags: internals
- Published: 2026-03-06

---

**Dragonboat guarantees at-most-once request processing by combining client-side session tracking with server-side response caching, ensuring that duplicate proposals with the same SeriesID are never applied twice to the Raft state machine.**

The dragonboat library by lni provides a high-performance Raft consensus implementation in Go. When building distributed systems with dragonboat, ensuring at-most-once processing of client requests is critical to prevent duplicate state mutations during network retries or leader changes.

## Understanding At-Most-Once Semantics in Distributed Systems

At-most-once semantics guarantee that a client request executes either zero or one times, never multiple times. This differs from at-least-once delivery, which may duplicate operations, and exactly-once semantics, which require additional coordination overhead. Dragonboat achieves at-most-once processing through unique request identification and persistent response caching on the Raft leader.

## The Client-Side Session Mechanism

### Session Structure and Request Identification

Every client maintains a session object defined in [`client/session.pb.go`](https://github.com/lni/dragonboat/blob/main/client/session.pb.go) that tracks three critical fields for at-most-once processing:

- **ClientID**: A unique identifier for the client instance
- **SeriesID**: A monotonically increasing counter for each request issued from the client
- **RespondedTo**: The highest SeriesID the client has successfully processed and acknowledged

When submitting a proposal via `client.NewUpdateRequest`, these fields are automatically attached to the Raft log entry, creating a unique fingerprint for deduplication.

### Tracking Client Progress with RespondedTo

The `RespondedTo` field serves as an acknowledgment mechanism. When a client successfully receives a response, it updates this value, signaling to the server which historical responses can be safely discarded. This prevents unbounded memory growth in the session manager while maintaining the at-most-once guarantee for in-flight requests.

## Server-Side Request Deduplication

### The Session Manager Architecture

The server-side logic resides in [`internal/rsm/sessionmanager.go`](https://github.com/lni/dragonboat/blob/main/internal/rsm/sessionmanager.go), which maintains a per-client map of active sessions. Each session stores a `History` map containing previously computed results indexed by their SeriesID. The `UpdateRespondedTo` method clears acknowledged entries via `session.clearTo`, ensuring the response cache remains bounded.

### The UpdateRequired Guard in StateMachine

The critical deduplication logic executes in `StateMachine.update` within [`internal/rsm/statemachine.go`](https://github.com/lni/dragonboat/blob/main/internal/rsm/statemachine.go). When processing an entry, the state machine performs a three-way check via `s.sessions.UpdateRequired`:

1. **Already responded**: If the SeriesID is less than or equal to the server's `RespondedUpTo`, return immediately without action
2. **Cached result**: If the request was processed previously but not yet acknowledged, return the stored result from `History`
3. **New request**: Only when `toUpdate` is true does the state machine invoke the actual `Update` method

This guard ensures that even if the same log entry is replayed during Raft recovery or if a client retries due to network timeout, the underlying state machine never executes the command twice.

## Code Implementation Details

The following examples demonstrate the at-most-once processing flow in dragonboat.

**Client-side session creation and proposal:**

```go
import (
    "github.com/lni/dragonboat/v4/client"
    "github.com/lni/dragonboat/v4/config"
    "github.com/lni/dragonboat/v4/raftpb"
)

// assume nh is a *client.NodeHost and cfg is a client.Config
c, err := client.NewClient(nh, cfg)               // create a client
if err != nil { panic(err) }

session, err := c.NewSession(context.TODO(), shardID, replicaID) // <‑‑ creates a Session
if err != nil { panic(err) }

cmd := []byte("increment counter")
req := client.NewUpdateRequest(session, cmd)      // attaches ClientID, SeriesID, RespondedTo
result, err := c.SyncPropose(context.TODO(), shardID, replicaID, req)
if err != nil { panic(err) }

// result.Value holds the state‑machine reply
fmt.Printf("reply = %v\n", result.Value)

```

**Server-side deduplication in StateMachine.update:**

```go
func (s *StateMachine) update(e pb.Entry) (sm.Result, bool, bool, error) {
    // … session lookup omitted …
    s.sessions.UpdateRespondedTo(session, e.RespondedTo)

    // Has this SeriesID already been responded to?
    v, responded, toUpdate := s.sessions.UpdateRequired(session, e.SeriesID)
    if responded {
        // client already knows the answer – ignore
        return sm.Result{}, true, false, nil
    }
    if !toUpdate {
        // result was cached earlier – return it without re‑applying
        return v, false, false, nil   // <- at‑most‑once guarantee
    }

    // Normal execution path – apply to the SM
    payload, _ := GetPayload(e)
    r, _ := s.sm.Update(sm.Entry{Index: e.Index, Cmd: payload})
    session.addResponse(RaftSeriesID(e.SeriesID), r)
    return r, false, false, nil
}

```

**Session manager clearing acknowledged responses:**

```go
func (ds *SessionManager) UpdateRespondedTo(session *Session, respondedTo uint64) {
    // Remove all responses ≤ respondedTo
    session.clearTo(RaftSeriesID(respondedTo))
}

```

## Summary

Dragonboat achieves at-most-once processing through a coordinated client-server session protocol:

- **Unique request identification** via `ClientID` and monotonic `SeriesID` fields defined in [`client/session.pb.go`](https://github.com/lni/dragonboat/blob/main/client/session.pb.go)
- **Client-side progress tracking** using the `RespondedTo` field to acknowledge received responses
- **Server-side response caching** in [`internal/rsm/sessionmanager.go`](https://github.com/lni/dragonboat/blob/main/internal/rsm/sessionmanager.go) that stores results indexed by SeriesID
- **Conditional execution guard** in [`internal/rsm/statemachine.go`](https://github.com/lni/dragonboat/blob/main/internal/rsm/statemachine.go) that prevents re-application of previously processed commands via the `UpdateRequired` check

This architecture ensures that network retries, leader elections, or Raft log replays never result in duplicate state machine mutations.

## Frequently Asked Questions

### What happens if a client retries a request with the same SeriesID after a leader change?

The new leader's session manager will find the cached result for that SeriesID in the `History` map and return it immediately without invoking the state machine. This preserves the at-most-once guarantee across leadership transitions because the response persists in the Raft log and session state.

### How does Dragonboat prevent unbounded memory growth in the session manager?

The `RespondedTo` field in client requests allows the server to call `session.clearTo`, which removes all response entries with SeriesID less than or equal to the acknowledged value. This garbage collection mechanism ensures that confirmed responses do not accumulate indefinitely while maintaining the at-most-once guarantee for in-flight operations.

### Can a client accidentally violate at-most-once semantics by reusing a SeriesID?

No, because the `SeriesID` is managed internally by the client session object and increments monotonically for each new request. The client API prevents manual manipulation of this counter, ensuring that duplicate SeriesIDs only occur during legitimate retries of the same logical operation, which the server correctly identifies as duplicates.

### Does at-most-once processing impact the performance of the Raft state machine?

The overhead is minimal because the session check performed by `UpdateRequired` is an O(1) map lookup. The state machine only executes the expensive `Update` method for genuinely new requests, while duplicate or cached requests return immediately without touching the application logic, effectively short-circuiting redundant work.