C# ErrorTracker

using System;
using System.Collections.Concurrent;

public sealed record ErrorState(
    int ErrorCount,
    DateTime LastError,
    DateTime SkipUntil);

public sealed class ErrorTracker
{
    private readonly ConcurrentDictionary<string, ErrorState> _errors = new();

    public bool ShouldSkip(string id)
    {
        return _errors.TryGetValue(id, out var state) && state.SkipUntil > DateTime.UtcNow;
    }

    public void RegisterError(string id)
    {
        _errors.AddOrUpdate(
            id,
            _ => CreateState(1),
            (_, existing) => CreateState(existing.ErrorCount + 1));
    }

    public void RegisterSuccess(string id)
    {
        _errors.TryRemove(id, out _);
    }

    public ErrorState? GetState(string id)
    {
        return _errors.TryGetValue(id, out var state) ? state : null;
    }

    private static ErrorState CreateState(int errorCount)
    {
        var now = DateTime.UtcNow;

        var backoff = errorCount switch
        {
            1 => TimeSpan.FromMinutes(5),
            2 => TimeSpan.FromMinutes(30),
            3 => TimeSpan.FromHours(2),
            _ => TimeSpan.FromHours(24)
        };

        return new ErrorState(
            ErrorCount: errorCount,
            LastError: now,
            SkipUntil: now.Add(backoff));
    }
}

Usage example

if (errorTracker.ShouldSkip(item.Id))
{
    Console.WriteLine($"Item {item.Id} wordt overgeslagen.");
    return;
}

try
{
    await SynchronizeAsync(item);

    errorTracker.RegisterSuccess(item.Id);
}
catch (Exception ex)
{
    errorTracker.RegisterError(item.Id);

    var state = errorTracker.GetState(item.Id);

    Console.WriteLine(
        $"Synchronisatie mislukt voor {item.Id}. " +
        $"Aantal fouten: {state?.ErrorCount}, " +
        $"Opnieuw proberen na: {state?.SkipUntil:u}");
}
102320cookie-checkC# ErrorTracker