Rate Limiting API Calls in Go

I recently needed to connect to the Desk.com API and realized that their API imposes a rate limit. There are several ways to get around this, but I decided to go with the simplest thing that would work for my code and wrote a bucket that keeps track of how many API calls I have available, and returns a “Not enough capacity” error if I can’t consume any at this time. Then in my code that makes API calls, I simply check if I can consume an API call, and if i can’t I sleep for a few seconds.

Simple enough, except keeping track of times events can be a pain. In my case I would need to keep track of how many API calls are available in my bucket, as well as when a new set was last added to the bucket. When a new API call attemps to consume an existing API call, I would need to first calculate how many many times the timed event had occured and update the remaining API calls accordingly. Only after doing these calculations could I determine if I could consume an API call.

Concurrency in Go makes this much simpler. Instead of keeping track of the last time API calls were added to my bucket, I can instead create a go routine with the sole responsibility of adding API calls to the bucket at a set interval. This ends up simplifying the code significantly, and the final rate limiter is roughly 65 lines of code. You can see it below, or you can check it out on GitHub.

package drip

import (
  "errors"
  "sync"
  "time"
)

type Bucket struct {
  Capacity     int
  DripInterval time.Duration
  PerDrip      int
  consumed     int
  started      bool
  kill         chan bool
  m            sync.Mutex
}

func (b *Bucket) Start() error {
  if b.started {
    return errors.New("Bucket was already started.")
  }

  ticker := time.NewTicker(b.DripInterval)
  b.started = true
  b.kill = make(chan bool, 1)

  go func() {
    for {
      select {
      case <-ticker.C:
        b.m.Lock()
        b.consumed -= b.PerDrip
        if b.consumed < 0 {
          b.consumed = 0
        }
        b.m.Unlock()
      case <-b.kill:
        return
      }
    }
  }()

  return nil
}

func (b *Bucket) Stop() error {
  if !b.started {
    return errors.New("Bucket was never started.")
  }

  b.kill <- true

  return nil
}

func (b *Bucket) Consume(amt int) error {
  b.m.Lock()
  defer b.m.Unlock()

  if b.Capacity-b.consumed < amt {
    return errors.New("Not enough capacity.")
  }
  b.consumed += amt
  return nil
}

Learn Web Development with Go!

Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.

You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.

Avatar of Jon Calhoun
Written by
Jon Calhoun

Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.

Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.

Related articles

Spread the word

Did you find this page helpful? Let others know about it!

Sharing helps me continue to create both free and premium Go resources.

Want to discuss the article?

See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!

You can reach me via email or via twitter.

Recent Articles All Articles Mini-Series Progress Updates Tags About Me Go Courses

©2024 Jonathan Calhoun. All rights reserved.