The problem was caused by the Execution setting of the top-level VI being set to reentrant. I set it to non-reentrant.
The key was using
netstat -a
I saw that the application had multiple ESTABLISHED connections where only one should be possible.
Along the way I cleaned up my code a bit, but the root cause was multiple instances of the application running on the cRIO. If the startup vi is reentrant then you get an instance when the hardware starts or restarts, then you get another instance when you fire up the web-based front panel. Instead of the panel connecting to the running program another one is started.
When running from the development environment this does not happen. The development environment enforces single execution on the top-level vi.
A shout-out to the NI software people: A little warning: "Multiple instances of the program may run because the top-level VI is reentrant" when the build starts, plus adding to the panel documentation something like "Connecting a web-based remote panel to a reentrant top-level VI will invoke a second copy of the top-level VI" would have saved me a day...
Cheers,
Bill