Best practice for Static + API + DB?

Hi folks,

I’ve been evaluating Render for a couple of days and really like a lot of what I see, but I’m struggling with the best way to do something that seems like a really, really common use case: A site with significant static assets, a dynamic piece, and a DB.

It seems fairly clear that static site + web service + DB is the way to go, the trick is in wiring up the static site and the web service in the right way.

First I tried rewrite rules on the static site to make API requests go to the web service. That works, but is slow – an API call that takes on average 40ms when pinging the web service directly takes an average of 400ms when performed through the static site rewrite. (Note this is rewrite, not redirect, so I wasn’t expecting that, but I guess there’s CDN plumbing and rerouting once it gets to Render’s servers and such…)

Another approach would be to use example.com for the main site and api.example.com for the API calls. That requires handling CORS and, in the case of “complex” requests, will mean the browser has to do an OPTIONS request before doing the real request. That’s unnecessary overhead. You can reduce the OPTIONS calls via Access-Control-Max-Age but it’s per-path…

A third approach would be for the “API” server to actually serve the static files as well – no static site. But that’s markedly more complex to set up in an optimized way. I’m using a Node.js backend for this test, and while it’s good, it’s fairly classically not the best way to serve static content. Maybe it would be okay if it’s fronted by Render’s CDN, I don’t know.

What’s Render’s best practice for doing this?

TIA,

– T.J.

4 Likes

Hey @tjcrowder, this is a really excellent summary of the options Render makes available to you. We don’t have an official best practice, but I’d personally recommend either of the first two approaches. As you pointed out, the third approach works, but doesn’t get the benefits of a CDN. We’re planning to implement a feature that would let you more easily attach a CDN to a server (see CDN for static assets in backend apps | Feature Requests | Render), but functionally I imagine it will end up being pretty similar to the rewrite rule approach.

I’m surprised to hear that the rewrite rule approach was so slow. You’re correct that there’s an extra hop to the CDN, but our provider (fastly) has POPs all over the globe so we shouldn’t be seeing that much additional latency. If you’re willing to help, I’d be very eager to see if we can reproduce this in order to gain more insight into what’s causing the 10x slowdown.

As for CORS preflight requests, in practice they may not be too bad, especially if you have, say, a GraphQL API at a particular path rather than a REST API that uses many different paths. For what it’s worth, Render serves all traffic over HTTP/2 so “simple” requests can be multiplexed and we might make up for some of that unnecessary overhead.

Thanks for the great question!

1 Like

Thanks David.

Enabling the CDN for static assets from web services would be great. That said, I think the ideal solution is the internal rewrite.

I’d be happy to help demonstrate the performance problem with rewrite rules. It was easy to set up:

  1. Static site https://my-static-site.onrender.com rewriting /api/* to https://my-web-service.onrender.com/api/ (the actual rules I was using had parameter captures if it matters, but that was because I was being dumb).

  2. Web service running Node.js + Express with endpoints for /api/example (POST for some, GET for others).

  3. I very much doubt what the API requests were doing was relevant, but they were doing simple queries updates against a Render hosted Postgres DB table with only a dozen or so records in it.

Hitting the GET route https://my-web-service.onrender.com/api/search directly averaged about ~40ms-70ms, but hitting https://my-static-site.onrender.com/api/search was roughly 10x slower, averaging ~400ms-700ms.

If you have any trouble replicating it just let me know, I can spin up a couple of example services. (Ideally without being charged for them :slight_smile: ).

For now my approach is to use CORS and set Access-Control-Max-Age high enough that preflights don’t happen all that often. With that setup, a POST route typically takes 165ms for the OPTIONS and 40-70ms for the POST, and then subsequent POSTs within the max-age average 40ms-70ms. The downside is that the preflight cache is per-URL (including query string), so for instance if you have something that deletes a resource, /api/resource/delete/23 and /api/resource/delete/42 are different URLs and the preflight cache isn’t used. So to get maximum benefit I should use just /api and hide everything in the POST body – which makes debugging harder and logs less clear (but there’s an argument for doing it anyway – URLs aren’t encrypted, POST bodies are).

– T.J. :slight_smile:

2 Likes

Thanks for the extra info. And it does sound like CORS w/ Access-Control-Max-Age is a good working approach. Hopefully we can figure out what’s causing the slowdown with rewrites to the point that it becomes the best option. We’ll keep you posted!

1 Like

I also tried to rewrite between a static React site and Express, but the api is a lot slower (10x). What could cause this?

Then I deployed the static site to Netlify with a rewrite to the backend on Render. That works without the delay.

[[redirects]]
  from = "/api/*"
  to = "https://mybackend.onrender.com/api/:splat"
  status = 200
  forece = true

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

@roeland could you share the URLs you’re using here - you can DM me on here or email them to support@render.com

sorry, I already removed my account. It was a simple express app with a single api. No Cors.
The location was Frankfurt.

Just following up because I’m in the same situation.

I am running a Vue site as a static site and an Express server as a web service. I used url rewrite to reroute my API requests to the server and it seems to be working. There is a small delay (200ms from the server url while 300-350ms from the frontend url), but not enough for me to suffer through CORS setup (at least for now)

I’m curious if an official best practice has manifested since the original post. Is using a separate static site + web service the best approach? Is URL Rewrite meant to handle this use case? (Will this work for web-sockets is the next thing on my list to test, but maybe someone already knows?)

1 Like

The problem with the slow rewrites still exists. I had ~80ms calling the backend web service directly, ~160ms using rewrites with a web app hosted on AWS Amplify, and ~500ms using rewrite on a static site on render.com

The problem with the slow rewrites still exists. I had ~80ms calling the backend web service directly, ~160ms using rewrites with a web app hosted on AWS Amplify, and ~500ms using rewrite on a static site on render.com

Update from render.com:

As of a few weeks ago static sites are now deployed closer to your other services. As you have a service deployed in Frankfurt, deploying a static site would deploy it there so the rewrite latency should now be significantly reduced.

The request time went down from ~500ms to ~180ms after I deleted and re-created my static site