# LudicrousDB: a HyperDB-derived database layer for WordPress at scale

> LudicrousDB is a GPL-2.0 WordPress database interface that replaces $wpdb with a layer that can route reads and writes across replicated servers, fail over, balance load and partition tables by dataset. It suits a large multisite install that has outgrown one MySQL server, and it is a fork of Automattic's HyperDB that now tracks upstream changes deliberately.

**stuttter/ludicrousdb** — LudicrousDB is an advanced database interface for WordPress that supports replication, failover, load balancing, & partitioning

- Repository: https://github.com/stuttter/ludicrousdb
- Stars: 565 · Forks: 84
- Language: PHP
- License: GPL-2.0
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/stuttter-ludicrousdb

## A WordPress database layer built on HyperDB, kept deliberately close to it

LudicrousDB is described as an advanced database interface for WordPress that supports replication, failover, load balancing and partitioning, based on Automattic's HyperDB drop-in.

That last clause is the one to understand before anything else, and the project is unusually explicit about it. LudicrousDB began as a distribution of HyperDB and intentionally retains it as an upstream compatibility source, and there is an UPSTREAM.md file that documents the audited baseline and the rules used to review later HyperDB changes.

That is a meaningful position for a WordPress plugin. The alternative to forking HyperDB is to write your own layer, and then you inherit the problem that HyperDB itself was written to solve, that WordPress's $wpdb assumes one server and no notion of replication, and that a site with a read replica has to choose between serving stale data and doing the routing itself. For a large install, forking a maintained implementation is a reasonable trade, and the price is the obligation to track upstream, which is what the UPSTREAM.md file is for.

The project also has the tooling of a maintained PHP library in the repository: a composer.json and composer.lock, a phpunit.xml.dist, a phpcs.xml with a phpcs-baseline.json, a phpstan.neon.dist with a phpstan-baseline.neon, an AGENTS.md and a tests/ directory. The two baselines are the interesting detail, because a baseline file records the violations that exist today, which is how a large legacy codebase adopts a strict linter and a strict analyser without rewriting itself first.

The releases are 5.3.1 from 2026-09-23, 5.3.0 from 2026-04-29 and 5.2.0 from 2024-06-23, and the last push to the repository was on 2026-10-17, so the work is current. The primary language is PHP and the licence is GPL-2.0, which for a WordPress plugin is the licence a site running it would expect.

## Installing: a plugin folder and three drop-in files

The installation is two parts, and the second part is the one that is easy to get wrong.

The first part is the plugin itself. You copy the main ludicrousdb plugin folder and its contents to either wp-content/plugins/ludicrousdb/ or wp-content/mu-plugins/ludicrousdb/, and the documentation says it does not matter which one because LudicrousDB will work it out. The folder name should be exactly ludicrousdb, and there is a specific warning about this: be careful when you use Download ZIP from GitHub and unzip it, because the archive usually contains a top-level folder named after the repository, and unzipping it into wp-content/plugins produces a directory called ludicrousdb-main or similar, and the loader will not find it.

The second part is the drop-ins. WordPress supports a small number of drop-in style plugins used for advanced overriding of specific pieces of functionality, and LudicrousDB includes three:

* db.php, which maps to wp-content/db.php, is the bootstrap for the replacement $wpdb object.
* db-error.php, which maps to wp-content/db-error.php, is the endpoint for fatal database error output to users.
* db-config.php, which maps to ABSPATH/db-config.php, is for configuring your database environment.

The documentation's advice is that you will probably want to copy these to their locations and modify them once you are comfortable with what they do.

The mapping is worth internalising, because it explains how the mechanism works at all. WordPress loads wp-content/db.php in place of its own database layer if the file exists, and ABSPATH is the WordPress root, so a file there is outside the theme and outside the plugin directory. A plugin in mu-plugins is loaded on every request automatically, which is why the layer is always in place, and a normal plugin in plugins is loaded by the plugin system, which means it can be deactivated, which is a failure mode worth knowing about for a component that the site depends on for correctness.

The db-error.php drop-in is the safety net. Without the layer, a database connection failure during a request produces a WordPress error page, and this file gives the site owner a way to surface a fatal database error to users in a controlled way.

## Datasets: the idea that removes the ambiguity

The configuration model has two concepts, and the second one is where the design shows.

A database is a server connection with a set of capabilities and priorities, defined by host, user, password, name, and whether it is readable and writable, and which dataset it contains. A dataset is a group of tables that live in the same database.

The documentation explains why the term exists. There may be similarly named databases containing different tables on different servers, and there may be many replicas of a database on different servers, and the term dataset removes any ambiguity. A dataset should be considered as a group of tables that can be mirrored on many servers.

