/ Fly Edge Apps

Fail(over) upward!

Failing sucks. No one ever wants to fail. But sometimes it's possible to fail upward, meaning we turn a bad thing into something not so bad.

What does this have to do with serving your app, you ask? Well, servers aren't perfect. Sometimes they fail. But it's hard to know when they'll fail. If your app is important to you, you should have measures in place to handle downtime.

You do have that figured out ... right?

If you're anything like me, app reliability isn't your first priority. There isn't enough time in the day to ship features! Do marketing! Get sales! Why spend precious time optimizing for a tail event, too?

There's a ton of stuff to do. But tail events happen. And they always seem to happen at the worst possible times. In this article, we'll show you how you can use Fly to prevent server downtime from alienating the most important people in the world (your users!) with minimal effort.

Simple: Serve From Cache

If your site's servers are down, you can use Fly to serve it from cache. Simply, check your request's status and if it's not successful, serve from cache:

fly.http.respondWith( async function( request ) {
    let url_parts = url.parse( request.url, true );
        
    const pr = proxy( "https://example.com", {
        headers: {
            'Content-Type': 'text/html',
            'Accept-Encoding': 'utf-8'
        }
    });
    let response = await pr( request );
    if( response.status === 200 ) {
        //everything's dandy
        let response_body = await response.text();
        return new Response( response_body, { status: 200 } );
    } else {
        //in case of failure, handle different pages differently
        if( ( url_parts.path ).length <= 1 ) {  //assume front page
            let backup = await fly.cache.getString( 'backup_frontpage' );
            return new Response( backup, { status: 200 } );
        } else if( ( url_parts.path ).indexOf( 'app' ) > -1 ) {
            let backup = await fly.cache.getString( 'backup_apology' );
            return new Response( backup, { status: 200 } );
        }

    }
});

You'll notice above that we're handling the front page and app differently if the request isn't successful. That's because serving your front page from cache might work fine, since it's mostly static, but the same might not go for your app. Your app is probably more sophisticated, and cache might not cut it.

In that case, we've told Fly to serve a static "apology" page while your servers are down to tell your users that you know there's a problem and you're frantically working to fix it.

It's not ideal to serve an apology page, but it's way better than serving nothing and leaving your users hanging (or if your venture is young, perhaps leading your users to think you've gone out of business!).

Advanced: Beyond Cache

But what if caching (or admitting failure) aren't viable options?

Consider the following example: DEX is a decentralized cryptocurrency exchange. It needs real-time prices to facilitate trades—no excuses, no delays—but it doesn't have its own central database to get real-time prices. So it needs to rely on third parties—which it doesn't control—which means it needs a backup plan. Ideally, more than one backup plan.

Can Fly help DEX deliver the reliability it needs?

Well, yes! Instead of falling back to cache or a static page, simply have Fly send price requests to backup servers in case the first attempt fails. This may not be feasible for a low-budget app with sophisticated deployment requirements, but it's not impractical in our case: our 'app' is really just an API that returns some JSON (i.e., crypto prices).

Assuming we configure our backup servers to source price data wisely (i.e., 3 servers that fetch prices from 3 different sources), we can just fail over from one option to the next and be reasonably sure that we won't ever have issues:

let response = await fetch( "https://btcpricenode1.com/api/get_last_price" ); 
if( response.status === 200 ) {
    return read_return_response( response );
} else {
    
    let response = await fetch( "https://btcpricenode2.com/api/get_last_price" ); 
    if( response.status === 200 ) {
        return read_return_response( response );
    } else {

        let response = await fetch( "https://btcpricenode3.com/api/get_last_price" ); 
        if( response.status === 200 ) {
            return read_return_response( response );
        }
    }
}

...

async function read_return_response( response ) {
    let btcprice = await response.text();
    return new Response( btcprice, { status: 200, headers: { 'Content-Type': 'application/json' } } );
}

And if all three sources fail...well the world might have bigger problems at that point!

So there you have it: failover measures that are quick & simple to implement, so you can get on with shipping.

You know what else is quick & simple? Getting started with Fly. Check it out here!

Steve Jain

@m52go

http://jain.io

Steve Jain

Indie developer running 2 apps that make the high-brow a little less high. Uses JavaScript everywhere, including places it has no business being in. Recovering B2B biz developer & financial analyst.