Dragonfly

Stop Ad Fatigue with Dragonfly’s Built-In Rate Limiting

Rate limiting with CL.THROTTLE prevents ad fatigue by controlling impression frequency—allowing controlled bursts while maintaining smooth ad delivery at scale.

Stop Ad Fatigue with Dragonfly’s Built-In Rate Limiting

If you’re a company operating in the AdTech space, you understand how important it is to optimize ad impressions. Show too few ads, and you’re leaving money on the table. Show too many of the same ads, and people get annoyed. Ensuring ads reach your target audience effectively without becoming intrusive can be a struggle to manage.

The Ad Exposure Challenge

Ad fatigue is a known phenomenon in digital advertising. While repeated exposure increases brand retention, bombarding users with the same ad too often can cause annoyance and negative brand perception. Finding the right balance between ad exposure and user experience is crucial. One of the key strategies used to maintain this balance is frequency capping, also known as rate limiting.

What is Frequency Capping

Frequency capping ensures that a user does not see the same ad too many times within a specific period. For example, as an advertiser, you might want to set a cap of no more than seven impressions per day for a particular user for a specific ad. To implement frequency capping effectively, AdTech companies need a fast and scalable rate-limiting mechanism. Traditional solutions are no longer able to support the scale at which today’s growing AdTech platforms operate.

Dragonfly is able to do significantly better in this area thanks to its native support for rate limiting. In this blog, we’ll learn about what Dragonfly is and what features it offers that make it the perfect choice for such a use case.

What is Dragonfly?

Dragonfly is a high-performance in-memory data store designed for modern computing. It functions as a seamless drop-in replacement for traditional in-memory database solutions such as Redis or Valkey, delivering substantially enhanced performance while maintaining lower operational costs. This efficiency is achieved through its architecture, which has been engineered from the ground up to leverage the capabilities of modern multi-core cloud hardware infrastructure. By leveraging architectural advancements and advanced data structures, Dragonfly ensures optimal resource utilization and exceptional throughput.

Let’s take a look at how you can use the traditional approach of rate limiting, that is, using counters with auto-expiration with Dragonfly. Then we’ll explore an even better way to do this with Dragonfly.

Counters with Auto-Expiration to Implement Rate Limiting

The most straightforward implementation for restricting how often a user sees a specific ad uses a fixed-window counter approach. This approach works by incrementing a counter each time an ad is shown and resetting it after a predefined period (e.g., every hour).

Let's consider an example where a user should not see a specific ad more than 10 times per hour. We track this using a key structured as {ad_id}:{user_id}:{current_hour}, ensuring that impressions are counted within hourly windows.

Here’s how we can implement this in Go using Dragonfly:

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

var dragonfly *redis.Client

func init() {
	dragonfly = redis.NewClient(&redis.Options{
		Addr: "localhost:6379", // Dragonfly address
	})
}

// CurrentHour returns the start of the current hour in UTC
func CurrentHour() time.Time {
	now := time.Now().UTC()
	return time.Date(
		now.Year(), now.Month(), now.Day(),
		now.Hour(), 0, 0, 0, now.Location())
}

// CurrentHourStr returns the current hour in "2006-01-02-15" format
func CurrentHourStr() string {
	return CurrentHour().Format("2006-01-02-15")
}

// IncrementAdCounter increments and expires the counter for a specific ad and user
func IncrementAdCounter(ctx context.Context, adID string, userID string) (int64, error) {
	key := fmt.Sprintf("%s:%s:%s", adID, userID, CurrentHourStr())

	pipe := dragonfly.Pipeline()
	incrCmd := pipe.Incr(ctx, key)
	pipe.Expire(ctx, key, 2*time.Hour) // Expire after current hour passes

	_, err := pipe.Exec(ctx)
	if err != nil {
		return 0, fmt.Errorf("pipeline exec error: %w", err)
	}

	return incrCmd.Val(), nil
}

