C++'s std::map::operator[] Lookup Trap: The Rate Limiter That Tracks Strangers

2026-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

The Bug

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:

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.

Key Takeaway: 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.

All newsletters