estimateGas works by pretending the transaction was actually being included in the blockchain, and then returning the exact gas amount that would have been charged if that pretend operation was real. In other words, it uses the exact same procedure a miner would use to calculate the actual fee.
Given this approach, a casual observer might think that there are no limitations and that the estimate will always be exactly right. Unfortunately, they would be mistaken.
Imagine a simple contract which checks the most recent block hash and then calls another large, gas-guzzling contract only if the 10th bit in the block hash is a 1. If you use estimateGas to measure the consumption of a transaction which calls this simple contract, your result will depend entirely on whether the most recent block contained a 1 in the 10th bit of its block hash. If a new block is published between when you call estimateGas and when the transaction is actually included, your estimate has a 50% chance of being wildly wrong. This may seem like a contrived example, but lots of contracts use block hashes as a source of entropy, and change their behaviour based on what the hash is. So this is something that could definitely happen in the wild.
But it gets worse. What if the contract your transaction is calling on changes its behaviour based on the transactions submitted to it? Now it's possible for the gas consumption to be different based on the order in which transactions were submitted.
For example, suppose a commonly called contract nearly always costs only a few hundred gas to call. But it contains one weird clause in its code so that a message signed by a specific key alters its behaviour, making it consume millions of gas every time it is called. The same key is then able to alter the behaviour back. Now the owner of that key can alter the contract's gas consumption at will. If the owner is a miner, they could use this "trapped contract" to turn this gas-wasting behaviour on at the beginning of every block they compute, and off at the end. As long as they never publicly publish these private transactions, then every call of estimateGas will return a low value, while every genuinely submitted transaction will cost millions of gas (which goes straight into the miner's pocket). With a careful contract design, they wouldn't even have to actually perform the calculations, because they know what the result will be. And they could execute this "fee-stealing attack" at random, only performing it occasionally so no one realises until many users have been taken in.
Again, this example is contrived. But the point is that you should always consider if an attacker has something to gain by you making a bad estimate. If there's a possible vector, put additional code into the part of your app to handle the situation where your estimate is wrong. One simple technique you should always use is to include a sane gas limit. That way there is at least an upper bound on what an attacker can do. Beyond that, remember that it's called the estimateGas call, not the guaranteedMaximumGas call and just be reasonable in your reliance on it.
tl;dr:
If there is any way for the consequences of your transaction to change depending on when or to whom it is submitted, there is a potential for estimateGas to be wrong by arbitrarily large amounts. Take extra precautions.