Staying up
What shinyctl does when a copy dies, when you deploy a new version, when you change the number of copies, and when you upgrade shinyctl itself.
When a copy dies
Scroll sideways to see the whole timeline.
- The spare takes over at once. The load balancer sends the dead copy's visitors to a warm copy, and
shinyctl-apimakes that copy active within milliseconds. The visitors stay on it. - The dead copy comes back as the spare. It restarts after its backoff and is the newest copy, so it becomes the warm spare: roles don't swap back.
- Backoff: crashes are counted for each copy, and restarts wait 2 seconds, doubling up to 1 minute.
statusshows the copy ascrash-loopinguntil it is up again; its crash count goes back to-once it has stayed up for a minute. restart-policydecides whether it comes back:always(the default) after any exit,on-failureonly after a non-zero exit,nevernot at all. A copy left down on purpose shows asexited, with a line under the table saying why.- Without a spare (
warm-replicas: 0), a request waits up to 15 seconds for the app to come back; after that the visitor gets a page saying the app is restarting, which reloads itself every few seconds. - If
shinyctl-apiis down, the load balancer still puts the spare in, but nothing restarts the dead copy untilshinyctl-apiis back.
After its copy changes, an open Shiny page greys out until it is reloaded. The example app reloads by itself once the app answers again: copy its tags$script into your app's UI. Shiny's own session$allowReconnect("force") is no substitute: with shiny 1.14, a page reconnected to a new R process keeps its old outputs, and its downloads stop working.
Redeploys
restart starts new copies from the manifest as it is now, so new code in app.R takes effect. An apply does the same when it changes how the app starts: env, working-dir, app-group, app-name or entrypoint-file. Both return at once.
Scroll sideways to see the whole timeline.
- New copies first.
replicasnew copies start next to the running ones. The spares start once those have taken over, so at most one extra R process runs with the defaults. - The switch. Once the new copies that serve are ready, new visitors go to them, and the old copies drain: they keep the visitors they have until those sessions end or
drain-timeout(15 minutes by default) passes. An old spare with no visitors stops right away, and an old copy with no sessions stops within about 15 seconds. - Old pages keep working. A page left open on an old copy keeps working on the old version, and reloading it brings the new one. When an old copy stops at the timeout, pages with the example's script reload onto the new version by themselves.
- A broken version never takes over. If the new copies don't start, the old ones keep serving, and
statussays which copy doesn't start and where to look. --nowstops the old copies as soon as the new ones are ready, without waiting for their visitors.- A redeploy during another goes straight to the newest version.
While it goes on, status shows the draining copies and how long they have left:
NAME COPY ROLE PORT PID STATUS READY MEM SESSIONS CRASHES STARTED
hello 1 draining 8765 61429 alive ready 142M 2 - 2026-10-27T10:00:01+02:00
hello 3 active 8765 61501 alive ready 140M 1 - 2026-10-27T11:30:05+02:00
hello 4 warm 8765 61502 alive ready 138M 0 - 2026-10-27T11:30:05+02:00
Memory: 420M in 3 R processes (1 warm, 1 draining), plus 41M for the load balancer.
hello: copy 1 is draining with 2 sessions; it stops when they end, or in 15m at the latest.
Both versions run side by side until the drain ends, so a change to shared data, such as a database schema or files, must suit both.
Scaling
Changing replicas or warm-replicas and applying starts copies, or drains the ones it removes the same way: they get no new visitors, and stop when their sessions end or at drain-timeout. Copies that are starting or dead stop at once, and --now stops them all at once. Copies leave in this order: those with no process, those still starting, then ready ones with the fewest open sessions, a warm copy before an active one when they tie.
Changing port, host, lb-policy, drain-timeout or restart-policy takes effect without restarting the app's processes.
Upgrading shinyctl
Install the new binary (running the installer again does it), then restart shinyctl-api:
systemctl --user restart shinyctl # if you installed the service
# or, if you started it by hand:
pkill -u "$USER" -f '(^|/)shinyctl serve$' && shinyctl serve -d
Scroll sideways to see the whole timeline.
Your apps keep running: the new shinyctl-api adopts them. When the new version comes with a new load balancer, it starts next to the old one, on the same ports, and takes over without refusing a connection. The old one answers the requests it is in the middle of, for up to five seconds, and then stops.
Connections that were open on the old one end, and a Shiny session is one, so a page open at that moment reconnects or reloads once. On Linux, a request that reaches the old one in the very instant it closes can fail once. In the release test on a Raspberry Pi 5, two upgrades under load lost 0 of 473 and 0 of 470 requests.