[HN Gopher] Simpler Backoff
       ___________________________________________________________________
        
       Simpler Backoff
        
       Author : todsacerdoti
       Score  : 115 points
       Date   : 2025-05-31 04:43 UTC (18 hours ago)
        
 (HTM) web link (commaok.xyz)
 (TXT) w3m dump (commaok.xyz)
        
       | charcircuit wrote:
       | Switching away from an implementation that everyone is familiar
       | with is the opposite of making something more readable.
        
         | larodi wrote:
         | indeed, back in the day i'd love to write perl code such as
         | 
         | this() and that() or otherr() and fail();
         | 
         | so this all seems very reasonable, and clear, and people who
         | read the code hated it. I've tried many times to do it in JS
         | sadly it does not work:
         | 
         | a && b || do { console.log (err) }
         | 
         | but this all does not make it easier to read, but makes it only
         | very likely to outsmart yourself.
         | 
         | some code is algorithmically more beautiful than other, more
         | declarative perhaps. but going away from the common
         | understanding of trivial stuff, does not always benefit the
         | group or the project.
        
           | spauldo wrote:
           | It's down to what's idiomatic for the language. I'd argue
           | that the Perl example is idiomatic Perl (just as the Lisp
           | equivalent would be idiomatic Lisp), but that's certainly not
           | idiomatic JavaScript.
        
         | ayhanfuat wrote:
         | Aren't both implementations fairly common? Precomputed /
         | hardcoded set of values vs adjusting it inside the loop?
        
         | anonzzzies wrote:
         | But how many people need to read/write this code? There are
         | tons of open source libraries, gateways, proxies etc etc.
        
       | unsnap_biceps wrote:
       | While I like the simplicity of the lookup table, I find myself in
       | larger projects to have different backoffs for different upstream
       | services. I'll have shorter ones for services that can degrade
       | without a large impact to service results and longer ones for
       | services that are critical for results. I would prefer to have
       | one algorithmically generated call that is flexible vs multiple
       | individual functions that have different lookup tables, or
       | generating tables and passing them in each time.
        
         | fmbb wrote:
         | You could have two lookup tables.
        
           | VWWHFSfQ wrote:
           | what if you have 100 lookup tables
        
             | barbazoo wrote:
             | Careful, that's awfully close to N. /s
        
             | mceachen wrote:
             | If you don't have lookup tables for your lookup tables you
             | aren't doing it right
        
             | avinassh wrote:
             | you could use SQLite, store all of them
        
               | kstrauser wrote:
               | MongoDB is web scale.
        
         | harpiaharpyja wrote:
         | Generating tables is the way to go in that case. The
         | flexibility you need, with the advantages of being data driven.
        
         | kiitos wrote:
         | Backoff is a client-side behavior, so if you want to give
         | different backoff behaviors to different classes of upstream
         | services, you just define those backoff tables somewhere where
         | the client can pick from them based on the target service it's
         | trying to talk to.
        
       | OutOfHere wrote:
       | Using a lookup table doesn't allow parameter values to easily be
       | changed. Also, some of us do not get paid by the line. Fwiw, I do
       | often note the values in a comment, and this is generally
       | sufficient.
        
       | hsbauauvhabzb wrote:
       | Change of the sake of change?
        
       | axeljohnsson wrote:
       | I recently listened to Marc Brooker's talk [1] on retries. My key
       | takeaway: You don't want to put more load on your system during
       | stress. At AWS, they do "adaptive retries", which I found
       | interesting.
       | 
       | [1] https://www.youtube.com/watch?v=rvHd4Y76-fs
        
         | zoover2020 wrote:
         | I second this, retry mechanisms can cause retry storms in vast
         | enough, distributed systems. In Amazon code bases I found the
         | same adaptive retry strategy after a Christmas where once we
         | played whack-a-mole for a service to get back up as its clients
         | kept retrying.
        
       | jpillora wrote:
       | Simple backoff ftw https://github.com/jpillora/backoff
        
         | ricardobeat wrote:
         | That's the exact same logic, just written more verbosely and
         | excluding the actual retry mechanism.
        
       | blensor wrote:
       | Is this meant as a joke or a serious argument?
       | 
       | I personally find the original code just as if not more readable
       | than the static lookup table and I don't need to count out the
       | elements if I want to know how often it will retry.
       | 
       | But more importantly, changing the max retries is trivial in the
       | original code and tedious in the static lookup table, especially
       | for bigger changes.
       | 
       | Also, this is something you most likely want to make configurable
       | to adapt to certain scenarios which is not possible with a static
       | table
       | 
       | There are more reasons against it but those are the the main ones
       | that jump at me right away
        
         | masklinn wrote:
         | Not only that, but now that Go has iterators if you need this
         | in several locations the _logic_ of the original can easily be
         | encapsulated in an iterator with a few relevant tunable knobs,
         | and then you just write something like:                 for
         | range ExponentialBackoff(ctx) {           err := request(ctx)
         | if err == nil {               return nil           }       }
         | 
         | and if one of the callsites needs to configure the backoff then
         | you've got something like:                 for range
         | ExponentialBackoff(ctx, MaxAttempts(20), BaseDelay(5)) {
         | err := request(ctx)           if err == nil {
         | return nil           }       }
        
       | efitz wrote:
       | Why a lookup table? Why not just have the client remember the
       | last backoff, and multiply it by a constant and apply a ceiling?
       | 
       | def getNewBackoff( oldBackoff: int): int {
       | 
       | newBackoff = oldBackoff * BACKOFF_CONSTANT
       | 
       | if (newBackoff > BACKOFF_CEILING) { newBackoff = BACKOFF_CEILING
       | }
       | 
       | return newBackoff }
       | 
       | if you bit align the ceiling value you can replace the
       | conditional with a bitwise and mask. If you use 2 as your
       | exponent you can replace the multiplication with a bit shift
       | left.
       | 
       | It's also a good idea to introduce jitter, e.g. don't set the new
       | backoff to the old backoff * a constant, but add a random value
       | such that the old value gets randomized around the desired new
       | value- this helps prevent pile-ons when a bunch of clients
       | noticed about the same time that the server became unresponsive;
       | if they all have the same backoff algorithm then they're all
       | going to hit the server again at about the same time. It's good
       | to spread that out to try to avoid server overloading during
       | restarts/cold starts/whatever.
       | 
       | The best results I ever got were when I used server side hinting
       | to avoid pile-ons when the retries from the client were all
       | clustered around the same time.
       | 
       | When my server got in an overloaded state, the auth path for
       | clients short circuited and delivered an overloaded message with
       | a randomized suggested retry value. The server can't make the
       | clients honor it (we could in my case because we wrote the
       | clients) but if the clients do honor it you can spread out
       | reconnects over a period instead of make them all point-in-time
       | and dampen or prevent oscillating overloads.
        
         | mariusor wrote:
         | Similar to your suggestion, I implemented a wrapper around
         | retries in Go before and despite liking the lookup table
         | version quite a lot, I think it's more important to be able to
         | parametrize the initial duration of the back-off and also the
         | rate at which it grows, so that's what I focused on.
         | 
         | I'm not sure how my example fares against on a simplicity
         | scale, but it feels a little more readable to me:
         | Retry(5, BackOff(time.Second, Linear(1.4), func(_
         | context.Context) { // do stuff } )
         | 
         | Example cribbed from the documentation:
         | https://pkg.go.dev/git.sr.ht/~mariusor/ssm#StrategyFn
        
         | 0x696C6961 wrote:
         | Backoff APIs often take a retry count as the input. Your
         | suggestion would require changing the API which isn't always
         | practical.
        
         | gdiamos wrote:
         | explicit is better than implicit
        
           | efitz wrote:
           | A server cannot make a client behave a certain way. Explicit
           | doesn't matter in this case, and as many of the comments
           | said, most of the time the best thing to do for the server is
           | to spread retry load randomly over an interval. Explicit is
           | bad for the server.
           | 
           | The only thing (IMO) explicit retries are good for is to make
           | client state more deterministic on the client, but there are
           | many ways of doing that other than hard coding retry
           | intervals. For example, use asynchronous patterns for
           | networking APIs and allow querying state of API-calls-in-
           | progress.
        
         | akshayshah wrote:
         | Full agreement from me that jitter is essential. Here's AWS on
         | good retry policies: https://aws.amazon.com/builders-
         | library/timeouts-retries-and...
        
       | ctxc wrote:
       | If the author is here - please turn word wrap off for code. It's
       | unreadable on mobile.
        
         | jitl wrote:
         | Code wraps for me in Safari on iOS 18.5
        
       | ChrisMarshallNY wrote:
       | I do this, in a couple of my Watch apps.
       | 
       | Might be worth looking at. The current implementation is slightly
       | awkward, but it also uses a random delay.
       | 
       | Watch/iPhone communication isn't very robust. It's much better,
       | in more recent Watches.
        
       | bravesoul2 wrote:
       | Another approach is to make this a cross-cutting concern using a
       | sidecar like Envoy.
        
       | jelder wrote:
       | This really just an argument for using iterator generators. Does
       | Go offer those yet?
        
         | jitl wrote:
         | Yes
        
       | kassner wrote:
       | Offtopic, but huge respect for this[1]:
       | 
       | > Lots of people don't want to read about AI.
       | 
       | > I respect that.
       | 
       | > But I'm currently steeped in the world of AI, for better or for
       | worse, and I want to blog about it. So I've split this blog in
       | half.
       | 
       | > The normal blog, which you are reading, is now AI-free.
       | 
       | > There's another, at /ai that contains only AI posts. It has its
       | own RSS feed.
       | 
       | Thank you. It (non-AI) definitely goes to my RSS reader.
       | 
       | 1: https://commaok.xyz/post/blog-schism/
        
       | rixed wrote:
       | Am I the only one who wonders how such trivial posts could end up
       | on HN first page?
        
         | qoez wrote:
         | It's so easy to make bot accounts I wouldn't be surprised if a
         | decent amount of them are manipulated.
        
         | Retr0id wrote:
         | Something doesn't have to be long or complex to be interesting.
        
       | handsclean wrote:
       | Here's a version that's IMO much simpler still, about half as
       | long as both (after fixing v2's line count cheating), and fixes a
       | bug: both article versions wait an extra 60 seconds between the
       | last attempt and returning an error.                   func
       | do(ctx context.Context) error {             for attempt := 1; ;
       | attempt++ {                 if request(ctx) == nil {
       | return nil                 }                      if attempt ==
       | 10 {                     return fmt.Errorf("failed after %d
       | attempts", attempt)                 }                      select
       | {                 case <-ctx.Done():                     return
       | ctx.Err()                 case <-time.After(time.Second *
       | min(60, math.Pow(2, attempt-1)*(0.75+rand.Float64()*0.5))):
       | }             }         }
       | 
       | I think the problem with the original isn't using a formula, it's
       | over-parameterization. Declaring constants right next to where
       | they're used once isn't giving you anything but the opportunity
       | for naming issues and rigidity in how you combine them. If they
       | aren't passed in or repeated, then I wouldn't pull them out; if
       | they are, then I'd try to look for minimal and natural boundary
       | points, like just min and max, or maybe an attempt - delay
       | function.
        
         | Waterluvian wrote:
         | I sometimes agree, sometimes disagree. I don't like magic
         | numbers like retry(3, 1_000, 2_500)
         | 
         | (Though sometimes your text editor and docs are good enough to
         | help, but I don't like making those feel required).
         | 
         | I like how Python (and probably many other languages) allows
         | for using the name of a positional arg. So you could call the
         | exact same function but write it as retry(count=3,
         | min_ms=1_000, max_ms=2_500) if you feel like it needs more
         | clarity. Now you've got documented values that are tied to the
         | function signature, their names have been decided for you, and
         | they're inline!
        
         | Mawr wrote:
         | > Declaring constants right next to where they're used once
         | isn't giving you anything but the opportunity for naming issues
         | and rigidity in how you combine them. If they aren't passed in
         | or repeated, then I wouldn't pull them out;
         | 
         | Using magic numbers all over the place is the mark of a novice
         | who lacks understanding that the meaning behind the numbers he
         | knows in his mind will not magically transfer over to other
         | people reading the code. Advising this approach is an
         | interesting call.
         | 
         | Specifically here, we've got these at the beginning of the
         | function:                   const (             maxAttempts =
         | 10             baseDelay   = 1 * time.Second
         | maxDelay    = 60 * time.Second         )
         | 
         | If I went out of my way to look at this function, these are
         | probably the values I'm interested in changing, and look,
         | they're right here, at the top. Neat. It's obvious how to
         | adjust the values to get what I want. I can stop reading the
         | function right there.
         | 
         | Without these declarations, how would I know that "10" midway
         | through the function is the number of attempts? I'd have to
         | read and understand the entire logic first. Great. Now multiply
         | this effort by the amount of programmers on the team and the
         | amount of magic numbers.
        
       | harpiaharpyja wrote:
       | So, this is just about declarative programming style?
       | 
       | I thought based on the title it would have been about different
       | approaches to backoff.
        
       | grg0 wrote:
       | Or a function backoff(attempt) that abstracts away the details
       | and can be re-used in places, perhaps a higher-order function
       | that takes as input the function that does whatever.
       | 
       | "This code, or something like it, probably looks really
       | familiar:"
       | 
       | Better leave that company, to be honest.
        
       | yawaramin wrote:
       | On a slightly different note, I am using Fibonacci backoff for a
       | couple of things here and there and (subjectively) finding it a
       | bit nicer than the standard 2x exponential backoff. I have it on
       | a system that waits for a few days to raise an alert on missing
       | data and I find the alerts on a cadence of 1, 2, 3, 5, 8... days
       | are a bit tighter but still not overwhelming.
        
       ___________________________________________________________________
       (page generated 2025-05-31 23:01 UTC)