HBase is a distributed, sorted map: rows live in regions — contiguous row-key ranges — and each region is served by exactly one RegionServer at a time. So the first question any client must answer, on every request, is deceptively simple: given this row key, which RegionServer do I talk to? The answer is a small but load-bearing piece of machinery called hbase:meta — a catalog table that maps row-key ranges to the servers hosting them. This piece walks the whole routing story: what meta stores, how a client bootstraps to it through ZooKeeper, why the old -ROOT- table was deleted, how clients cache locations aggressively and heal stale caches lazily, how splits keep meta current, and the failure modes — hotspotting, re-resolution storms, regions stuck in transition — that make meta the quiet single point everyone should understand.
The routing problem HBase has to solve
An HBase table is split by row-key range into regions. A region for the range [user_0400, user_0800) lives on one RegionServer; the next range lives on another. The mapping is not static — the balancer moves regions to even out load, regions split when they grow, and when a RegionServer dies its regions are reassigned elsewhere. So the client’s question ‘where is row user_0512?’ has an answer that changes over time.
HBase could have solved this with a central routing service that every request funnels through, but that service would become a bottleneck and a single point of failure on the hot path. Instead HBase does something more elegant: it stores the routing table as an HBase table, lets clients read and cache it directly, and keeps the master out of the read/write path entirely. That table is hbase:meta.
hbase:meta is just another HBase table
The key insight that makes the whole design cohere: hbase:meta is a normal HBase table, stored and served exactly like user data — it has regions, it lives on a RegionServer, it is read with ordinary scans and gets. There is nothing special about how it is stored; what is special is what it contains and the bootstrap trick that lets clients find it.
Its rows describe every region of every user table. Because HBase keeps rows sorted by key, and meta’s row keys are built from tableName,startKey,regionId, a client that knows a target row key can do a single reverse scan in meta to land on the region whose range brackets that key. One lookup resolves the location. Meta is, in effect, a sorted index over the cluster’s region boundaries.
What a meta row actually contains
Every region gets one row in hbase:meta, keyed by the region name and holding its routing facts in the info column family:
| Cell | Meaning |
|---|---|
info:regioninfo | The serialized RegionInfo: table name, start key, end key, region id, encoded name |
info:server | host:port of the RegionServer currently serving the region |
info:serverstartcode | Start timestamp of that RegionServer — disambiguates a restarted server from its former self |
info:seqnumDuringOpen | Store sequence id when the region was opened; used for replication and consistency checks |
info:splitA / info:splitB | Daughter regions recorded on the parent during a split |
info:mergeA / info:mergeB | Parents recorded on the child during a merge |
The pair that matters most on the hot path is regioninfo (which range?) and server (which host?). The start-code and sequence number exist to catch subtle staleness: a client must not send a request to a RegionServer instance that has since restarted and no longer owns the region.
Bootstrapping: ZooKeeper and the death of -ROOT-
There is a chicken-and-egg problem: to read meta you must know which RegionServer serves it, but that is itself a piece of routing information. Older HBase (pre-0.96) solved this with a second catalog table, -ROOT-, that pointed at meta — a two-level lookup. The reasoning was that meta might itself be large enough to split across many regions, so you needed a table to route to meta.
HBase 0.96 removed -ROOT- entirely. The realization: meta is kept to a single region in practice, so its location is one small fact — and one small fact belongs in ZooKeeper, not in another table. Today the location of the meta region is stored at the ZooKeeper znode /hbase/meta-region-server. The bootstrap chain collapsed from three hops to two:
ZooKeeper (/hbase/meta-region-server)
--> RegionServer hosting hbase:meta
--> scan meta for the user region
--> RegionServer hosting the user regionRemoving -ROOT- simplified the model and shaved a network round-trip off every cold lookup. The trade — meta living in one region — is the scaling ceiling we come back to below.
The client resolution path, step by step
Put the pieces together into what actually happens when a fresh client issues get('users', 'user_0512') with nothing cached:
1. Read /hbase/meta-region-server from ZooKeeper to learn which RegionServer hosts hbase:meta. 2. Send a reverse scan to that RegionServer: ‘find the meta row whose key is the greatest one ≤ users,user_0512.’ That row’s regioninfo gives a range that brackets the key. 3. Read info:server from that row to get the user region’s RegionServer. 4. Send the actual get to that RegionServer.
Four steps, two of them network hops beyond the request itself. That overhead is why the very next thing every client does is cache what it learned.
Aggressive client-side caching
The HBase client keeps a region location cache (historically MetaCache) mapping table + row-key ranges to RegionServers. After the first cold resolution, subsequent requests for nearby keys are answered from memory with zero ZooKeeper or meta traffic — the client goes straight to the right RegionServer. It also caches the meta location itself, so even cold user-table lookups usually skip the ZooKeeper hop.
This caching is what keeps meta off the hot path. In a healthy, stable cluster, meta is read rarely — only when a client is new, when it touches a region range it has never seen, or when a cached entry turns out to be wrong. The design deliberately pushes the routing state out to thousands of client caches rather than centralizing it, trading a little staleness for enormous read scalability.
Self-healing: lazy cache invalidation
Aggressive caching raises an obvious question: what happens when a cached location goes stale — the balancer moved the region, or it split, or its RegionServer crashed? HBase does not proactively push invalidations. It heals lazily, driven by the errors that a stale send produces.
If a client sends a request to a RegionServer that no longer owns the region, that server replies with NotServingRegionException (or RegionMovedException / RegionOpeningException). The client treats this as a cache-miss signal: it drops the stale entry, re-resolves the region through meta, updates its cache, and retries the request — transparently, within the configured retry budget. The application usually sees nothing but a slightly slower call.
This is the crux of the design’s resilience: routing state is allowed to be wrong in caches because being wrong is cheap and self-correcting. No coordination is needed to keep every client’s cache in sync; reality asserts itself one failed request at a time.
Region splits, merges, and how meta stays current
When a region grows past its threshold it splits into two daughters at a midpoint key. The RegionServer performs the split, and meta is updated atomically-in-effect: the parent row is marked offline and split (recording splitA/splitB), and two new daughter rows are added with their own ranges and server assignments. A merge does the reverse, recording mergeA/mergeB on the combined region.
Clients are not told. A client holding the now-defunct parent region in its cache will eventually send to it, receive NotServingRegionException, and re-resolve — discovering the daughters through the same lazy path as any other staleness. Splits and merges are thus just another source of cache misses, handled by the same self-healing machinery rather than a special case.
Meta is a single region — the scaling ceiling
Because -ROOT- is gone, hbase:meta is kept to one region, served by one RegionServer. For the overwhelming majority of clusters this is a non-issue: meta is read rarely thanks to caching, and one region holds millions of region rows comfortably. But it does establish a ceiling.
At very large region counts, or when many clients hit cold caches simultaneously, all meta reads converge on that single RegionServer — it cannot be split to spread the load like a user table. HBase mitigates this with meta region replicas (read-only replicas of the meta region on other servers, giving clients timeline-consistent reads that offload the primary), larger regions (fewer meta rows and fewer lookups), and generous client caches. The single-region design is a deliberate simplicity/scale trade, not an oversight — but it is the reason meta load deserves monitoring.
Failure modes: hotspotting and re-resolution storms
The failure modes almost all trace back to too much meta traffic hitting that single region at once. Meta hotspotting is the steady-state version: an application that creates fresh clients per request (never warming a cache), or churns through an enormous key space so caches never help, pins load on the meta RegionServer.
The acute version is a re-resolution storm. When a RegionServer dies, its regions are reassigned, and every client caching a region on that server will, on its next request, get an error, invalidate, and stampede to meta to re-resolve — all at once. The meta region gets slammed precisely when the cluster is already stressed from a failure. Client-side jittered backoff, healthy retry limits, and meta replicas are the defenses; the pattern to recognize is a latency spike on meta that trails a RegionServer loss.
Worth separating from these: the monotonic-key hotspot, where sequential row keys (timestamps, auto-increment ids) funnel all writes into the single newest region. That is a user-table hotspot, not a meta one, but it shows up in the same conversations — the fix is key salting or hashing so writes spread across regions.
Regions in transition and meta unavailability
Between the moment a region is closed on one server and opened on another it is in transition (RIT). Brief RIT is normal — every split, move, and reassignment passes through it. The problem is a region stuck in transition: requests for its keys fail to resolve to a live server, and clients retry against a moving target. Persistent RIT is one of the most common HBase incidents and usually points at a deeper issue (a slow or wedged RegionServer, a filesystem problem, an assignment bug).
The most severe case is meta itself being unavailable — if the RegionServer hosting the meta region is down and reassignment stalls, no client can resolve any new region location, and the cluster appears frozen even though user RegionServers are up. This is why meta assignment is prioritized during recovery and why meta health is the first thing to check when a cluster is inexplicably unresponsive.
How HBase routing compares to Cassandra and Bigtable
Seeing the alternatives sharpens why HBase does it this way. Cassandra takes the opposite tack: there is no meta table at all. Data placement is computed — a row key is hashed to a token, and consistent hashing plus the ring topology (which every node learns via gossip) tells any coordinator which nodes own that token. Routing is calculated, not looked up, so there is no catalog to bottleneck — but the price is that the placement scheme is fixed by the partitioner and rebalancing means moving token ranges.
Bigtable, HBase’s inspiration, uses a three-level hierarchy — a Chubby file pointing at a root tablet, pointing at METADATA tablets, pointing at user tablets — which is exactly the -ROOT- / .META. design HBase started with and later simplified. HBase’s current single-region meta plus ZooKeeper pointer is a deliberate middle ground: explicit, inspectable routing state (unlike Cassandra’s computed placement) but flattened to two levels (unlike Bigtable’s three). Understanding the spectrum — computed vs cataloged, flat vs hierarchical — explains why meta looks the way it does and what HBase gained by trimming a level.
How the Master and assignment use meta
Clients read meta to route requests, but meta is also the source of truth for the HBase Master’s assignment machinery. The Master’s AssignmentManager drives regions through their states — OFFLINE, OPENING, OPEN, CLOSING, CLOSED — and records the authoritative state in meta as it goes. When the Master decides to move a region for balancing, or to reassign the regions of a dead RegionServer, the transition is only considered durable once meta reflects it.
This split of duties is deliberate: the Master owns writing the assignment decisions into meta, while clients only ever read meta to follow those decisions. Because the Master is off the client hot path, a Master that is briefly down does not stop reads and writes to already-resolved regions — clients keep using their caches and meta. What a down Master does stop is new assignment: splits, balancing, and recovery from RegionServer failures pause until it returns. Meta is the shared ledger both sides agree on.
The catalog janitor: cleaning up after splits
Splits and merges leave debris in meta. When a region splits, the parent row is not deleted immediately — it is marked offline and split, and the two daughters are added while the parent’s data files are still referenced by the daughters (as half-store-file references) until compaction rewrites them. Deleting the parent too early would orphan data the daughters still point at.
A background process, the catalog janitor, periodically scans meta for split parents whose daughters have finished compacting away all references, and only then removes the parent row and its files. The same housekeeping cleans up merged regions. If the janitor is disabled or stuck, meta accumulates stale parent rows — harmless to correctness but a source of confusion when reading meta and a sign that split cleanup has stalled. It is one more reason meta is a living table that needs its background maintenance running, not a static index.
Reading meta directly: MetaTableAccessor and hbck
Because meta is a normal table, you can read it yourself — and operators frequently do, to debug routing. The supported programmatic path is MetaTableAccessor, which parses meta rows into RegionInfo and location objects so you don’t hand-decode the info:regioninfo bytes. You can also simply scan it from the shell:
hbase> scan 'hbase:meta', {LIMIT => 5}
# each row: region name -> info:regioninfo, info:server, info:serverstartcode ...When meta itself becomes inconsistent — a region present on disk but missing from meta, or two rows claiming the same range — the repair tool is HBCK2 (the successor to the old hbck), which can rebuild or patch meta assignments. Reaching for HBCK2 is a signal that normal assignment has failed; it is a recovery tool, not routine maintenance. But the fact that meta is inspectable and repairable with ordinary table tooling is a direct benefit of the ‘routing table is just a table’ design.
Operating meta: a practical checklist
Translating the architecture into operational habits:
Reuse connections. Share one Connection across your application so region caches stay warm; creating a connection per request defeats caching and hammers meta. Watch regions-in-transition as a first-class metric — a nonzero, non-decreasing RIT count is an incident forming. Enable meta region replicas if you have many clients or large region counts, to take read pressure off the single primary. Prefer fewer, larger regions over many tiny ones to keep meta small and lookups rare. Salt monotonic keys so writes don’t hotspot a single user region. And when a cluster goes unresponsive, check meta first: where it is assigned, whether that RegionServer is healthy, and whether anything is stuck in transition.
hbase:meta is the routing table that maps every region’s row-key range to the RegionServer serving it — and it is itself just an HBase table, found via a single ZooKeeper pointer (-ROOT- was deleted in 0.96). Clients resolve a row through ZooKeeper → meta → RegionServer, then cache aggressively and heal stale caches lazily on NotServingRegionException, which keeps meta off the hot path. Splits and merges update meta and surface to clients as ordinary cache misses. The catch is that meta lives in a single region: watch for meta hotspotting and post-failure re-resolution storms, use meta replicas and warm shared connections to defend it, and treat meta health and regions-in-transition as your first signals when a cluster stalls.