[Release][CloudSeed]: Faster, simpler routing with F# / Giraffe

Date: 2023-05-17 | create | business | cloudseed | fsharp | giraffe |

Overview

CloudSeed is a project boilerplate for F# / SvelteKit with the goal of making it fast and easy to launch a simple, scalable web service with F#.

Today we're releasing an upgrade to CloudSeed's F# / Giraffe implementation which makes routes both simpler to configure and substantially faster (~9% in request per second benchmarks).

What's changed?

We've updated the routing implementation in CloudSeed's F# / Giraffe implementation to use the newer ASP.NET Endpoint Routing paradigm. This paradigm is both simpler to configure (subjectively) and more performant.

Routing in F# / Giraffe is simple (just a function!) and this update doesn't change that. That said, there are a few differences in how this kind of routing works which we'll touch on here.

For a more in-depth dive, see: F# / Giraffe Endpoint Routing

Simplicity

The first benefit we get from this update is simplicity.

Legacy F# / Giraffe routing biased towards lots of nested functions, lists, and parentheses (a bias that most functional paradigms seem to have). In some cases this is good (we love composition!) but in others it just becomes hard to maintain.

Here's the actual routing section using the old F# / Giraffe routing:

choose [
    subRoute "" (
        choose[
            GET >=> 
                choose [
                    route "/" >=> 
                        warbler (fun _ -> 
                            apiResult (
                                (getSentinelsQueryHttpHandler serviceTree.SentinelServiceTree)
                            ))
                    route "/ping"   >=> text "pong"
                    route "/sentinels" >=> 
                        warbler (fun _ -> 
                            apiResult (
                                (getSentinelsQueryHttpHandler serviceTree.SentinelServiceTree)
                            ))
                ]
        ]
    )
    subRoute "/push-the-button" (
        choose[
            GET >=> 
                choose [
                    route "/push/totals" >=> 
                        warbler (fun _ -> 
                            apiResult (
                                (createGetButtonPushesTotalQueryHttpHandler serviceTree.PushTheButtonServiceTree)
                            ))
                ]
            POST >=> 
                choose [
                    route "/push" >=> 
                        warbler (fun _ -> 
                            apiResult (
                                (createPushTheButtonCommandHttpHandler serviceTree.PushTheButtonServiceTree)
                            ))
                ]
        ]
    )]

Quite a bit of code for what turns out to be just 5 routes!

Now let's take a look at what the routing section is after the change, using the updated F# / Giraffe Endpoint Routing paradigm:

[
    subRoute "" [
        GET [
            route "/" ( 
                apiResult (
                    (getSentinelsQueryHttpHandler serviceTree.SentinelServiceTree)
                ))
            route "/ping" (text "pong")
            route "/sentinels" ( 
                apiResult (
                    (getSentinelsQueryHttpHandler serviceTree.SentinelServiceTree)
                ))
        ]
    ]
    subRoute "/push-the-button" [
        GET [
            route "/push/totals" ( 
                apiResult (
                    (createGetButtonPushesTotalQueryHttpHandler serviceTree.PushTheButtonServiceTree)
                ))
        ]
        POST [
            route "/push" ( 
                apiResult (
                    (createPushTheButtonCommandHttpHandler serviceTree.PushTheButtonServiceTree)
                ))
        ]
    ]
]

Quite a bit cleaner!

Performance

Simplicity is a goal every software project should strive for. Performance is close behind.

The cool thing with this update is we can have both!

My understanding is that this new endpoint routing switches from using Giraffe's custom router (hand-spun by devoted volunteers) to using ASP.NET's internal routing which has been evolved and tweaked by Microsoft for years. This seems to be the only functional difference and thus where this speed up is.

Now onto the data - I'm claiming that F# / Giraffe's endoint routing is 9% faster than it's legacy routing.

This data comes from:

  • Previous F# / Giraffe performance investigations: Improving F# / Giraffe web benchmarks by 6.56x
    • Giraffe - Endpoints: 116.5k RPS
    • Giraffe - Legacy: 106.8 RPS
    • Endpoints / Legacy: 116.5 / 106.8 = 1.09 -> 9%
  • Current run of Web Frameworks Benchmark (2023.05.01)
    • Giraffe - Endpoints: 118.6k
    • Giraffe - Legacy: 109.1k
    • Endpoints / Legacy: 118.6 / 109.1 = 1.087 -> 9%

Next Steps

Keep building your F# web services with CloudSeed and let me know if you have any questions / suggestions for further improving it.

On my end I'll keep pushing to see if we can get more performance out of this thing and if we can make Giraffe.EndpointRouting the default moving forward.

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.