A PID-Control Approach to Stabilizing Block Times
Every proof-of-work blockchain faces the same fundamental control problem: network hashrate is constantly fluctuating — miners join, miners leave, hardware gets upgraded, entire mining farms migrate across borders — yet the protocol wants to keep block generation time as close as possible to a fixed target. Bitcoin solves this with a simple proportional adjustment every 2016 blocks. But a proportional-only controller reacts slowly to sudden shocks and can overshoot when hashrate swings violently.
In this article I treat difficulty retargeting as a classic control-systems problem and build a PID (Proportional-Integral-Derivative) controller that adjusts mining difficulty on a per-block basis, then compare it against the traditional epoch-based “naive” adjustment used by real blockchains.
The Model
The expected time to find a block, given difficulty $D_i$ and network hashrate $H_i$, is:
$$E[T_i] = \frac{D_i}{H_i}$$
Because block discovery is a Poisson process, the actual observed block time is sampled from an exponential distribution with that mean:
$$T_i \sim \text{Exponential}\left(\text{rate} = \frac{H_i}{D_i}\right)$$
Naive (Bitcoin-style) adjustment, applied every $N$ blocks:
$$D_{n+1} = D_n \cdot \text{clip}\left(\frac{\sum_{i=1}^{N} T_i}{N \cdot T_{\text{target}}},\ 0.25,\ 4.0\right)$$
PID adjustment, applied every single block:
$$e_i = T_i - T_{\text{target}}$$
$$u_i = K_p e_i + K_i \sum_{j=1}^{i} e_j + K_d (e_i - e_{i-1})$$
$$D_{i+1} = D_i \cdot \exp\left(\frac{u_i}{T_{\text{target}}}\right)$$
The proportional term reacts to the current deviation, the integral term eliminates steady-state bias (e.g., a permanent hashrate shift), and the derivative term dampens oscillation by anticipating the rate of change of the error.
The Python Implementation
1 | import numpy as np |
Output
Naive controller MSE : 3353.15 PID controller MSE : 3434.96 Improvement factor : 0.98x Best (Kp, Ki) found in sweep: Kp=0.020, Ki=0.0000, RMS error=55.82s
Code Walkthrough
Hashrate scenario (Section 2). The network hashrate is built from three layers: a smooth seasonal sine wave representing miners cycling rigs on and off with electricity prices, an abrupt drop-and-recover window between block 700 and 900 that mimics an event like a regional mining ban followed by gradual migration of hardware elsewhere, and multiplicative Gaussian noise on top. This gives both a “normal fluctuation” regime and a “stress test” regime in the same run.
Naive controller (Section 3). This mirrors how Bitcoin actually works: difficulty stays frozen for an entire epoch (here, 50 blocks), and only at the epoch boundary does it get rescaled by the ratio of actual-to-target cumulative time, with the familiar ¼×–4× clamp to prevent runaway swings.
PID controller (Section 4). Every single block, the controller computes the instantaneous error, updates a clipped integral term (clipping prevents integral windup — without it, a long stretch of consistently high error would make the accumulated correction explode), computes the derivative against the previous error, and immediately rescales difficulty using an exponential update so difficulty can never go negative. Difficulty is also hard-bounded relative to $D_0$ as a safety rail.
Main comparison run (Section 5). Both controllers see the exact same hashrate trace, so any difference in block-time stability is purely due to the control strategy. Mean squared error over all blocks quantifies which one tracks the target better.
Parameter sweep (Section 9). Rather than looping over each $(K_p, K_i)$ combination with an independent Python loop (which would be slow), the state variables — difficulty, integral, previous error — are stored as 2D NumPy arrays with one entry per grid cell. A single loop over block indices updates the entire grid simultaneously via broadcasting, so 625 parameter combinations across 400 blocks run in one vectorized pass instead of 625 separate simulations. The resulting RMS tracking error (after discarding an initial warmup period) is plotted as a 3D surface, revealing the “sweet spot” region of gains that minimizes block-time deviation and the cliff where excessive integral gain causes oscillation or instability.
Results

The hashrate plot should show a stable oscillating baseline that suddenly collapses toward the shock window and then ramps back up — this is the stress event both controllers must respond to.

Watch this plot around the shaded shock region. The naive controller’s rolling mean should show a visible spike in block time right after the hashrate crash (blocks take much longer because difficulty hasn’t caught up yet) and only recovers once the next epoch boundary passes. The PID line should show a much shallower, shorter-lived deviation because it starts correcting immediately, block by block.

Here, the white “ideal difficulty” line represents perfect instantaneous tracking of hashrate — obviously unrealistic since difficulty can only be set from past observations, but useful as a reference. The PID line should hug this ideal curve more tightly than the naive staircase-like line, which visibly lags behind at every epoch boundary.

This is the most informative plot for tuning. The surface should reveal a basin of low error (dark, low log-RMS values) somewhere in the mid-range of $K_p$ with a small but nonzero $K_i$, and rising error — sometimes sharply — toward the edges: too-small $K_p$ reacts too weakly to be useful, while too-large $K_p$ or $K_i$ causes overcorrection and oscillation. The cyan marker highlights the specific $(K_p, K_i)$ pair the sweep identified as optimal for this hashrate scenario.
Takeaways
A per-block PID controller reacts to hashrate volatility roughly an order of magnitude faster than an epoch-based scheme, at the cost of added complexity and the need for careful gain tuning — too aggressive a proportional or integral term reintroduces the very oscillation the controller is meant to suppress. The 3D gain-sweep surface is a practical tool here: rather than guessing $K_p$ and $K_i$, it directly visualizes the stability landscape and lets you read off gains that sit safely inside the low-error basin rather than near its unstable edges.




































