std::map::operator[] Lookup Trap: The Rate Limiter That Tracks Strangers2026-06-27
This RateLimiter tracks how many requests each client has made and decides whether to throttle them. Looks watertight — but in production the map grows without bound, and an audit shows clients in activeClients() that the application has never served. What's going on?
#include <map>
#include <string>
#include <vector>
class RateLimiter {
std::map<std::string, int> requestCounts;
static constexpr int LIMIT = 100;
public:
void recordRequest(const std::string& clientId) {
requestCounts[clientId]++;
}
bool shouldThrottle(const std::string& clientId) {
return requestCounts[clientId] >= LIMIT;
}
std::vector<std::string> activeClients() const {
std::vector<std::string> result;
for (const auto& [id, count] : requestCounts)
result.push_back(id);
return result;
}
};
// limiter.shouldThrottle("bob"); // bob never made a request
// limiter.activeClients(); // {"alice", "bob"} — bob is a ghost
std::map::operator[] is not a lookup operator — it's a find-or-insert operator. When the key is missing, it default-constructs a value, inserts it, and returns a reference to that new entry. For int, that means a zero. So every call to shouldThrottle("bob") for a never-seen client silently grows the map by one entry pointing at 0.
The consequences cascade:
shouldThrottle with arbitrary client IDs — a health check, a misrouted request, a malicious scanner — inflates the map forever. This is a textbook DoS vector.activeClients() now reports clients who never sent a request. Any analytics built on the map's size() or keys lies.shouldThrottle is non-const. Mark it const and the compiler rejects operator[] immediately, because the operator's signature reflects its mutating nature.The fix is to lookup with find() (or contains() / at() in C++20), and — critically — to make read-only methods const so the type system catches future regressions:
bool shouldThrottle(const std::string& clientId) const {
auto it = requestCounts.find(clientId);
if (it == requestCounts.end()) return false;
return it->second >= LIMIT;
}
The same trap lurks in std::unordered_map, and a nastier variant lives in pointer-valued maps: map<K, Widget*> inserts a nullptr on missed lookup, so the very next line that dereferences the "result" segfaults — often in a totally unrelated piece of code that later iterates the map.
There's a deeper principle here: when an operator's behavior depends on whether something exists, and the failure mode is silent insertion rather than an exception or sentinel, const-correctness is your only line of defense. Treat const not as decoration but as a load-bearing part of your API contract. If a method only reads, mark it const — and let the compiler enforce it.
map[key] in C++ is not a lookup — it's an insert-if-missing; use find() for reads and mark read-only methods const so the compiler catches the mistake for you.
