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 for the main site and 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?


– T.J.

1 Like

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 rewriting /api/* to (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 directly averaged about ~40ms-70ms, but hitting 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:

1 Like

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