Skip to content

24-7 Today

Menu
  • Home
  • Ads Guide
  • Blogging
  • Sec Tips
  • SEO Strategies
Menu

Affordable Shiny app hosting for 500 concurrent users

Posted on August 19, 2025 by 24-7

[This article was first published on Open Analytics, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)


Want to share your content on R-bloggers? click here if you have a blog, or here if you don’t.

Cloud providers are no longer just offering traditional x86-based servers,
ARM-based servers are now becoming a serious alternative. And for good reason:
they’re often far more cost-effective and power-efficient than their x86
counterparts. For instance, we found a provider offering a server with 16
virtual cores and 32 GB of RAM for just €30 per month. To be realistic, most
providers charge between €100 and €200 per month for such a server. These
more expensive servers usually have less customers on a single server, improving
the real life performance. In this post, we’ll put one of these budget-friendly
ARM servers to the test. Of course, similar results can be achieved with an
Intel or AMD server. The goal is to set up a production-grade ShinyProxy
environment, designed to host a single public app, and see how many concurrent
users it can realistically handle. (And yes, the title probably gives a hint
already!).

Although this post focuses on a specific use case, the same principles also
apply when running multiple apps with authentication.

Server selection

The first step is to pick a cloud provider and order a server. Choose a
datacenter that’s located close to your end users to minimize latency and
improve performance. Most providers also allow you to resize your server later
by adding more CPU or RAM, so you can start small and scale up as demand grows.
Make sure the server comes with a public IP address, since there is no need for
a load balancer in this setup.

When it comes to the operating system, you can use any Linux distribution you
prefer. In this tutorial, we’ll use Ubuntu 24.04. Once your server is
provisioned, the next step is to connect via SSH. Usually your cloud provider
provides documentation that explains how to do this.

As a final prerequisite, you’ll need a domain name. This makes it easier to
access your server using a human-friendly address and is also necessary for
configuring TLS. You can use either a root domain (e.g. example.com) or a
subdomain (e.g. shinyproxy.example.com). In your domain’s DNS settings, create
an A record that points the domain or subdomain to your server’s public IP
address.

Installation

With the server up and running, the next step is to install the ShinyProxy
Operator. This tool takes care of the ShinyProxy installation process as well as
all the required side components, so you don’t have to manage them manually. For
detailed instructions, follow the
official documentation.
For the best setup, make sure to enable TLS and the monitoring stack. At this
point you should be able to login and start the demo app.

Inspecting the resource consumption of the app

With ShinyProxy deployed, you can deploy your app. Once again, the
documentation contains
everything you need to know. To keep things simple, you can build the Docker
image on the server. For example, clone your Git repository with all your code on the
server and build the image using Docker. This way, you don’t need a container
registry. After you added the app to the ShinyProxy configuration, you can start
it through ShinyProxy. In order to know how many concurrent
users are supported by the server, it’s a good idea to first have a look at the
resources consumed by the app. Start the app, perform some user actions and run
the following command:

sudo docker stats

The output will be similar too:

The last line in the screenshot shows the container running our Shiny app. It
uses about 125 MiB of RAM and very little CPU time. Keep in mind that we’re
working with a small demo app, CPU usage will increase if your app processes
more data or performs heavier computations. Most Shiny apps typically use less
than 500 MiB of RAM, but some can consume multiple gigabytes, especially when
loading large datasets. To keep our example realistic, we’ll assume each app
requires 500 MiB of RAM. The other system components (including ShinyProxy) also
consume some memory. In the screenshot, these consume about 1.5 GiB. ShinyProxy
generally requires up to 2 GiB of RAM. To avoid degrading server performance,
it’s a good practice to leave a few gigabytes free. For this setup, we’ll
reserve 4 GiB for ShinyProxy and the server itself, leaving 28 GiB available for
the Shiny containers. With each container using 0.5 GiB, this allows us to run
up to 56 containers concurrently.

Optimizing the configuration

Originally, ShinyProxy created a new container for each user. While this approach
is simple and improves security, it’s not ideal for hosting a public app with
many users. Fortunately, since quite some time, ShinyProxy has excellent
support for what we
call pre-initialization and container-sharing.
With this feature, a single container can serve multiple users simultaneously.
Since the containers are pre-initialized, loading times are minimal. However,
each Shiny container still has a limit on the number of users it can handle. For
simple dashboards, you can typically expect about 20 concurrent users per
container. In our example, we’ll assume 10 concurrent users per container. Given
our 56 containers, this setup allows the server to handle up to 560 concurrent
users. To stay on the safe side and avoid overloading the server, we’ll round
this down to 500 concurrent users.

