Best practice for using external api in a Shiny app

Hi all,

Are there any guidelines/best practices for politely using an external API in a Shiny app?

For example, is it possible to limit the number of api calls across all process running the Shiny app so that you don't inadvertently overload the external api or use beyond the external api's guidelines such as 10 calls per second?

I have read a few documents regarding how to do web-scraping politely, e.g. https://www.rostrum.blog/2019/03/04/polite-webscrape/, but none around politely using api calls within a Shiny app

Thanks

Iain

3 Likes

This is a fantastic question. I'm unfortunately not aware of a great "out of the box" solution for this type of behavior. If you are trying to slow down reactive triggers, Shiny has shiny::debounce() and shiny::throttle() for this type of "slowdown."

If you're speaking about pure backend, the solutions I come up with off the top of my head are pretty simplistic. For instance, if I 99% or 100% of the time will have fewer than 5 instances of the app, then I don't want to make more than 2 calls per second on any given instance. So I could ensure each app "slows itself down" by e.g. updating a reactiveValue() to be a timestamp 0.5 seconds in the future. If the process tries to execute again before then, it polls until that time is up. R is single threaded, so you don't have to worry about a single process bypassing itself.

It would be a bit more complex, but you could also do this in a "shared" way across the processes to ensure that you max out the 10/second. The problem you have to worry about is multi-writes. You could imagine a database where requests are logged, and each client / R process checks to be sure that there are fewer than 10 in the last second before logging itself and then firing.

Someone has gotta have a more robust solution than this though :smile: Just the first hackery that came to mind!

1 Like

Thanks Cole.

Would you be able show me an example of the reactiveValue concept? It sounds like a great first step.

This was a kinda fun prototype to build! :smile: I have to say I didn't have a whole lot of time to think about it deeply, so there may be weird edge cases, but it seems to work. I slowed things down a bit to make it more "human understandable." 10 requests per second is pretty fast for a human :smile: This limits to 1 request per second, and has a CPU sleep for a tenth of that (0.1) so there is room for other things to sneak into the thread.

library(shiny)

ui <- fluidPage(
    actionButton("action", "Request"),
    textOutput("counter"),
    textOutput("next_fire")
)

server <- function(input, output) {
  timer <- reactiveVal(Sys.time())
  i_counter <- reactiveVal(0)

  observeEvent(input$action, {
      # poll timer
      while (timer() > Sys.time()) {
          # so we don't burn CPU
          Sys.sleep(0.1)
      }
      # timer satisfied! push out timer 1 second
      timer(Sys.time() + 1)

      # make request
      i_counter(i_counter() + 1)
  })

  output$next_fire <- renderText(timer())
  output$counter <- renderText(i_counter())
}

shinyApp(ui = ui, server = server)

2 Likes

Brilliant, thanks! That is a great starting point for me to address this concern.

1 Like