func main() {
	var (
		ctx    = context.Background()
		adID   = "123"
		userID = "user_456"
	)

	count, err := IncrementAdCounter(ctx, adID, userID)
	if err != nil {
		fmt.Printf("error incrementing counter: %v\n", err)
		return
	}

	if count > 10 {
		fmt.Printf("ad %s should not be shown to user %s anymore this hour\n", adID, userID)
	} else {
		fmt.Printf("serving ad %s to user %s (impression %d)\n", adID, userID, count)
	}
}

This approach works by:

  1. Creating a composite key combining ad ID, user ID, and current hour
  2. Incrementing the counter for each impression. If the key doesn’t exist, the INCR command automatically creates the key with the value 1.
  3. Setting an expiration to automatically clean up old data
  4. Checking if the count exceeds the limit (10 in this case)

While this fixed-window approach works, it has several notable limitations. The rigidity of fixed windows means all 10 impressions could theoretically be served in the first minute of the hour, rather than being evenly distributed. It also requires careful expiration management. In our example, we set a 2-hour expiration as a safety buffer to prevent premature data loss.

Additionally, edge cases can occur around window boundaries, where users might receive double the intended limit if their requests span across two time windows, such as at the top of the hour.

At 10:59 PM

  • The user sees 10 impressions just before the hour ends.
  • All these are counted in the 2025-04-16-22 window (10 PM).

Now the hour changes...

At 11:00 PM

  • The counter resets automatically (new hour = new key, like 2025-04-16-23).
  • The user can now see 10 more impressions right after the clock ticks over.

The result? Your user sees 20 ads in just a few minutes, even though your "rate limit" is 10 per hour. This creates a poor user experience and potentially reduces engagement with your platform.

A Better Solution: Dragonfly’s Built-in CL.THROTTLE

Dragonfly offers a better solution with its built in CL.THROTTLE command, which uses a leaky bucket algorithm for more sophisticated rate limiting. Think of it like this:

You have a bucket that holds a maximum number of tokens, representing your rate limit. These tokens leak out at a steady rate (your time window). When new requests are made, they add tokens to the bucket. If the bucket overflows, the request gets denied.

This approach offers your ad tech platform several benefits:

  • Smoother distribution of requests over time since requests are spread more evenly
  • Better handling of short bursts of traffic, as long as they don't exceed the overall rate
  • Avoiding fixed windows so there are no edge cases at window boundaries

Unlike Redis, which requires the redis-cell module, Dragonfly implements CL.THROTTLE natively. We’re especially thankful to Brandur Leach, the original author of redis-cell, for introducing an implementation of this algorithm to the ecosystem. In his blog post, he highlights the advantages of implementing CL.THROTTLE natively in Dragonfly. His work has made it easier for others to adopt this powerful rate-limiting approach.

Here’s how you can modify the code we saw above to use the CL.THROTTLE command instead:

package main

import (
	"context"
	"fmt"

	"github.com/redis/go-redis/v9"
)

var dragonfly *redis.Client

func init() {
	dragonfly = redis.NewClient(&redis.Options{
		Addr: "localhost:6379", // Dragonfly address
	})
}

func CheckAdFrequency(ctx context.Context, adID string, userID string) (bool, error) {
	// Create a unique key for this ad-user combination
	key := fmt.Sprintf("ad_throttle:%s:%s", adID, userID)

	// CL.THROTTLE parameters:
	// key - our composite key
	// max_burst - 0
	// count_per_period - 10
	// period - 3600 seconds (1 hour)
	// quantity - 1 (we're checking for one impression)
	res, err := dragonfly.Do(ctx, "CL.THROTTLE", key, 0, 10, 3600, 1).Result()
	if err != nil {
		return false, fmt.Errorf("throttle command error: %w", err)
	}

	// The response is an array of 5 values:
	// [0] - 0 (allowed) or 1 (limited)
	// [1] - total limit, which is max_burst+1, equivalent to X-RateLimit-Limit
	// [2] - remaining limit, equivalent to X-RateLimit-Remaining
	// [3] - seconds until next retry, equivalent to Retry-After
	// [4] - seconds until limit resets, equivalent to X-RateLimit-Reset
	response := res.([]interface{})
	limited := response[0].(int64)

	return limited == 0, nil
}

