Limit and merge saving operations
Alternative: Rx
Creating your own producer/consumer solution is not always easy and is one option but you could also use the tools that are already there. One such tool are the Reactive Extensions (NuGet-Package: System.Reactive). It can be very helpful in your scenario.
I requires two parties

an observable and
an observer

Your not yet real observables are the POCO objects that implement the INotifyPropertyChanged interface. You can turn them into observables with helper factories that can derive an IObservable from the PropertyChanged event pattern.
class Person : INotifyPropertyChanged, IObservable&lt;EventPattern&lt;PropertyChangedEventArgs&gt;&gt;
{
    private readonly IObservable&lt;EventPattern&lt;PropertyChangedEventArgs&gt;&gt; _observable;

    private string _name;

    public Person()
    {
        _observable =
            Observable
                .FromEventPattern&lt;PropertyChangedEventHandler, PropertyChangedEventArgs&gt;(
                    handler =&gt; PropertyChanged += handler,
                    handler =&gt; PropertyChanged -= handler
                );
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public string Name
    {
        get =&gt; _name;
        set
        {
            _name = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
        }
    }

    public IDisposable Subscribe(IObserver&lt;EventPattern&lt;PropertyChangedEventArgs&gt;&gt; observer)
    {
        return _observable.Subscribe(observer);
    }
}

Now you only need a task to process a collection of them. You can create it in many different ways so here is just one of them. I created an observable from an array of persons that I Merge into a single observable that I want to watch. I'd like this to be processed either every ten items or every two seconds (there are also other options). Then each batch is processed with the ForEachAsync loop.
var persons = new[]
{
    new Person { Name = &quot;John&quot;},
    new Person { Name = &quot;Tom&quot; }
};

var savePersonBatchTask = 
    persons
        .Merge()        
        .Buffer(timeSpan: TimeSpan.FromSeconds(2), count: 10)
        .Where(batch =&gt; buffer.Count &gt; 0)
        .ForEachAsync(batch =&gt;
        {
            Console.WriteLine($&quot;New batch arrived: {batch.Count}&quot;);
        });

You can test this simulation by creating another loop that modifies the person's name 27 times.
for (int i = 0; i &lt; 27; i++)
{
    foreach (var person in persons)
    {
        person.Name = i.ToString();
    }
}

await savePersonBatchTask;

The output will look like this
New batch arrived: 10
New batch arrived: 10
New batch arrived: 10
New batch arrived: 10
New batch arrived: 10
New batch arrived: 4

There is a lot more it can do and all that is explained very nicely on Introduction to Rx.

Review
And I also have a couple of comments about your code...

private readonly Queue&lt;Tuple&lt;string, object&gt;&gt; _saveOrders


You can simplify this and make it easier to use by using strong tuples with named properties. All you need is to throw away the Tuple type and let the compiler derive the tuple for you.
 private readonly Queue&lt;(string FileName, object Item)&gt; _saveOrders = ..;

later in code you also don't need it anymore
saveOrders.Enqueue((fileName,item));

Your while loop will become simpler too because tuples can be deconstructed into variables:
while (_saveOrders.Count &gt; 0)
{
    var (fileName, item) = _saveOrders.Dequeue();
    if (_saveOrders.All(o =&gt; o.FileName != fileName))
    {
        await SaveHelper.TrySaveToAppData(fileName, item);
    }
}