Redis Redis Stream Created: 14 Jan 2026 Updated: 14 Jan 2026

Redis Streams: At-Most-Once vs. At-Least-Once

When using Redis Streams, you are already moving away from the "fire-and-forget" nature of Pub/Sub toward a more durable architecture. However, the way your application handles communication failures with the Redis server defines the final reliability of the message.

Below is a detailed guide on implementing both patterns using a while loop for retries.

1. At-Most-Once (Fire and Forget)

In this pattern, the application attempts to add the message to the stream exactly once. If the network is down or Redis is busy, the message is discarded. There is no retry logic.

Best For: Logging, non-critical metrics, or high-frequency updates where the "latest" data is more important than "all" data.

/// <summary>
/// Attempts to add a message to the stream once.
/// If it fails, the message is lost (At-Most-Once).
/// </summary>
public async Task PublishAtMostOnceAsync(string streamKey, string message)
{
var db = _redis.GetDatabase();

try
{
// One-time attempt. We do not check for success or failure for the sake of the guarantee.
await db.StreamAddAsync(streamKey, "payload", message);
Console.WriteLine("[At-Most-Once] Message sent.");
}
catch (Exception)
{
// Fail silently or just log. We don't try again.
Console.WriteLine("[At-Most-Once] Message lost due to error.");
}
}

2. At-Least-Once (Persistent & Reliable)

This pattern uses a while loop to ensure that the message is successfully acknowledged by the Redis server. If an error occurs, the publisher retries until the message is confirmed or the maximum retry limit is reached.

Best For: Financial transactions, order processing, and critical system events.

/// <summary>
/// Ensures the message is added to the stream by retrying on failure.
/// Guarantees 'At-Least-Once' delivery to the stream.
/// </summary>
public async Task<bool> PublishAtLeastOnceAsync(string streamKey, string message)
{
var db = _redis.GetDatabase();
int maxAttempts = 3;
int currentAttempt = 0;
bool success = false;

while (currentAttempt < maxAttempts && !success)
{
currentAttempt++;
try
{
// XADD: Returns a non-null ID if stored successfully
var messageId = await db.StreamAddAsync(streamKey, "payload", message);

if (!messageId.IsNull)
{
Console.WriteLine($"[At-Least-Once] Success! Message ID: {messageId} (Attempt {currentAttempt})");
success = true;
}
}
catch (Exception ex)
{
Console.WriteLine($"[At-Least-Once] Attempt {currentAttempt} failed: {ex.Message}");

if (currentAttempt < maxAttempts)
{
// Wait before retrying
await Task.Delay(1000);
}
}
}

if (!success)
{
Console.WriteLine("[At-Least-Once] Fatal: Could not persist message after 3 tries.");
}

return success;
}

Summary Comparison

StrategyLogicRedis CommandResult on Failure
At-Most-OnceNo Loop / Single CallXADDMessage is lost if Redis is unreachable.
At-Least-Oncewhile Loop + RetriesXADDMessage is eventually stored (may cause duplicates).

Important Note on Duplicates

When you use the At-Least-Once strategy with retries, you may accidentally store the same message twice if the network fails after Redis saved the message but before you received the response.

Solution: Always include a unique Idempotency Key (like a UUID) in your message body so that your Consumer can detect and ignore duplicate entries.

Share this lesson: