First of all, Data Loader transactions (and indeed, any API call) are limited to 200. When you load 50 million records, you're loading 250,000 calls of 200 records each. The limits reset between each transaction, as does the heap usage, so there's no concern there.
When possible, using a single flag is the more appropriate option. The trick, as I stated in my previous answer, is to set the flag before doing a potentially recursive operation, then resetting the flag afterwards. The reset step is important, because that allows partial retries to work correctly, as well as recursive updates that may occur from workflow rules, flows, etc.
My personal favorite is to simply check which records need to be updated (e.g. they differ in values), rather than blindly update all records and depend on a recursion blocking scheme. But if I absolutely need to block recursion, I'm going to use the set flag, do DML, reset flag pattern. It's the safest recursion blocker that will work 100% of the time if you are also already filtering for "only necessary updates."
Give it a try sometime, and see how efficient it can be to simply prefer not to update records, rather than to try and block recursion. It's much easier to avoid the infinite loop than to break the infinite loop with a flag.
Here's a trivial example. Let's say we want to update the account's description from the contact's description. We can do this efficiently with:
Map<Id, Account> accounts = new Map<Id, Account>([
SELECT Description FROM Account WHERE Id IN (SELECT AccountId FROM Contact WHERE Id = :Trigger.new
]);
Map<Id, Account> oldValues = new Map<Id, Account>();
Map<Id, Account> newValues = new Map<Id, Account>();
for(Contact record: Trigger.new) {
oldValues.put(
record.AccountId,
new Account(
Id=record.AccountId,
Description=accounts.get(record.AccountId).Description
)
);
newValues.put(
record.AccountId,
new Account(
Id=record.AccountId,
Description=record.Description
)
);
}
for(Id accountId: newValues.keySet().clone()) {
if(oldValues.get(accountId) == newValues.get(accountId)) {
newValues.remove(accountId);
}
}
update newValues.values();
While this example is overly complicated, it scales well with 10, 20, or even 100 fields. As many as you need. You could even write this out as a generic method with an interface or class that handles most of the work for you.
Note that there's no error handling and null checks, this is more illustrative than prescriptive, but hopefully you'll get the main point that it's easier to deal with recursion when you're not updating records that don't need to be updated, rather than blocking the triggers with any kind of static variable.
If you consider the trigger above for Contacts, and a similar one for Accounts (to update all Contact descriptions to match), you'll notice that the record that triggered the original update won't recurse back to the original contact, because the description will already match the new value. By checking for changes before applying them, you'll break almost all types of recursion naturally without any flags at all.