How frameworks actually do it — the tape. In define-by-run engines (PyTorch, JAX with tracing) every operation on a tensor that requires_grad records a node: the op, references to its input tensors, and a closure that knows the local backward. This linked structure is the autograd graph (the "tape"). Calling .backward() walks it in reverse topological order, running each closure and accumulating into .grad. The graph is rebuilt every forward pass, which is why Python control flow "just works" — the tape simply records whatever branch ran.
What gets cached, and why it's the real cost. The backward closures need forward values: tanh's backward needs its output, a matmul's backward needs both operands, max-pooling needs the argmax indices. So the forward pass keeps those tensors alive — they're pinned in memory from the moment they're produced until the backward pass consumes them. For a deep model the stored activations, not the parameters, dominate memory: training memory scales with depth × batch × activation size. This is why you can infer with a model that won't train on the same GPU — inference frees each activation immediately; training cannot.
Gradient checkpointing — trade compute for memory. If storing every activation is too expensive, checkpoint only a subset (say, one tensor per transformer block) and recompute the rest during the backward pass by re-running that block's forward. Memory drops from O(n) to roughly O(√n) in the number of layers, at the cost of ~one extra forward pass (~33% more compute). This single technique is what lets large models fit at all, and it's a one-line wrapper (torch.utils.checkpoint) in practice.
Where the chain of multiplications breaks. Because the backward pass is a long product of Jacobians, its magnitude is a product of factors. If those factors are consistently < 1, the gradient vanishes before it reaches early layers; if > 1, it explodes. The standard fixes all target this product directly: ReLU (derivative exactly 1 on the positive side), residual connections (y = x + F(x) adds an identity path whose local derivative is 1, a "gradient highway"), normalization (keeps activations — and thus σ′ — in a sane range), and gradient clipping (caps the norm to stop explosions in RNNs/transformers). See Neural Networks for the failure modes and Gradient Descent for what happens to the gradient once backprop hands it off.
Backprop through time (BPTT). Unrolling a recurrent network over T steps turns it into a deep feedforward graph sharing one weight matrix. Backprop then sums that weight's gradient across all T copies — but the same repeated-multiplication problem makes long-range gradients vanish, which is exactly what LSTMs and, later, attention were designed to sidestep. See Recurrent Neural Networks.
Higher-order and implicit gradients. Because the backward pass is itself a differentiable computation graph, you can backprop through it to get second derivatives (create_graph=True) — used in meta-learning (MAML), Hessian-vector products, and some physics-informed models. When a forward "layer" is the solution of an optimization or a fixed-point equation (deep equilibrium models, differentiable optimization), you skip unrolling entirely and get the gradient from the implicit function theorem instead — constant memory regardless of how many inner iterations ran.
A note on biological plausibility. Backprop needs a symmetric backward pass that reuses the forward weights and a global error signal — neither of which the brain obviously has. This "weight transport problem" motivates alternatives like feedback alignment, target propagation, and predictive coding. None matches backprop's efficiency on real tasks yet, but they're an active research thread on how learning might work without it.