That is a small vocabulary with a large effect on the routing. Once tables belong to a dataset, a query can be routed by table name, and the replicas of that dataset are the candidates. Without the concept you would have to map table names to servers directly, and then a replica of a table on a second server is a different thing from a replica of a table on a first server, and the configuration gets combinatorial.

Queries are distributed to appropriate servers by mapping table names to datasets, and there are two ways to do the mapping. You can list the exact table names in a dataset with add_table, or you can register one or more callback functions that translate table names to datasets.

The callback route is the one that scales with a WordPress site, and the documented example shows why. The multisite blog tables are named with a pattern of the base prefix followed by the blog id and a trailing separator character, so a single callback that matches that pattern against the current table assigns every blog's tables to the blog dataset:

```php
$wpdb->add_callback( 'my_db_callback' );

// Multisite blog tables are "{$base_prefix}{$blog_id}_*"
function my_db_callback( $query, $wpdb ) {
    if ( preg_match("/^{$wpdb->base_prefix}\d+_/i", $wpdb->table) ) {
        return 'blog';
    }
}
```

The callback group defaults to dataset, and callbacks in that group are called with two arguments and are expected to compute a dataset or return null. Callbacks are executed in the order in which they are registered until one of them returns something other than null.

There is one sharp edge in that contract, and the documentation states it plainly: anything evaluating to false will cause the query to be aborted. A callback that returns false rather than null is not a no-match, it is a stop. For more complex setups a callback may return an array instead, which LudicrousDB extracts, and that array should include a dataset value corresponding to a database added with add_database, and may also include a server value used to overwrite the connection parameter.

## The parameters that decide routing, including the lag threshold

The add_database function takes an associative array, and the parameters are documented with their defaults.

```php
host          (required) Hostname with optional :port. Default port is 3306.
user          (required) MySQL user name.
password      (required) MySQL user password.
name          (required) MySQL database name.
read          (optional) Whether server is readable. Default is 1 (readable).
                         Also used to assign preference. See "Network topology".
write         (optional) Whether server is writable. Default is 1 (writable).
                         Also used to assign preference in multi-primary mode.
dataset       (optional) Name of dataset. Default is 'global'.
timeout       (optional) Seconds to wait for TCP responsiveness. Default is 0.2
lag_threshold (optional) The minimum lag on a replica in seconds before we consider it lagged.
                         Set null to disable. When not set, the value of
                         $wpdb->database_defaults['lag_threshold'] is used.
```

The first sample configuration shows the minimum, which adds the server defined in wp-config.php as a read and write server for the global dataset, since every table is in global by default:

```php
$wpdb->add_database( array(
    'host'     => DB_HOST,     // If port is other than 3306, use host:port.
    'user'     => DB_USER,
    'password' => DB_PASSWORD,
    'name'     => DB_NAME,
) );
```

The second sample adds the same server again as a replica, with the last three parameters set to the defaults but shown for clarity, including a timeout of 0.2 seconds and a dataset of global.

Two of those parameters deserve attention. The timeout of 0.2 seconds for TCP responsiveness is short, and it is the right order of magnitude for a router that must fail over quickly: a server that does not answer a TCP check in 200 milliseconds is treated as unavailable, which keeps a failed replica from adding half a second to every request. The lag_threshold is the more interesting one. It is the minimum replication lag on a replica, in seconds, before the replica is considered lagged, and it can be set to null to disable the check. A lagged replica that still answers queries will serve stale rows, and a site that shows a comment count that does not match the comment page is the symptom of that configuration being wrong. Setting the threshold is the mechanism for taking a lagging replica out of rotation, and null is the way to say that you would rather have stale reads than a smaller replica pool.

The read and write flags do double duty. Besides saying whether a server can be used for reads or writes, they are used to assign preference, including in multi-primary mode, where several servers are writable and the router has to pick one.

## Charsets, and the read-back query worth avoiding

The most recent release changed something small that is worth understanding, because it is the kind of detail that shows up as an extra query in a slow request profile.

Since 5.3.1, a cached connection adopts later changes to $wpdb->charset or $wpdb->collate the next time it is used, and reuse with unchanged settings sends no charset query at all. Before that, a connection was created with whatever charset was set at the time and kept it.

The other half is the empty charset case. An empty effective charset keeps the server default and validates its session charsets once when the link is configured, rejecting unsupported defaults. The documented advice is to set a supported, non-empty effective charset to avoid that read-back query.

The reason this matters is that a read-back query is a query on every new connection, and with a pool of servers and short-lived connections that is a measurable cost on a busy site. The wiki page on character sets and collations explains the precedence of constants and db-config.php assignments, so the configuration side is documented in more detail than the readme goes into.