With these numbers in mind, it’s time to adjust the configuration. Let’s start
with the app configuration.

proxy:
specs:
- id: my-app
container-image: openanalytics/shinyproxy-demo
seats-per-container: 10
max-total-instances: 500
minimum-seats-available: 500

The seats-per-container: 10 setting tells ShinyProxy that each container can
handle 10 users or “seats”. To prevent overloading the server, we limit the
total number of concurrent users to 500 with the max-total-instances: 500
setting. If more than 500 users try to access the app at the same time, they
will see a message indicating that not enough capacity is available. Additionally, the
minimum-seats-available: 500 setting ensures that ShinyProxy pre-creates 50
containers and keeps them running, so users can start the app immediately
without waiting for containers to initialize.

In this example, we estimated the maximum number of concurrent users and limited
the server to that value. Since we’re deploying only one app, it makes sense to
create all containers at startup. However, if you are hosting multiple apps on a
single server (which ShinyProxy fully supports), it’s better to start with just a
few containers per app. ShinyProxy automatically creates new containers as
users begin accessing the apps, ensuring resources are used efficiently.

While you are updating the configuration of ShinyProxy, add (or update) the
following settings as well:

proxy:
hide-navbar: true
landing-page: SingleApp
authentication: none
# ...
memory-limit: 2Gi

The first property hides the ShinyProxy navbar, which is often unnecessary when
hosting a single public app. By setting landing-page: SingleApp, users are
automatically redirected to the app when they visit the landing page. Since this
is a public app, we also disable authentication. Finally, we limit the memory
that ShinyProxy can use to ensure there is always enough RAM available for the
Shiny apps.

After updating the configuration, the operator creates a new ShinyProxy instance
with the updated settings. This process usually takes a few minutes. You might
see a “forbidden” page if you previously logged in using a username and
password. This happens only once when switching from authentication: simple to
authentication: none. To fix it, simply clear the ShinyProxy cookies in your
browser.

Testing the performance

As soon as ShinyProxy starts, it creates 50 new containers. When you launch your
app, it loads almost instantly, thanks to the pre-initialized containers. We
tested this setup with 500 browser sessions, and the system handled them
effortlessly. Both ShinyProxy and the Shiny app responded quickly, and the server
load remained minimal. Of course, actual performance depends on the resource
consumption of your app, but ShinyProxy is designed to manage these variations
efficiently. The setup demonstrates that even a single ARM-based server can
easily manage hundreds of concurrent users, delivering robust, production-grade
performance.

The first panel in the screenshot shows how long users wait before being
assigned a seat, and the second panel shows the number of available seats. When
we launched 500 sessions within just two minutes, the available seats dropped
quickly. Even so, loading times stayed at only a few milliseconds, which makes
it clear that ShinyProxy remains highly responsive during a sudden surge of
hundreds of new sessions.

Once all apps are loaded, ShinyProxy’s CPU usage drops and remains low, as shown
by the docker stats output:

Monitoring

Although the monitoring stack was enabled when ShinyProxy was first deployed,
disabling authentication means that Grafana is no longer accessible through
ShinyProxy. A simple workaround is to deploy a second ShinyProxy instance that
still uses authentication. This private instance can also serve as a testing
environment for new development versions of your app before deploying them to
the public production server. To set this up, create a new configuration file in
the input directory, for example private.shinyproxy.yaml, and add the
following code:

server:
servlet:
context-path: /private

Be sure to update the proxy.realm-id, for example by setting it to private.

After deploying the server, you can access it at the same hostname as the main
server, but under the /private/ sub-path. For instance, if your server is
available at http://localhost/, the private instance is accessible at
http://localhost/private/, and Grafana at http://localhost/private/grafana/.
To view the logs and metrics of the public instance, you need to adjust the
namespace variable in the dashboards.

Conclusion

With the introduction of the ShinyProxy Operator for Docker, setting up a
production-grade ShinyProxy server has never been easier. This post demonstrates
that only minimal configuration is needed to handle a large number of concurrent
users. Expensive servers or clusters aren’t required, just a single server is
sufficient. If you need to support more users, simply scale up the server: in
most cases, doubling CPU and RAM roughly doubles the number of concurrent users.
Compared to the resource usage of the Shiny apps themselves, ShinyProxy consumes
very little CPU and RAM, so it rarely becomes a bottleneck even under heavy
load.

Don’t hesitate to send in questions or suggestions and have fun with ShinyProxy!

Related

Related

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

©2025 24-7 Today | Design: WordPress | Design: Facts