Ok, I've finally reproduced it. For what it's worth, it's not even the duration of the step, let alone the stage, that the TimingAction is missing - it's <2 seconds total. I was copying the build directory aside every second. One copy, before 12.xml was written (i.e., before the docker pull started), all was fine. Next copy, one second later, 12.xml was there, 12.log was completely empty, and 12.xml didn't have a TimingAction. And one second after that, 12.xml was still the latest, 12.log had some output in it, and 12.xml did have a TimingAction. So the shell step calling docker pull was still running but the node now had a proper TimingAction. While the time-since-epoch duration seems to stick around in the UI after that, that's something in the UI not refreshing it from the API, because a reload of the page gets the right duration at that point.
I'll defer to Sam Van Oort if he's got a better sense of what might be happening here, but I'd guess it's just that there's a tiny tiny tiny window between when the FlowNode is created and first saved and when CpsFlowExecution.notifyListeners gets called, which leads to the actual adding of the TimingAction.
So frankly? I think Blue Ocean should just be a little smarter and either not display a duration at all if it's expecting a TimingAction but gets null, or it should somehow decide what an appropriate analogue for the start time would be. This tiny gap between the FlowNode being initially written and the TimingAction being added is, IMO, entirely reasonable.