func main() {
	var (
		ctx    = context.Background()
		adID   = "123"
		userID = "user_456"
	)

	allowed, err := CheckAdFrequency(ctx, adID, userID)
	if err != nil {
		fmt.Printf("error checking frequency: %v\n", err)
		return
	}

	if allowed {
		fmt.Printf("serving ad %s to user %s\n", adID, userID)
	} else {
		fmt.Printf("frequency limit reached for ad %s and user %s\n", adID, userID)
	}
}

This implementation uses CL.THROTTLE to simplify frequency capping by handling rate limiting natively. Unlike basic counters, it smoothly distributes impressions while strictly enforcing limits—preventing users from being overwhelmed by ads in short bursts. Now, let’s break down each argument passed to the CL.THROTTLE command:

$> CL.THROTTLE "ad_throttle:123:user_456" 0 10 3600 1
#                             ^           ^ ^  ^    ^
#                             |           | |  |    └───── use 1 token for this ad delivery request
#                             |           | └──┴────────── 10 tokens / 3600 seconds
#                             |           └─────────────── 0 burst
#                             └─────────────────────────── key = "ad_throttle:123:user_456"

With these arguments, we control how often an ad (ID 123) is delivered to a user (ID user_456), allowing precisely 10 impressions per hour. This means an ad delivery request is permitted every 360 seconds, and any additional requests within that window are rejected—effectively distributing impressions evenly without allowing bursts.

However, this configuration is quite restrictive. What if we want to permit a small burst of deliveries to the same user? In that case, we can adjust the arguments as follows:

$> CL.THROTTLE "ad_throttle:123:user_456" 2 8 3600 1
#                             ^           ^ ^ ^    ^
#                             |           | | |    └────── use 1 token for this ad delivery request
#                             |           | └─┴─────────── 8 tokens / 3600 seconds
#                             |           └─────────────── max burst = 2
#                             └─────────────────────────── key = "ad_throttle:123:user_456"

By adjusting the max_burst argument, we allow an initial burst of 2 delivery requests, followed by 8 additional requests over the next hour. This example balances burst flexibility with strict rate-limiting. Overall, the CL.THROTTLE command can be an ideal solution for high-scale AdTech platforms.

Dragonfly Solves the Problems of Frequency Capping in Advertising

Dragonfly provides AdTech companies with powerful tools for implementing frequency capping at scale. When compared to traditional solutions, Dragonfly offers you the built-in CL.THROTTLE command that:

  • Handles complex rate-limiting scenarios with a single command
  • Provides more natural impression distribution than fixed windows
  • Reduces operational complexity compared to module-based solutions
  • Delivers excellent performance for high-throughput ad platforms

While both fixed-window counters and CL.THROTTLE limit when ads are served, here’s a summary of their differences:

Fixed Window Counters

CL.THROTTLE

You need absolute limits per exact time windows

You want smoother distribution of impressions

Your limits reset strictly at window boundaries

You need to handle bursts more gracefully

Simple and easier to understand

A bit of learning at first, becomes intuitive over time

Whether you're building a new ad platform or optimizing an existing one, Dragonfly's native rate limiting capabilities can help you deliver better user experiences while maintaining precise control over ad frequency. If you're currently using Redis for rate limiting and struggling with module management or performance bottlenecks, it’s time to consider Dragonfly. You can get started immediately by checking out our cloud offering here!

Dragonfly Wings

Stay up to date on all things Dragonfly

Join our community for unparalleled support and insights

Join

Switch & save up to 80%

Dragonfly is fully compatible with the Redis ecosystem and requires no code changes to implement. Instantly experience up to a 25X boost in performance and 80% reduction in cost