Backpropagation Through Time

Imagine training a network to predict the next character in a sentence, one token at a time. The hidden state carries information from every previous character. A single output error at time step 100 might depend on a decision made at time step 3. How do you assign credit — or blame — across a hundred time steps of shared computation? This is the exact problem faced by every Recurrent Neural Network. Backpropagation Through Time (BPTT) is the algorithm that solves it. BPTT is not a new learning rule. It is ordinary backpropagation applied to a clever reformulation of the problem. A recurrent network reuses the same weight matrices at every time step. So BPTT "unrolls" the recurrence into an equivalent deep feedforward computational graph, one layer per time step. It then runs standard reverse-mode differentiation through it. The twist that makes RNNs special — and hard — is that the same weights appear at every layer of this unrolled graph. Their total gradient is therefore a sum of contributions from every time step. The chain rule forces repeated multiplication by the same recurrent weight matrix (or its Jacobian) over and over. That repeated multiplication is the root of one of deep learning's most consequential phenomena: the vanishing and exploding gradient problem. If the recurrent Jacobian systematically shrinks signals, gradients from distant time steps vanish before reaching early layers. The network then becomes effectively blind to long-range dependencies. If it amplifies signals instead, gradients explode into numerical instability. Understanding BPTT precisely matters a great deal. It is not just "backprop on an unrolled graph," but an algorithm with a specific complexity profile and a specific failure mode. This understanding is essential to grasping why plain RNNs struggle with long sequences. It also explains why architectures like LSTM and GRU were invented. This article gives a complete, mechanical account of BPTT. It covers the exact unrolling construction, and pseudocode for forward and backward passes. It also includes a worked numerical trace, and the complexity and correctness argument. Truncated variants used in practice are explained, too. Finally, it details the mathematical anatomy of vanishing and exploding gradients. By the end you should be able to derive the recurrent-weight gradient by hand. You should also be able to explain precisely why long sequences break plain RNNs.

Key takeaways
  • BPTT unrolls an RNN across time into a deep feedforward graph and applies standard backpropagation to it.
  • The same weight matrices are shared at every time step, so their gradients are summed over all time steps.
  • The chain rule through time forces repeated multiplication by the recurrent Jacobian, causing gradients to vanish or explode over long sequences.
  • Time complexity is O(T)O(T) per sequence (linear in sequence length); memory is O(T)O(T) because every hidden state must be cached for the backward pass.
  • Truncated BPTT trades exact long-range gradients for tractable memory and compute by limiting the backward window.
  • The vanishing gradient problem motivates gated architectures — LSTM and GRU — that maintain more stable gradient flow.
The core insight

BPTT is exact backpropagation on the time-unrolled RNN graph, where shared weights receive gradients summed across all time steps, and this repeated chain-rule multiplication through the recurrent Jacobian is precisely what makes gradients vanish or explode over long sequences.

The idea

A Recurrent Neural Network computes a hidden state hth_t from the current input xtx_t and the previous hidden state ht−1h_{t-1}, using the same parameters Whh,Wxh,bhW_{hh}, W_{xh}, b_h at every time step:

ht=ϕ(Whhht−1+Wxhxt+bh),y^t=ψ(Whyht+by)h_t = \phi(W_{hh} h_{t-1} + W_{xh} x_t + b_h), \qquad \hat y_t = \psi(W_{hy} h_t + b_y)

Because hth_t depends on ht−1h_{t-1}, which depends on ht−2h_{t-2}, and so on back to h0h_0, the computation graph is not a simple chain of independent layers — it is a single recurrent cell applied TT times, with parameters tied across every application. Unrolling means writing out this graph explicitly for time steps 1,…,T1, \dots, T as if it were a TT-layer feedforward network, where "layer tt" is the RNN cell instantiated with the actual xtx_t and connected to layer t−1t-1's output. Once unrolled, the graph is an ordinary computational graph with a clear topological order, and reverse-mode automatic differentiation (backpropagation) applies unchanged. The one subtlety — and it is the entire story of BPTT — is parameter tying. In a normal deep feedforward network, layer ℓ\ell has its own weight matrix W(ℓ)W^{(\ell)}, and ∂L/∂W(ℓ)\partial L/\partial W^{(\ell)} only involves that layer. In the unrolled RNN, every layer uses the identical WhhW_{hh}. Backpropagation still computes a local gradient contribution at each time step, but because WhhW_{hh} is one tensor, not TT separate tensors, its total gradient is the sum of the local contributions from every time step:

∂L∂Whh=∑t=1T∂L∂Whh∣(t)\frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^{T} \frac{\partial L}{\partial W_{hh}}\bigg|_{(t)}

where the notation ∣(t)|_{(t)} denotes "the gradient contribution computed using WhhW_{hh} as it appears at time step tt's copy in the unrolled graph." This sum-over-time is the defining structural feature of BPTT, distinguishing it from vanilla backpropagation in a non-recurrent network. The other defining feature is what happens to the hidden-state gradient (often called δt=∂L/∂ht\delta_t = \partial L/\partial h_t) as it flows backward through time. Because ht=ϕ(Whhht−1+… )h_t = \phi(W_{hh}h_{t-1} + \dots), the chain rule gives

∂ht∂ht−1=diag⁡(ϕ′(zt)) Whh,\frac{\partial h_t}{\partial h_{t-1}} = \operatorname{diag}(\phi'(z_t))\, W_{hh},

so propagating error from time step TT back to time step 11 multiplies by this Jacobian T−1T-1 times. Repeated multiplication by the same (or similar) matrix is exactly the setting in which linear algebra tells us eigenvalues dominate: if the spectral radius of diag⁡(ϕ′)Whh\operatorname{diag}(\phi')W_{hh} is consistently below 1, the product shrinks geometrically (vanishing gradients); if consistently above 1, it grows geometrically (exploding gradients). This is developed rigorously in the Complexity & Correctness section.

How it works

BPTT proceeds in three conceptual phases: unroll, forward pass with caching, backward pass with gradient accumulation. 1. Unroll. Fix a sequence length TT (or a truncation window, see below). Instantiate the recurrent cell TT times, one per time step, all sharing the same parameters Wxh,Whh,Why,bh,byW_{xh}, W_{hh}, W_{hy}, b_h, b_y. 2. Forward pass. Run the RNN forward from h0h_0 (usually zero) to hTh_T, computing and caching every intermediate quantity needed for the backward pass: each pre-activation zt=Whhht−1+Wxhxt+bhz_t = W_{hh}h_{t-1} + W_{xh}x_t + b_h, each hidden state hth_t, each output y^t\hat y_t, and each per-step loss LtL_t (e.g. cross-entropy against target yty_t). The total loss is typically L=∑t=1TLtL = \sum_{t=1}^T L_t. 3. Backward pass. Walk time backward from t=Tt = T down to t=1t = 1. At each step, combine the gradient flowing in from the local output loss with the gradient flowing in from the future hidden state (i.e., from t+1t+1), producing δt=∂L/∂ht\delta_t = \partial L / \partial h_t. Use δt\delta_t to accumulate gradients into the shared parameter matrices, then propagate δt\delta_t further backward into δt−1\delta_{t-1}. The crucial bookkeeping variable is the carried hidden-state gradient, which plays the same role that "gradient flowing into a layer's input" plays in ordinary backprop, except here it also folds in the direct loss gradient at that time step. ```text PSEUDOCODE: Backpropagation Through Time (single sequence, length T) Inputs: x[1..T] # input sequence y[1..T] # target sequence W_xh, W_hh, W_hy # shared weight matrices b_h, b_y # shared biases h0 # initial hidden state (often zero vector)

# ---------- FORWARD PASS (cache everything needed for backward) ---------- h[0] = h0 for t = 1 to T: z[t] = W_xh @ x[t] + W_hh @ h[t-1] + b_h h[t] = phi(z[t]) # e.g. tanh o[t] = W_hy @ h[t] + b_y yhat[t] = psi(o[t]) # e.g. softmax L[t] = loss(yhat[t], y[t]) L_total = sum(L[t] for t in 1..T)

# ---------- BACKWARD PASS ----------

# Gradient accumulators for shared parameters, all initialized to zero dW_xh = 0; dW_hh = 0; dW_hy = 0 db_h = 0; db_y = 0 dh_next = 0 # gradient flowing back from step t+1 into h[t] for t = T downto 1: do_t = d(loss)/d(o[t]) # local output gradient, e.g. yhat[t]-y[t] dW_hy += do_t @ h[t].T db_y += do_t dh_t = W_hy.T @ do_t + dh_next # loss gradient + future-time gradient dz_t = dh_t * phi_prime(z[t]) # elementwise, backprop through phi dW_xh += dz_t @ x[t].T dW_hh += dz_t @ h[t-1].T db_h += dz_t dh_next = W_hh.T @ dz_t # this becomes "dh_next" for step t-1 return dW_xh, dW_hh, dW_hy, db_h, db_y ``` Two lines deserve emphasis because they encode the two structural facts from the previous section. The line `dW_hh += dz_t @ h[t-1].T` (rather than `=`) implements gradient summation over shared parameters. The line `dh_t = W_hy.T @ do_t + dh_next` implements the merging of local loss gradient and carried-forward-in-time gradient — this addition is exactly the multivariate chain rule applied to a node (hth_t) that has two downstream consumers: the output y^t\hat y_t and the next hidden state ht+1h_{t+1}. After computing gradients, an ordinary optimizer step (SGD, Adam, etc.) updates Wxh,Whh,Why,bh,byW_{xh}, W_{hh}, W_{hy}, b_h, b_y using the accumulated gradients, exactly as in feedforward backprop.

Figure
The unrolled RNN as a feedforward graph: shared weights, summed gradients
x₁ x₂ x₃ x₄ h₁ h₂ h₃ h₄ h₀ ŷ₁ ŷ₂ ŷ₃ ŷ₄ W_hh W_hh W_hh δₜ flows backward, each hop multiplies by Wₕₕᵀ Every W_hh edge is the SAME matrix — gradients from all 3 recurrent edges sum into one ∂L/∂W_hh.

Truncated BPTT. For long or streaming sequences, caching all TT hidden states and backpropagating through all of them is often infeasible. Truncated BPTT (TBPTT) runs the forward pass continuously but only backpropagates through the last kk time steps (a fixed window), detaching (stopping gradient flow at) the boundary. This trades exactness — gradients no longer reflect dependencies older than kk steps — for bounded memory O(k)O(k) and bounded per-step compute, which is essential for training on very long or infinite streams. ```text PSEUDOCODE: Truncated BPTT with window k, chunk size k (common variant: k1 = k2 = k) h = h0 for chunk_start = 1, 1+k, 1+2k, ... up to T: chunk = x[chunk_start .. chunk_start+k-1] h = h.detach() # cut gradient flow into earlier chunks run forward pass over chunk, starting from h, caching intermediates compute chunk loss backward pass over just this chunk (as in full BPTT pseudocode above) accumulate/apply parameter gradients h = final hidden state of this chunk # carried forward, but gradient-detached ```

Complexity & correctness

Time complexity. The forward pass performs O(1)O(1) matrix–vector work per time step (dominated by Whhht−1W_{hh}h_{t-1}, WxhxtW_{xh}x_t, and WhyhtW_{hy}h_t, each costing O(d2)O(d^2) or O(d⋅∣x∣)O(d \cdot |x|) for hidden size dd), so the forward pass over a length-TT sequence is O(Td2)O(T d^2). The backward pass performs the same amount of matrix work per time step (each line in the backward loop is one matrix–vector product), so it is also O(Td2)O(T d^2). Total: Θ(Td2)\Theta(T d^2) time, i.e. linear in sequence length, same asymptotic order as the forward pass — this is the general fact that reverse-mode autodiff costs a small constant factor (roughly 2–3×) times the forward pass, regardless of graph shape.

Space complexity. The forward pass must cache every zt,ht,otz_t, h_t, o_t (and whatever the elementwise nonlinearity needs to compute its local derivative) because the backward pass visits time steps in reverse order and needs these values. This gives Θ(Td)\Theta(T d) memory for full BPTT — memory that grows linearly with sequence length, which is the practical reason long sequences are expensive to train on and the reason truncated BPTT exists (it caps memory at Θ(kd)\Theta(k d) regardless of total stream length).

Correctness argument. BPTT's correctness reduces entirely to two facts, both standard in reverse-mode differentiation: 1. Unrolling is a faithful reformulation. The unrolled graph, with TT copies of the cell connected in a chain and tied weights, computes exactly the same function of (x1,…,xT,h0,W)(x_1,\dots,x_T, h_0, W) as the original recurrence — it is the same computation written out explicitly rather than expressed with a self-reference. Therefore any correct differentiation procedure on the unrolled graph yields the correct gradient of the original recurrent function. 2. The multivariate chain rule handles shared parameters and fan-out correctly. When a single scalar (or tensor) parameter feeds into multiple downstream computations — here, WhhW_{hh} feeding into z1,z2,…,zTz_1, z_2, \dots, z_T, and hth_t feeding into both y^t\hat y_t and ht+1h_{t+1} — the total derivative is the sum of the derivatives along each path, by the multivariable chain rule:

∂L∂Whh=∑t=1T∂L∂zt∂zt∂Whh,∂L∂ht=∂Lt∂ht∣direct+∂L∂ht+1∂ht+1∂ht.\frac{\partial L}{\partial W_{hh}} = \sum_{t=1}^T \frac{\partial L}{\partial z_t}\frac{\partial z_t}{\partial W_{hh}}, \qquad \frac{\partial L}{\partial h_t} = \frac{\partial L_t}{\partial h_t}\bigg|_{\text{direct}} + \frac{\partial L}{\partial h_{t+1}}\frac{\partial h_{t+1}}{\partial h_t}.

The pseudocode's `+=` accumulation and the `dh_t = ... + dh_next` line are literal implementations of these two identities. Since reverse-mode autodiff is known to compute exact gradients on any directed acyclic computational graph (a standard result inherited from ordinary backpropagation), and the unrolled RNN graph is exactly such a DAG, BPTT computes the exact gradient of the sequence loss with respect to every shared parameter.

Why gradients vanish or explode. Unrolling the recursive Jacobian relation gives, for the gradient flowing from time TT back to time tt,

∂hT∂ht=∏k=t+1T∂hk∂hk−1=∏k=t+1Tdiag⁡(ϕ′(zk)) Whh.\frac{\partial h_T}{\partial h_t} = \prod_{k=t+1}^{T} \frac{\partial h_k}{\partial h_{k-1}} = \prod_{k=t+1}^{T} \operatorname{diag}(\phi'(z_k))\, W_{hh}.

This is a product of T−tT - t matrices. If WhhW_{hh} were a scalar ww and ϕ′\phi' were bounded by a constant cc, this product behaves like (cw)T−t(cw)^{T-t}: for ∣cw∣<1|cw| < 1 it shrinks exponentially in T−tT - t (vanishing gradient — early time steps get essentially zero learning signal from distant losses), and for ∣cw∣>1|cw| > 1 it grows exponentially (exploding gradient — parameter updates become enormous and unstable, or overflow to NaN). For matrices, the rigorous statement uses the spectral radius ρ\rho of diag⁡(ϕ′(z))Whh\operatorname{diag}(\phi'(z))W_{hh} evaluated along the trajectory: if ρ<1\rho < 1 consistently, the largest singular values of the product decay geometrically in T−tT-t; if ρ>1\rho > 1, they grow geometrically. With tanh⁡\tanh or sigmoid activations, ϕ′(z)∈(0,1]\phi'(z) \in (0, 1] (and is often much less than 1 away from the origin), which biases the product toward vanishing — this is why plain ("vanilla") RNNs empirically struggle to learn dependencies spanning more than a few dozen time steps, a phenomenon documented well before gated architectures existed.

Tradeoffs, honestly stated.

  • Full BPTT is exact but O(T)O(T) in both time and memory — infeasible for very long sequences (e.g. streaming audio, book-length text) without truncation.
  • Truncated BPTT bounds memory and compute but introduces bias: gradients cannot represent dependencies longer than the truncation window, so the network is structurally prevented from learning them, no matter how much data it sees.
  • Gradient clipping (rescaling the gradient norm if it exceeds a threshold) is a cheap, effective mitigation for exploding gradients, but does nothing for vanishing gradients — it only bounds the magnitude of instability, not the loss of long-range signal. This asymmetry is precisely why architectural fixes were needed.

Worked example

Consider a toy scalar RNN (hidden size 1, so all "matrices" are scalars) with ϕ=tanh⁡\phi = \tanh, no biases, mapped through a linear readout y^t=whyht\hat y_t = w_{hy}h_t, trained with squared-error loss Lt=12(y^t−yt)2L_t = \tfrac12(\hat y_t - y_t)^2, over T=3T = 3 steps. Let

wxh=0.5,whh=1.2,why=1.0,h0=0.w_{xh} = 0.5,\quad w_{hh} = 1.2,\quad w_{hy} = 1.0,\quad h_0 = 0.

Inputs x=(1.0, 1.0, 1.0)x = (1.0,\ 1.0,\ 1.0), targets y=(0.5, 0.5, 0.5)y = (0.5,\ 0.5,\ 0.5).

Forward pass.

ttzt=wxhxt+whhht−1z_t = w_{xh}x_t + w_{hh}h_{t-1}ht=tanh⁡(zt)h_t=\tanh(z_t)y^t=whyht\hat y_t = w_{hy}h_tLt=12(y^t−yt)2L_t=\tfrac12(\hat y_t-y_t)^2
10.5(1)+1.2(0)=0.5000.5(1)+1.2(0)=0.500tanh⁡(0.5)=0.4621\tanh(0.5)=0.46210.46210.462112(−0.0379)2=0.00072\tfrac12(-0.0379)^2=0.00072
20.5(1)+1.2(0.4621)=1.05450.5(1)+1.2(0.4621)=1.0545tanh⁡(1.0545)=0.7834\tanh(1.0545)=0.78340.78340.783412(0.2834)2=0.04016\tfrac12(0.2834)^2=0.04016
30.5(1)+1.2(0.7834)=1.44010.5(1)+1.2(0.7834)=1.4401tanh⁡(1.4401)=0.8938\tanh(1.4401)=0.89380.89380.893812(0.3938)2=0.07755\tfrac12(0.3938)^2=0.07755

Backward pass. Use ϕ′(zt)=1−ht2\phi'(z_t) = 1 - h_t^2: ϕ′(z1)=1−0.2135=0.7865\phi'(z_1)=1-0.2135=0.7865, ϕ′(z2)=1−0.6137=0.3863\phi'(z_2)=1-0.6137=0.3863, ϕ′(z3)=1−0.7989=0.2011\phi'(z_3)=1-0.7989=0.2011. Start with dhnext=0dh_{\text{next}} = 0 and step t=3→1t = 3 \to 1:

  • t = 3: do3=y^3−y3=0.3938do_3 = \hat y_3 - y_3 = 0.3938.   dwhy+=do3⋅h3=0.3938(0.8938)=0.3520\; dw_{hy} \mathrel{+}= do_3 \cdot h_3 = 0.3938(0.8938)=0.3520.

dh3=why⋅do3+dhnext=0.3938+0=0.3938dh_3 = w_{hy}\cdot do_3 + dh_{\text{next}} = 0.3938 + 0 = 0.3938. dz3=dh3⋅ϕ′(z3)=0.3938(0.2011)=0.0792dz_3 = dh_3\cdot \phi'(z_3) = 0.3938(0.2011)=0.0792. dwxh+=dz3⋅x3=0.0792dw_{xh}\mathrel{+}=dz_3\cdot x_3 = 0.0792;   dwhh+=dz3⋅h2=0.0792(0.7834)=0.0620\;dw_{hh}\mathrel{+}=dz_3\cdot h_2 = 0.0792(0.7834)=0.0620. dhnext←whh⋅dz3=1.2(0.0792)=0.0951dh_{\text{next}} \leftarrow w_{hh}\cdot dz_3 = 1.2(0.0792)=0.0951 (passed to t=2t=2).

  • t = 2: do2=0.2834do_2 = 0.2834.   dwhy+=0.2834(0.7834)=0.2220\;dw_{hy}\mathrel{+}=0.2834(0.7834)=0.2220 → running total 0.3520+0.2220=0.57400.3520+0.2220=0.5740.

dh2=why⋅do2+dhnext=0.2834+0.0951=0.3785dh_2 = w_{hy}\cdot do_2 + dh_{\text{next}} = 0.2834 + 0.0951 = 0.3785. dz2=0.3785(0.3863)=0.1462dz_2 = 0.3785(0.3863)=0.1462. dwxh+=0.1462dw_{xh}\mathrel{+}=0.1462 → running total 0.0792+0.1462=0.22540.0792+0.1462=0.2254;   dwhh+=0.1462(0.4621)=0.0676\;dw_{hh}\mathrel{+}=0.1462(0.4621)=0.0676 → running total 0.0620+0.0676=0.12960.0620+0.0676=0.1296. dhnext←1.2(0.1462)=0.1754dh_{\text{next}} \leftarrow 1.2(0.1462)=0.1754 (passed to t=1t=1).

  • t = 1: do1=−0.0379do_1 = -0.0379.   dwhy+=−0.0379(0.4621)=−0.0175\;dw_{hy}\mathrel{+}=-0.0379(0.4621)=-0.0175 → running total 0.5740−0.0175=0.55650.5740-0.0175=0.5565.

dh1=−0.0379+0.1754=0.1375dh_1 = -0.0379 + 0.1754 = 0.1375. dz1=0.1375(0.7865)=0.1082dz_1 = 0.1375(0.7865)=0.1082. dwxh+=0.1082dw_{xh}\mathrel{+}=0.1082 → final dwxh=0.2254+0.1082=0.3336dw_{xh}=0.2254+0.1082=0.3336;   dwhh+=0.1082(h0)=0.1082(0)=0\;dw_{hh}\mathrel{+}=0.1082(h_0)=0.1082(0)=0 → final dwhh=0.1296+0=0.1296dw_{hh}=0.1296+0=0.1296.

Final accumulated gradients: ∂L/∂wxh≈0.334\partial L/\partial w_{xh} \approx 0.334, ∂L/∂whh≈0.130\partial L/\partial w_{hh}\approx 0.130, ∂L/∂why≈0.557\partial L/\partial w_{hy}\approx 0.557. Notice concretely how the recurrent-weight gradient dwhhdw_{hh} is a sum of three terms (0.0620+0.0676+00.0620 + 0.0676 + 0), one per time step, exactly as the theory predicted — and notice how dhnextdh_{\text{next}} carries information from t=3t=3 back to t=1t=1, shrinking somewhat at each hop because ϕ′(zt)<1\phi'(z_t) < 1 and getting rescaled by whh=1.2w_{hh}=1.2; with a smaller whhw_{hh} or deeper unrolling this shrinkage compounds into the vanishing-gradient regime described above.

Common pitfalls & traps

PitfallWhy it happensFix
Overwriting instead of accumulating gradients for shared weightsTreating WhhW_{hh} like a per-layer parameter (`=` instead of `+=`)Always accumulate (`+=`) gradients for every tied parameter across all time steps
Forgetting to add dhnextdh_{\text{next}} to the local output gradientMissing the "fan-out" chain rule term from hth_t feeding both y^t\hat y_t and ht+1h_{t+1}Explicitly sum both contributions before backpropagating through ϕ\phi
Not detaching hidden state between TBPTT chunksBelieving gradient must "keep flowing" for correctnessDetach at chunk boundaries by design — that is the whole point of truncation
Caching too little in the forward passDiscarding ztz_t or ht−1h_{t-1} to save memory, then needing them in backward passCache every quantity the backward pass reads, or recompute via gradient checkpointing
Blaming exploding gradients solely on learning rateInstability actually comes from repeated multiplication by WhhW_{hh} with spectral radius >1>1Apply gradient clipping and/or inspect WhhW_{hh}'s spectral radius, not just the learning rate
Assuming truncated BPTT is "basically exact"Believing a large enough window recovers full gradientsRecognize TBPTT introduces a structural bias — dependencies beyond the window are provably invisible to gradients

Pioneers & history

The unrolling idea and the general reverse-mode differentiation machinery behind BPTT trace back to the broader development of backpropagation for feedforward networks, but its specific application to recurrent networks — treating time as depth — was articulated explicitly in the mid-1980s and formalized soon after. Paul Werbos described the "backpropagation through time" formulation in the context of dynamical systems, and the technique was subsequently popularized within the connectionist community alongside the broader adoption of backpropagation itself.

Origins · 1 minPaul J. Werbos and how this was discovered
PJ
Paul J. Werbos
1947– · American scientist who formalized backpropagation through time for dynamic and recurrent systems

Werbos developed the mathematical framework for propagating error gradients through time-unrolled dynamical systems, extending the chain-rule-based gradient computation that underlies all of modern backpropagation into settings with temporal recurrence — the exact operation this article calls BPTT.

History
1974
— Werbos's dissertation introduces gradient-based training ideas for dynamical/recurrent systems, precursors to BPTT.
1986
— Rumelhart, Hinton, and Williams popularize backpropagation for feedforward networks, establishing the reverse-mode differentiation machinery BPTT reuses.
1990
— Werbos formally names and describes "Backpropagation Through Time" for recurrent networks.
1994
— Bengio, Simard, and Frasconi analyze the vanishing/exploding gradient problem in training recurrent networks with gradient descent.
1997
— Hochreiter and Schmidhuber introduce the LSTM architecture specifically to combat vanishing gradients identified through BPTT analysis.

Where this matters

BPTT is the training engine behind every sequence model built on recurrence, and its failure modes directly motivated the architectures that dominate sequence modeling today. Language models built on LSTM and GRU cells — used historically in machine translation, speech recognition, and text generation before the rise of attention-based models — are trained with BPTT (or its truncated variant) at their core; the gating mechanisms in those architectures exist specifically to create more stable gradient paths through time, directly addressing the vanishing-gradient analysis derived above. Speech recognition systems that process audio streams frame-by-frame use truncated BPTT to keep training tractable over very long recordings. Time-series forecasting systems in finance and demand prediction, sequence tagging in NLP pipelines, and any online/streaming RNN deployment (e.g. on-device keyboard prediction) rely on the exact mechanics — accumulation over shared weights, caching for the backward pass, and truncation windows — described here. Even in the transformer era, understanding BPTT remains foundational: it is the conceptual bridge that explains why attention mechanisms and gated recurrent units were considered necessary advances, and it remains the training algorithm for any modern architecture that retains explicit recurrent state (e.g. certain streaming or state-space sequence models).

Ask the tutor: How exactly do LSTM gates modify the recurrent Jacobian to prevent gradients from vanishing the way they do in a vanilla RNN?

Is BPTT a different algorithm from backpropagation?

No — BPTT is standard backpropagation applied to the unrolled computational graph of a recurrent network. There is no separate mathematical machinery; reverse-mode automatic differentiation on a DAG works identically whether that DAG came from a feedforward network or from unrolling a recurrence over time. The only genuinely new bookkeeping is that BPTT must (1) sum gradient contributions for parameters that are shared/tied across every unrolled time step, and (2) merge, at each hidden state, the gradient arriving from that time step's own output with the gradient arriving from the next time step. Both are direct, unremarkable applications of the multivariate chain rule, not new theory.

Why do gradients vanish specifically in RNNs but not (as severely) in typical feedforward networks?

Feedforward networks generally have independent weights per layer, so there's no forced repeated multiplication by the same matrix. In an RNN, backpropagating error from time step TT to time step tt requires multiplying by the same Jacobian diag⁡(ϕ′)Whh\operatorname{diag}(\phi')W_{hh} roughly T−tT - t times — a genuine matrix power. Very deep feedforward networks can suffer an analogous problem (which is one reason residual connections were introduced), but plain RNNs applied to long sequences hit this issue especially severely because sequence lengths (hundreds or thousands of steps) routinely exceed typical feedforward network depths (tens to low hundreds of layers).

What exactly does truncated BPTT sacrifice compared to full BPTT?

Full BPTT computes the exact gradient of the total sequence loss with respect to every parameter, using dependencies across the entire sequence. Truncated BPTT only backpropagates through the last kk time steps of the current chunk, then detaches — so any dependency an output at time tt has on inputs more than kk steps earlier receives literally zero gradient signal, not just a small one. This is a structural, not statistical, limitation: no amount of additional training data changes it. In exchange, TBPTT bounds memory and compute to O(k)O(k) regardless of total stream length, which is what makes training on very long or effectively infinite sequences (like continuous audio or logging streams) practical at all.

How does BPTT relate to the vanishing gradient problem in a precise mathematical sense?

The hidden-state-to-hidden-state gradient ∂hT/∂ht\partial h_T/\partial h_t equals a product of T−tT-t Jacobians, each of the form diag⁡(ϕ′(zk))Whh\operatorname{diag}(\phi'(z_k)) W_{hh}. Products of many matrices are governed by the spectral radius of the matrices involved: values below 1 cause exponential shrinkage in the product's norm as the number of factors grows, and values above 1 cause exponential growth. Since tanh⁡\tanh and sigmoid derivatives are bounded above by 1 (and often much smaller away from zero), the product is biased toward shrinking, which is the formal statement of "vanishing gradients." This is a property of the architecture and activation choice, not of any particular training run going wrong.

Does BPTT still matter now that attention-based models are common?

Yes, for two reasons. First, many production and research systems still use explicit recurrent state — streaming models, certain state-space sequence models, and on-device systems where constant-memory-per-step inference matters — and these are trained with BPTT or truncated BPTT. Second, and more importantly for learning the field, BPTT's failure analysis (vanishing/exploding gradients through repeated Jacobian multiplication) is the historical and conceptual reason gated units and, later, attention mechanisms were developed. Understanding BPTT is what makes the design motivations behind LSTM, GRU, and even residual/attention-based alternatives legible rather than arbitrary.

Why must the forward pass cache every hidden state instead of recomputing them during the backward pass?

Technically, recomputation is possible — this is exactly the idea behind gradient checkpointing, which trades compute for memory. But without checkpointing, the backward pass needs ht−1h_{t-1}, ztz_t, and other forward quantities at every time step because the local Jacobians (ϕ′(zt)\phi'(z_t), and the outer products used to accumulate dWhhdW_{hh}, dWxhdW_{xh}) are functions of those specific values. If they aren't cached, they would need to be recomputed by rerunning the forward pass from scratch up to each time step, which is far more expensive than simply storing them, hence the standard O(Td)O(Td) memory cost of full BPTT.

What's a common way to detect that vanishing gradients are hurting training, in practice?

A useful diagnostic is a synthetic long-range dependency task — for example, requiring the network to copy or recall a signal presented many time steps earlier — since ordinary loss curves on real data can plateau in ways indistinguishable from garden-variety optimization difficulty. Another diagnostic is directly inspecting the norm of ∂L/∂ht\partial L/\partial h_t as a function of tt across a long sequence: if it decays sharply for small tt (far in the past), long-range gradients are vanishing. Comparing performance against a gated architecture (LSTM or GRU) trained identically is also a strong practical signal, since those architectures are specifically designed to mitigate this failure mode.

Before this

Where this leads