Overview
When building applications that make many API calls, optimization is essential to stay within rate limits and provide responsive experiences.
Batch Requests: Execute multiple operations efficiently
Batch: Concurrent requests for maximum throughputBatchGet: Concurrent with built-in rate limitingBatchSequential: Sequential execution for ordered operationsBatchWithCallback: Progress tracking for large operations
Caching: Reduce redundant API calls
- In-memory cache with configurable TTL
- Cache key isolation for multi-tenant apps
- Manual invalidation when needed
Rate Limiting: Stay within Twitch’s limits (800 requests/minute)
- Automatic tracking via response headers
- Wait functions for graceful handling
Middleware: Extend client functionality
- Logging, retries, metrics, custom headers
- Chainable for complex behaviors
Batch Requests
Execute multiple API calls concurrently for maximum throughput:
package main
import (
"context"
"fmt"
"log"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
client := helix.NewClient("your-client-id", authClient)
// Batch get users (concurrent requests)
userLogins := []string{"user1", "user2", "user3", "user4", "user5"}
results, err := helix.Batch(ctx, userLogins, func(ctx context.Context, login string) (*helix.GetUsersResponse, error) {
return client.GetUsers(ctx, &helix.GetUsersParams{
Logins: []string{login},
})
})
if err != nil {
log.Fatal(err)
}
for i, result := range results {
if result.Error != nil {
fmt.Printf("Failed to get user %s: %v\n", userLogins[i], result.Error)
continue
}
if len(result.Value.Data) > 0 {
fmt.Printf("User: %s (ID: %s)\n", result.Value.Data[0].DisplayName, result.Value.Data[0].ID)
}
}
}
BatchGet with Built-in Rate Limiting
package main
import (
"context"
"fmt"
"log"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
client := helix.NewClient("your-client-id", authClient)
// Get multiple channel information with automatic rate limiting
broadcasterIDs := []string{"12345", "67890", "11111", "22222", "33333"}
results, err := helix.BatchGet(ctx, client, broadcasterIDs,
func(ctx context.Context, client *helix.Client, id string) (*helix.GetChannelInformationResponse, error) {
return client.GetChannelInformation(ctx, &helix.GetChannelInformationParams{
BroadcasterIDs: []string{id},
})
},
)
if err != nil {
log.Fatal(err)
}
for _, result := range results {
if result.Error != nil {
fmt.Printf("Error: %v\n", result.Error)
continue
}
if len(result.Value.Data) > 0 {
ch := result.Value.Data[0]
fmt.Printf("Channel: %s - %s\n", ch.BroadcasterName, ch.Title)
}
}
}
Sequential Batch (Rate Limited)
package main
import (
"context"
"fmt"
"log"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
client := helix.NewClient("your-client-id", authClient)
// Process sequentially (useful when order matters or for heavy operations)
userIDs := []string{"12345", "67890", "11111"}
results, err := helix.BatchSequential(ctx, userIDs, func(ctx context.Context, id string) (*helix.GetUsersResponse, error) {
return client.GetUsers(ctx, &helix.GetUsersParams{
IDs: []string{id},
})
})
if err != nil {
log.Fatal(err)
}
for _, result := range results {
if result.Error == nil && len(result.Value.Data) > 0 {
fmt.Printf("User: %s\n", result.Value.Data[0].DisplayName)
}
}
}
Batch with Callback
package main
import (
"context"
"fmt"
"log"
"sync/atomic"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
client := helix.NewClient("your-client-id", authClient)
userIDs := make([]string, 100)
for i := range userIDs {
userIDs[i] = fmt.Sprintf("%d", 10000+i)
}
var processed int64
// Process with progress callback
err := helix.BatchWithCallback(ctx, userIDs,
func(ctx context.Context, id string) (*helix.GetUsersResponse, error) {
return client.GetUsers(ctx, &helix.GetUsersParams{
IDs: []string{id},
})
},
func(input string, result *helix.GetUsersResponse, err error) {
atomic.AddInt64(&processed, 1)
current := atomic.LoadInt64(&processed)
if current%10 == 0 {
fmt.Printf("Progress: %d/%d\n", current, len(userIDs))
}
},
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Processed %d users\n", processed)
}
Caching Setup
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
// Create client with caching enabled
cache := helix.NewMemoryCache(5 * time.Minute) // 5 minute TTL
client := helix.NewClient("your-client-id", authClient,
helix.WithCache(cache),
)
// First request - hits API
start := time.Now()
users1, _ := client.GetUsers(ctx, &helix.GetUsersParams{
Logins: []string{"twitchdev"},
})
fmt.Printf("First request: %v (API call)\n", time.Since(start))
// Second request - hits cache
start = time.Now()
users2, _ := client.GetUsers(ctx, &helix.GetUsersParams{
Logins: []string{"twitchdev"},
})
fmt.Printf("Second request: %v (cached)\n", time.Since(start))
fmt.Printf("Same data: %v\n", users1.Data[0].ID == users2.Data[0].ID)
// Invalidate cache for specific endpoint
client.InvalidateCache(ctx, "users")
// Third request - hits API again
start = time.Now()
_, _ = client.GetUsers(ctx, &helix.GetUsersParams{
Logins: []string{"twitchdev"},
})
fmt.Printf("Third request: %v (API call after invalidation)\n", time.Since(start))
}
Custom Cache Implementation
package main
import (
"context"
"sync"
"time"
"github.com/Its-donkey/kappopher/helix"
)
// RedisCache implements helix.Cache using Redis
type RedisCache struct {
// Add your Redis client here
mu sync.RWMutex
data map[string]cacheEntry
}
type cacheEntry struct {
value []byte
expiresAt time.Time
}
func NewRedisCache() *RedisCache {
return &RedisCache{
data: make(map[string]cacheEntry),
}
}
func (c *RedisCache) Get(ctx context.Context, key string) ([]byte, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
entry, ok := c.data[key]
if !ok || time.Now().After(entry.expiresAt) {
return nil, false
}
return entry.value, true
}
func (c *RedisCache) Set(ctx context.Context, key string, value []byte, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = cacheEntry{
value: value,
expiresAt: time.Now().Add(ttl),
}
}
func (c *RedisCache) Delete(ctx context.Context, key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.data, key)
}
func main() {
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
cache := NewRedisCache()
client := helix.NewClient("your-client-id", authClient,
helix.WithCache(cache),
helix.WithCacheTTL(10*time.Minute),
)
// Use client normally - caching is automatic
_ = client
}
Cache Key Isolation
package main
import (
"context"
"fmt"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
// Generate cache key with context isolation
// This prevents cache collisions between different tokens/environments
key := helix.CacheKeyWithContext("users", "https://api.twitch.tv", "access-token-123")
fmt.Printf("Cache key: %s\n", key)
// Generate token hash for cache isolation
hash := helix.TokenHash("access-token-123")
fmt.Printf("Token hash: %s\n", hash)
// Invalidate cache with context
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
})
cache := helix.NewMemoryCache(5 * time.Minute)
client := helix.NewClient("your-client-id", authClient, helix.WithCache(cache))
client.InvalidateCacheWithContext(ctx, "users")
}
Rate Limiting
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
client := helix.NewClient("your-client-id", authClient)
// Make a request
_, _ = client.GetUsers(ctx, &helix.GetUsersParams{
Logins: []string{"twitchdev"},
})
// Check rate limit status
rateLimit := client.GetRateLimitInfo()
fmt.Printf("Rate Limit: %d/%d\n", rateLimit.Remaining, rateLimit.Limit)
fmt.Printf("Resets at: %s\n", rateLimit.Reset.Format(time.RFC3339))
// Wait if rate limited
if rateLimit.Remaining < 10 {
waitTime := client.WaitForRateLimit(ctx)
if waitTime > 0 {
fmt.Printf("Rate limited, waiting %v\n", waitTime)
time.Sleep(waitTime)
}
}
// Continue making requests...
}
Middleware
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
"github.com/Its-donkey/kappopher/helix"
)
// LoggingMiddleware logs all API requests
func LoggingMiddleware(next helix.RoundTripper) helix.RoundTripper {
return helix.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
start := time.Now()
log.Printf("→ %s %s", req.Method, req.URL.Path)
resp, err := next.RoundTrip(req)
duration := time.Since(start)
if err != nil {
log.Printf("← ERROR: %v (%v)", err, duration)
} else {
log.Printf("← %d %s (%v)", resp.StatusCode, resp.Status, duration)
}
return resp, err
})
}
// RetryMiddleware retries failed requests
func RetryMiddleware(maxRetries int) helix.Middleware {
return func(next helix.RoundTripper) helix.RoundTripper {
return helix.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
for i := 0; i <= maxRetries; i++ {
resp, err = next.RoundTrip(req)
if err == nil && resp.StatusCode < 500 {
return resp, nil
}
if i < maxRetries {
time.Sleep(time.Duration(i+1) * time.Second)
log.Printf("Retrying request (attempt %d/%d)", i+2, maxRetries+1)
}
}
return resp, err
})
}
}
// MetricsMiddleware tracks request metrics
type Metrics struct {
TotalRequests int64
Errors int64
TotalLatency time.Duration
}
func MetricsMiddleware(metrics *Metrics) helix.Middleware {
return func(next helix.RoundTripper) helix.RoundTripper {
return helix.RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
start := time.Now()
metrics.TotalRequests++
resp, err := next.RoundTrip(req)
metrics.TotalLatency += time.Since(start)
if err != nil || (resp != nil && resp.StatusCode >= 400) {
metrics.Errors++
}
return resp, err
})
}
}
func main() {
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
metrics := &Metrics{}
// Create client with middleware stack
client := helix.NewClient("your-client-id", authClient,
helix.WithMiddleware(LoggingMiddleware),
helix.WithMiddleware(RetryMiddleware(3)),
helix.WithMiddleware(MetricsMiddleware(metrics)),
)
ctx := context.Background()
// Make some requests
for i := 0; i < 5; i++ {
_, _ = client.GetUsers(ctx, &helix.GetUsersParams{
Logins: []string{"twitchdev"},
})
}
// Print metrics
fmt.Printf("\n=== Metrics ===\n")
fmt.Printf("Total Requests: %d\n", metrics.TotalRequests)
fmt.Printf("Errors: %d\n", metrics.Errors)
fmt.Printf("Average Latency: %v\n", metrics.TotalLatency/time.Duration(metrics.TotalRequests))
}
Concurrent Requests with Different Tokens
When endpoints require user-specific tokens (e.g., Get Channel Followers requires moderator:read:followers), use WithToken to override the client token per-request:
package main
import (
"context"
"fmt"
"log"
"sync"
"github.com/Its-donkey/kappopher/helix"
)
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
client := helix.NewClient("your-client-id", authClient)
// Each channel's user token (obtained via Authorization Code flow)
type ChannelToken struct {
BroadcasterID string
Token *helix.Token
}
channels := []ChannelToken{
{BroadcasterID: "111", Token: &helix.Token{AccessToken: "token-a"}},
{BroadcasterID: "222", Token: &helix.Token{AccessToken: "token-b"}},
{BroadcasterID: "333", Token: &helix.Token{AccessToken: "token-c"}},
}
var wg sync.WaitGroup
for _, ch := range channels {
wg.Add(1)
go func(ch ChannelToken) {
defer wg.Done()
// Override token for this request only
reqCtx := helix.WithToken(ctx, ch.Token)
followers, err := client.GetChannelFollowers(reqCtx, &helix.GetChannelFollowersParams{
BroadcasterID: ch.BroadcasterID,
})
if err != nil {
log.Printf("Error: %v", err)
return
}
fmt.Printf("Channel %s: %d followers\n", ch.BroadcasterID, *followers.Total)
}(ch)
}
wg.Wait()
}
The client’s rate limiter, cache, and middleware are shared across all requests regardless of which token is used. When caching is enabled, requests with different tokens produce different cache keys automatically.
Complete Example: Efficient Multi-Channel Dashboard
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/Its-donkey/kappopher/helix"
)
type ChannelData struct {
User *helix.User
Channel *helix.ChannelInformation
Stream *helix.Stream
}
func main() {
ctx := context.Background()
authClient := helix.NewAuthClient(helix.AuthConfig{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
})
_, _ = authClient.GetAppAccessToken(ctx)
// Setup client with caching
cache := helix.NewMemoryCache(1 * time.Minute)
client := helix.NewClient("your-client-id", authClient,
helix.WithCache(cache),
)
// Channels to monitor
channels := []string{"streamer1", "streamer2", "streamer3", "streamer4", "streamer5"}
// Fetch all data concurrently
channelData := make(map[string]*ChannelData)
var mu sync.Mutex
// Get users
userResults, _ := helix.Batch(ctx, channels, func(ctx context.Context, login string) (*helix.GetUsersResponse, error) {
return client.GetUsers(ctx, &helix.GetUsersParams{Logins: []string{login}})
})
for i, result := range userResults {
if result.Error == nil && len(result.Value.Data) > 0 {
mu.Lock()
channelData[channels[i]] = &ChannelData{User: &result.Value.Data[0]}
mu.Unlock()
}
}
// Get user IDs for next requests
var userIDs []string
for _, login := range channels {
if data, ok := channelData[login]; ok && data.User != nil {
userIDs = append(userIDs, data.User.ID)
}
}
// Get channel info and streams concurrently
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
channelResults, _ := helix.Batch(ctx, userIDs, func(ctx context.Context, id string) (*helix.GetChannelInformationResponse, error) {
return client.GetChannelInformation(ctx, &helix.GetChannelInformationParams{BroadcasterIDs: []string{id}})
})
for i, result := range channelResults {
if result.Error == nil && len(result.Value.Data) > 0 {
mu.Lock()
for _, data := range channelData {
if data.User != nil && data.User.ID == userIDs[i] {
data.Channel = &result.Value.Data[0]
break
}
}
mu.Unlock()
}
}
}()
go func() {
defer wg.Done()
streams, _ := client.GetStreams(ctx, &helix.GetStreamsParams{UserIDs: userIDs})
mu.Lock()
for _, stream := range streams.Data {
for _, data := range channelData {
if data.User != nil && data.User.ID == stream.UserID {
s := stream // Copy
data.Stream = &s
break
}
}
}
mu.Unlock()
}()
wg.Wait()
// Display dashboard
fmt.Println("=== Channel Dashboard ===\n")
for login, data := range channelData {
if data.User == nil {
fmt.Printf("❌ %s: Not found\n", login)
continue
}
status := "⚫ Offline"
viewers := 0
if data.Stream != nil {
status = "🔴 LIVE"
viewers = data.Stream.ViewerCount
}
title := "N/A"
game := "N/A"
if data.Channel != nil {
title = data.Channel.Title
game = data.Channel.GameName
}
fmt.Printf("%s %s\n", status, data.User.DisplayName)
fmt.Printf(" Title: %s\n", title)
fmt.Printf(" Game: %s\n", game)
if viewers > 0 {
fmt.Printf(" Viewers: %d\n", viewers)
}
fmt.Println()
}
// Show rate limit status
rateLimit := client.GetRateLimitInfo()
fmt.Printf("Rate Limit: %d/%d remaining\n", rateLimit.Remaining, rateLimit.Limit)
}