The db-config.php drop-in is the third piece of the installation and the one that carries the environment configuration, and it is the file whose presence makes the other two work as a set rather than as three separate overrides. A site that copies db.php but forgets db-config.php gets the bootstrap without the environment, and a site that copies db-config.php without db.php gets configuration for a layer that is not active.

## Where LudicrousDB is the wrong tool

The first limit is that it solves a database throughput problem, and most WordPress performance problems are not that. A site with a slow front page is usually slow because of a plugin, a theme, an uncached query in a loop or an image, and putting a replica router in front of a single slow query makes the site slower. The layer routes between servers; it does not make a query cheaper, and it adds a decision to every connection.

The second is the operational cost. This is a component that changes how every request reaches the database, and it has to be configured correctly in three places, with drop-ins that are easy to delete by accident and a plugin folder whose name has to be exactly right. A site owner who upgrades WordPress, or a host who reinstalls, can end up with a half-installed layer, and the failure mode is not always obvious.

The third is the failover behaviour versus the data. The layer can take a server out of rotation, and it can refuse to send a query to a replica that is too far behind. Neither of those makes the underlying replication safe, and a WordPress site with a misconfigured replica can serve stale content without anything failing loudly. The failure mode of a routing layer is that it works.

The fourth is the fork's own cost. Keeping a distribution of HyperDB means taking on the obligation the project has documented in UPSTREAM.md, reviewing later HyperDB changes against an audited baseline, and merging them. A site that would rather consume upstream directly is better served by HyperDB itself, and the project is honest that HyperDB remains the compatibility source.

And the fifth is the scope of what is documented. The readme covers the installation, the configuration functions, the callback contract and the parameters, and it refers out to a wiki page for character sets and collations and to a network topology section that is not in the file at all. A configuration layer with three sources of truth, constants, db-config.php and PHP code, is the kind of thing where a reader needs the full documentation, and a good part of it lives outside the repository.

## Conclusion

LudicrousDB is worth considering for a WordPress install whose database is the bottleneck, because the routing decisions are explicit and inspectable rather than hidden in a connection pool, because the dataset model lets you split multisite blog tables onto a different server from the global tables, and because lag_threshold lets a lagged replica be taken out of rotation before it serves stale data. It is the wrong tool if your problem is query performance rather than database throughput. Before you install it, check three things: that you understand the drop-in layout, because the plugin folder must be named exactly ludicrousdb and three separate files have to be placed by hand, db.php in wp-content, db-error.php in wp-content and db-config.php in ABSPATH, and getting any of them wrong means the site either ignores the layer or fails on database errors; that you set a non-empty effective charset in the configuration, because an empty one makes the layer read the session charsets back once when the link is configured and reject unsupported defaults; and that you read UPSTREAM.md, since the project is a distribution of HyperDB that keeps upstream as a compatibility source and documents the audited baseline and the rules used to review later HyperDB changes.

## FAQ

### How do I install LudicrousDB in WordPress?

Copy the plugin folder to wp-content/plugins/ludicrousdb/ or wp-content/mu-plugins/ludicrousdb/, with the folder named exactly ludicrousdb, and then copy the three drop-ins: db.php to wp-content/db.php, db-error.php to wp-content/db-error.php and db-config.php to ABSPATH/db-config.php. The documentation notes that unzipping a Download ZIP from GitHub without renaming the extracted folder is the usual way to get the folder name wrong.

### How do I split WordPress multisite tables onto separate servers with LudicrousDB?

Add the servers with add_database, giving the blog server a dataset of blog, and register a callback with add_callback that returns that dataset for tables matching the multisite pattern. The documented example matches the base prefix followed by the blog id and a trailing separator character, and callbacks in the dataset group run in registration order until one returns something other than null.

### What does the lag_threshold setting do in LudicrousDB?

It is the minimum lag on a replica, in seconds, before the replica is considered lagged, and setting it to null disables the check. When it is not set, the value of $wpdb->database_defaults['lag_threshold'] is used. It lets a replica that is too far behind be taken out of rotation instead of serving stale rows.

## Sources

- [Issues](https://github.com/stuttter/ludicrousdb/issues)
- [License: GPL-2.0](https://github.com/stuttter/ludicrousdb/blob/master/LICENSE)
- [README](https://github.com/stuttter/ludicrousdb/blob/master/README.md)
- [Releases](https://github.com/stuttter/ludicrousdb/releases)
- [stuttter/ludicrousdb on GitHub](https://github.com/stuttter/ludicrousdb)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/stuttter-ludicrousdb
