Skip to main content

Rust SDK

The official GrowthBook SDK for Rust. This SDK provides a powerful, type-safe way to integrate feature flagging and A/B testing into your Rust applications with automatic feature refreshing, caching, and tracking callbacks.

Rust SDK Resources
v0.0.3
growthbook-rustcrates.ioRust example appGet help on Slack

Requirements

  • Rust 1.70.0 or higher (as specified in rust-toolchain)
  • Async runtime: Tokio (recommended) or any async-std compatible runtime

Installation

Add this to your Cargo.toml:

[dependencies]
growthbook-rust = "0.0.3"
tokio = { version = "1", features = ["full"] }
serde_json = "1.0"

Or install via cargo:

cargo add growthbook-rust

Quick Usage

Step 1: Initialize the Client

use growthbook_rust::client::GrowthBookClientBuilder;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a GrowthBook client with auto-refresh enabled
let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.ttl(Duration::from_secs(60)) // Cache TTL
.auto_refresh(true) // Enable background updates
.refresh_interval(Duration::from_secs(30))
.build()
.await?;

Ok(())
}

Step 2: Evaluate Feature Flags

// Simple boolean check
if client.is_on("my-feature", None) {
println!("Feature is enabled!");
}

// Get typed feature value
let result = client.feature_result("button-color", None);
if let Ok(color) = result.value_as::<String>() {
println!("Button color: {}", color);
}

// With user attributes
use growthbook_rust::model_public::{GrowthBookAttribute, GrowthBookAttributeValue};
use std::collections::HashMap;

let mut attrs = Vec::new();
attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String("user-123".to_string())
));

if client.is_on("premium-feature", Some(attrs)) {
println!("Premium feature enabled for this user!");
}

Loading Features and Experiments

The Rust SDK provides multiple ways to load and refresh feature definitions from the GrowthBook API.

Built-in Fetching and Auto-Refresh

The recommended approach is to use the builder with auto-refresh enabled:

use growthbook_rust::client::GrowthBookClientBuilder;
use std::time::Duration;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
// Cache settings
.ttl(Duration::from_secs(60)) // How long to cache features
// Auto-refresh settings
.auto_refresh(true) // Enable background sync
.refresh_interval(Duration::from_secs(30)) // Refresh every 30 seconds
.build()
.await?;

// Features are now loaded and will refresh automatically
// in the background every 30 seconds

Ok(())
}

How Auto-Refresh Works

When auto_refresh is enabled:

  1. Features are fetched immediately during build()
  2. A background task spawns that periodically fetches updates
  3. The cache is updated automatically without blocking your application
  4. The refresh task runs until the client is dropped

Benefits:

  • Always up-to-date features without manual intervention
  • Non-blocking updates in the background
  • Configurable refresh intervals
  • Automatic retry logic on failures

Manual Refresh

You can manually trigger a feature refresh at any time:

// Force a feature refresh
client.refresh().await;

// Useful for scenarios like:
// - User login/logout events
// - Navigation changes
// - Manual override in admin panels

Starting with Initial Features

If you want to start with a specific set of features (e.g., from a file or cache) and then enable updates:

use serde_json::json;

let initial_features = json!({
"my-feature": {
"defaultValue": true
},
"button-color": {
"defaultValue": "blue"
}
});

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.features_json(initial_features)? // Start with these features
.auto_refresh(true) // Still enable auto-refresh
.refresh_interval(Duration::from_secs(30))
.build()
.await?;

// Client starts with initial features and updates them in the background

Disabling Auto-Refresh

For use cases where you want full control (e.g., testing, edge workers, or custom update logic):

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.auto_refresh(false) // Disable background sync
.build()
.await?;

// Manually control when to refresh
client.refresh().await;

Refresh Callback

Get notified when features are refreshed (useful for logging and debugging):

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.auto_refresh(true)
.refresh_interval(Duration::from_secs(30))
.add_on_refresh(Box::new(|| {
println!("✅ Features refreshed at {}", chrono::Utc::now());
// Update metrics, logs, or caches
}))
.build()
.await?;

Configuration via Environment Variables

The SDK supports configuration through environment variables:

VariableDescriptionDefault
GB_HTTP_CLIENT_TIMEOUTHTTP request timeout10 seconds
GB_UPDATE_INTERVALAuto-refresh interval60 seconds
GB_URLGrowthBook API URL-
GB_SDK_KEYSDK client key-
// Environment variables will be used if not explicitly set
let client = GrowthBookClientBuilder::new()
// api_url and client_key will be read from GB_URL and GB_SDK_KEY
.build()
.await?;

Encrypted Features

For enhanced security, GrowthBook supports encrypted feature payloads. This prevents sensitive feature configurations and PII data from being exposed in transit or in logs.

Setup

  1. Enable encryption in your GrowthBook SDK Connection settings
  2. Copy the decryption key shown in the GrowthBook dashboard
  3. Pass the key to the SDK during initialization
use growthbook_rust::client::GrowthBookClientBuilder;

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.decryption_key("your-decryption-key-here".to_string()) // Add this
.build()
.await?;

// Features are automatically decrypted when loaded

How It Works

  • Feature payloads from the API are encrypted using AES-256
  • The SDK automatically decrypts them using your decryption key
  • Decryption happens transparently - your code doesn't change
  • Invalid keys or corrupted data will cause initialization to fail

Security Best Practices

use std::env;

// ✅ DO: Load from environment variables
let decryption_key = env::var("GROWTHBOOK_DECRYPTION_KEY")
.expect("GROWTHBOOK_DECRYPTION_KEY must be set");

let client = GrowthBookClientBuilder::new()
.api_url(env::var("GROWTHBOOK_API_URL").unwrap())
.client_key(env::var("GROWTHBOOK_CLIENT_KEY").unwrap())
.decryption_key(decryption_key)
.build()
.await?;

// ❌ DON'T: Hardcode keys in source code
// let decryption_key = "key-123456789".to_string(); // NEVER DO THIS!

Recommendations:

  • Use environment variables or secret management systems (AWS Secrets Manager, HashiCorp Vault)
  • Rotate keys regularly
  • Use different keys for different environments (dev, staging, production)
  • Never commit keys to version control

Error Handling

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.decryption_key("wrong-key".to_string())
.build()
.await;

match client {
Ok(client) => {
println!("Client initialized successfully");
},
Err(e) => {
eprintln!("Failed to initialize client: {}", e);
// Could be due to:
// - Wrong decryption key
// - Network issues
// - Invalid client key
// Fall back to safe defaults
}
}

Attributes

Attributes are used for two main purposes:

  1. Feature targeting - Show different values to different user segments
  2. Experiment bucketing - Ensure consistent variation assignment

Setting Global Attributes

You can set default attributes that apply to all feature evaluations:

use std::collections::HashMap;
use growthbook_rust::model_public::GrowthBookAttributeValue;

// Define global attributes during client creation
let mut global_attrs = HashMap::new();
global_attrs.insert(
"tenantId".to_string(),
GrowthBookAttributeValue::String("acme-corp".to_string())
);
global_attrs.insert(
"plan".to_string(),
GrowthBookAttributeValue::String("enterprise".to_string())
);

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.attributes(global_attrs) // Set global attributes
.build()
.await?;

Per-Evaluation Attributes

You can override or supplement global attributes on a per-check basis:

use growthbook_rust::model_public::{GrowthBookAttribute, GrowthBookAttributeValue};

// Create per-evaluation attributes
let mut user_attrs = Vec::new();
user_attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String("user-456".to_string())
));
user_attrs.push(GrowthBookAttribute::new(
"country".to_string(),
GrowthBookAttributeValue::String("US".to_string())
));
user_attrs.push(GrowthBookAttribute::new(
"isPremium".to_string(),
GrowthBookAttributeValue::Bool(true)
));

// These attributes are merged with global attributes
if client.is_on("new-dashboard", Some(user_attrs)) {
// Show new dashboard
}

Attribute Types

The SDK supports all JSON data types as attributes:

use growthbook_rust::model_public::GrowthBookAttributeValue;
use serde_json::json;

let mut attrs = Vec::new();

// String
attrs.push(GrowthBookAttribute::new(
"email".to_string(),
GrowthBookAttributeValue::String("user@example.com".to_string())
));

// Number (integer)
attrs.push(GrowthBookAttribute::new(
"age".to_string(),
GrowthBookAttributeValue::Number(serde_json::Number::from(25))
));

// Boolean
attrs.push(GrowthBookAttribute::new(
"isLoggedIn".to_string(),
GrowthBookAttributeValue::Bool(true)
));

// Array
attrs.push(GrowthBookAttribute::new(
"tags".to_string(),
GrowthBookAttributeValue::Array(vec![
json!("premium"),
json!("beta-tester")
])
));

// Object
attrs.push(GrowthBookAttribute::new(
"company".to_string(),
GrowthBookAttributeValue::Object(json!({
"id": "company-123",
"name": "Acme Corp"
}))
));

Common Attribute Patterns

// Web application attributes
let mut web_attrs = Vec::new();
web_attrs.push(GrowthBookAttribute::new("id".to_string(),
GrowthBookAttributeValue::String(user_id)));
web_attrs.push(GrowthBookAttribute::new("url".to_string(),
GrowthBookAttributeValue::String(request.uri().to_string())));
web_attrs.push(GrowthBookAttribute::new("userAgent".to_string(),
GrowthBookAttributeValue::String(user_agent)));
web_attrs.push(GrowthBookAttribute::new("country".to_string(),
GrowthBookAttributeValue::String(geo_ip_country)));

// API attributes
let mut api_attrs = Vec::new();
api_attrs.push(GrowthBookAttribute::new("apiKey".to_string(),
GrowthBookAttributeValue::String(api_key)));
api_attrs.push(GrowthBookAttribute::new("requestsToday".to_string(),
GrowthBookAttributeValue::Number(serde_json::Number::from(request_count))));
api_attrs.push(GrowthBookAttribute::new("tier".to_string(),
GrowthBookAttributeValue::String("premium".to_string())));

Attribute Merging Behavior

When you provide per-evaluation attributes:

  1. They are merged with global attributes
  2. Per-evaluation attributes take precedence over global ones
  3. This allows you to set common attributes globally and override them as needed
// Global attributes
let mut global = HashMap::new();
global.insert("tenantId".to_string(),
GrowthBookAttributeValue::String("tenant-1".to_string()));
global.insert("plan".to_string(),
GrowthBookAttributeValue::String("free".to_string()));

let client = GrowthBookClientBuilder::new()
// ... other settings ...
.attributes(global)
.build()
.await?;

// Per-evaluation attributes (overrides "plan")
let mut user_attrs = Vec::new();
user_attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String("user-123".to_string())
));
user_attrs.push(GrowthBookAttribute::new(
"plan".to_string(), // This overrides the global "plan"
GrowthBookAttributeValue::String("premium".to_string())
));

// Final attributes used: { tenantId: "tenant-1", plan: "premium", userId: "user-123" }
let result = client.is_on("premium-feature", Some(user_attrs));

Using Features

The SDK provides multiple methods for evaluating features with different levels of detail.

Basic Feature Checks

is_on() - Simple Boolean Check

Check if a feature is enabled (evaluates to a truthy value):

// Without attributes
if client.is_on("new-navigation", None) {
println!("Show new navigation");
}

// With attributes
let mut attrs = Vec::new();
attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String("user-123".to_string())
));

if client.is_on("beta-feature", Some(attrs)) {
println!("User is in beta program");
}

is_off() - Inverse Boolean Check

Check if a feature is disabled (evaluates to a falsy value):

if client.is_off("maintenance-mode", None) {
// Allow normal operations
process_request();
}

Getting Feature Values

feature_result() - Get Detailed Feature Information

Get the full feature result with metadata:

let result = client.feature_result("button-color", None);

// Access the raw value
println!("Value: {:?}", result.value);

// Check if enabled
if result.on {
println!("Feature is on");
}

// Get typed value with error handling
match result.value_as::<String>() {
Ok(color) => println!("Button color: {}", color),
Err(e) => println!("Error getting value: {}", e),
}

// Understand why this value was assigned
println!("Source: {:?}", result.source); // e.g., "experiment", "force", "defaultValue"

Type-Safe Feature Values

The value_as::<T>() method provides type-safe access to feature values:

// String values
let color_result = client.feature_result("button-color", None);
let color: String = color_result.value_as::<String>()
.unwrap_or("blue".to_string());

// Integer values
let max_items_result = client.feature_result("max-items", None);
let max_items: i32 = max_items_result.value_as::<i32>()
.unwrap_or(10);

// Boolean values
let enabled_result = client.feature_result("new-feature", None);
let enabled: bool = enabled_result.value_as::<bool>()
.unwrap_or(false);

// Complex types (JSON)
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
struct ThemeConfig {
primary_color: String,
secondary_color: String,
font_size: i32,
}

let theme_result = client.feature_result("theme-config", None);
match theme_result.value_as::<ThemeConfig>() {
Ok(theme) => {
println!("Primary: {}, Font: {}", theme.primary_color, theme.font_size);
},
Err(_) => {
// Use default theme
let default_theme = ThemeConfig {
primary_color: "blue".to_string(),
secondary_color: "gray".to_string(),
font_size: 16,
};
}
}

Feature Result Properties

The FeatureResult struct contains detailed information about the feature evaluation:

let result = client.feature_result("my-feature", None);

// The actual feature value (Option<serde_json::Value>)
println!("Value: {:?}", result.value);

// Boolean helpers
if result.on {
// Feature value is truthy
}
if result.off {
// Feature value is falsy
}

// Why was this value assigned?
match result.source {
FeatureResultSource::DefaultValue => println!("Using default value"),
FeatureResultSource::Force => println!("Forced value from targeting rule"),
FeatureResultSource::Experiment => println!("Value from A/B test"),
FeatureResultSource::UnknownFeature => println!("Feature not found"),
}

// Experiment information (if from an A/B test)
if let Some(exp) = &result.experiment {
println!("Experiment key: {}", exp.key);
}

if let Some(exp_result) = &result.experiment_result {
println!("Variation ID: {}", exp_result.variation_id);
println!("In experiment: {}", exp_result.in_experiment);
}

Handling Missing Features

Features that don't exist return None as their value:

let result = client.feature_result("non-existent-feature", None);

if result.value.is_none() {
println!("Feature not found, using default behavior");
// Fallback logic
}

// Or use value_as with a default
let value = result.value_as::<String>()
.unwrap_or("default-value".to_string());

Feature Flags Usage - Best Practices

// ✅ Good: Use descriptive feature keys
if client.is_on("enable-dark-mode", None) {
// ...
}

// ✅ Good: Provide fallback values
let timeout = client.feature_result("api-timeout", None)
.value_as::<i32>()
.unwrap_or(30);

// ✅ Good: Handle errors gracefully
match client.feature_result("config", None).value_as::<Config>() {
Ok(config) => use_config(config),
Err(_) => use_default_config(),
}

// ❌ Bad: Using magic values without fallbacks
let timeout = client.feature_result("timeout", None)
.value_as::<i32>()
.unwrap(); // Panics if feature doesn't exist!

// ❌ Bad: Not handling type mismatches
let value = client.feature_result("my-feature", None).value;
// Assuming type without checking

Tracking Callbacks

Tracking callbacks allow you to integrate GrowthBook with your analytics systems (Segment, Mixpanel, Amplitude, etc.) to track when users are exposed to experiments.

Experiment Viewed Callback

This callback fires when a user is assigned a variation in an A/B test:

use growthbook_rust::client::GrowthBookClientBuilder;

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.on_experiment_viewed(Box::new(|experiment_result| {
// Track in your analytics system
println!("🧪 Experiment Viewed:");
println!(" Experiment: {}", experiment_result.key);
println!(" Variation: {}", experiment_result.variation_id);
println!(" Value: {:?}", experiment_result.value);
println!(" In Experiment: {}", experiment_result.in_experiment);
println!(" Hash Used: {}", experiment_result.hash_used);

// Example: Send to Segment
// analytics.track("Experiment Viewed", json!({
// "experiment_id": experiment_result.key,
// "variation_id": experiment_result.variation_id,
// "variation_value": experiment_result.value,
// }));
}))
.build()
.await?;

When is the Callback Triggered?

The on_experiment_viewed callback is called when:

  • A feature evaluation runs an experiment
  • The user is included in the experiment (passes targeting rules)
  • The user is randomly assigned a variation (not forced)

It is NOT called when:

  • A feature uses a forced value (no experiment)
  • The user is excluded from the experiment due to targeting
  • The feature doesn't exist

Feature Usage Callback

Track every feature evaluation, regardless of whether it's part of an experiment:

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.on_feature_usage(Box::new(|feature_key, result| {
// Track feature usage
println!("📊 Feature Used:");
println!(" Key: {}", feature_key);
println!(" Value: {:?}", result.value);
println!(" On: {}", result.on);
println!(" Source: {:?}", result.source);

// Example: Send to monitoring system
// monitoring.record_metric("feature.used", 1, vec![
// format!("feature:{}", feature_key),
// format!("enabled:{}", result.on),
// ]);
}))
.build()
.await?;

Use Cases for Feature Usage Tracking:

  • Monitor which features are being evaluated
  • Debug feature flag behavior
  • Track adoption of new features
  • Send metrics to monitoring systems (DataDog, New Relic)

Using Both Callbacks Together

You can use both callbacks for comprehensive tracking:

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
// Track A/B test exposures for analytics
.on_experiment_viewed(Box::new(|exp_result| {
// Critical for experiment analysis
analytics::track_experiment_viewed(
exp_result.key.clone(),
exp_result.variation_id,
exp_result.value.clone(),
);
}))
// Track all feature usage for monitoring
.on_feature_usage(Box::new(|key, result| {
// For debugging and monitoring
monitoring::record_feature_usage(key, result.on);
}))
.build()
.await?;

Integration Examples

Segment Integration

// Assuming you have a Segment client
use segment::{HttpClient, Message};

let segment_client = HttpClient::default();

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.on_experiment_viewed(Box::new(move |exp_result| {
let message = Message::track("user-123", "Experiment Viewed")
.properties(json!({
"experiment_id": exp_result.key,
"variation_id": exp_result.variation_id,
"variation_value": exp_result.value,
}));

segment_client.send(message);
}))
.build()
.await?;

Custom Analytics System

use tokio::spawn;

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.on_experiment_viewed(Box::new(|exp_result| {
// Send async without blocking
spawn(async move {
let event = json!({
"event": "experiment_viewed",
"experiment_id": exp_result.key,
"variation_id": exp_result.variation_id,
"timestamp": chrono::Utc::now().to_rfc3339(),
});

// Send to your analytics endpoint
if let Err(e) = send_analytics_event(event).await {
eprintln!("Failed to send analytics: {}", e);
}
});
}))
.build()
.await?;

Context and Caching

Context

The SDK uses a context object internally to manage state. You typically don't interact with it directly, but it's useful to understand how it works:

// The builder pattern creates and manages context for you
let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.attributes(global_attributes) // Context attributes
.build()
.await?;

// Per-evaluation attributes merge with context attributes
let result = client.is_on("my-feature", Some(user_specific_attributes));

Caching

The SDK implements intelligent caching to minimize network requests:

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.ttl(Duration::from_secs(60)) // Cache features for 60 seconds
.build()
.await?;

How Caching Works:

  • Features are cached in memory after the first fetch
  • Cache is automatically refreshed based on TTL
  • Manual refresh with client.refresh().await bypasses cache
  • Cache is shared across all evaluations
  • TTL defaults to 60 seconds if not specified

Cache Behavior:

// First call: fetches from API
let result1 = client.is_on("my-feature", None);

// Second call within TTL: uses cache (fast)
let result2 = client.is_on("my-feature", None);

// After TTL expires: fetches from API again
tokio::time::sleep(Duration::from_secs(61)).await;
let result3 = client.is_on("my-feature", None);

Debugging and Logging

Enable Debug Output

The Rust SDK uses standard Rust logging. Enable it using env_logger or tracing:

// Add to Cargo.toml
// [dependencies]
// env_logger = "0.11"
// log = "0.4"

use log::{info, debug};

fn main() {
// Initialize logger
env_logger::init();

// Or with custom format
env_logger::Builder::from_default_env()
.filter_level(log::LevelFilter::Debug)
.init();

// Now SDK operations will log details
let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.build()
.await?;
}

Set the log level via environment variable:

RUST_LOG=debug cargo run
# or
RUST_LOG=growthbook_rust=debug cargo run

Common Issues and Solutions

Issue: Features not loading

// Check initialization
let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.build()
.await;

match client {
Ok(c) => println!("✅ Client initialized"),
Err(e) => eprintln!("❌ Initialization failed: {}", e),
}

// Check if features are loaded
let result = client.feature_result("test-feature", None);
if result.value.is_none() {
eprintln!("Feature not found - features may not be loaded");
}

Issue: Wrong feature values

// Debug feature evaluation
let result = client.feature_result("my-feature", None);

println!("Feature: my-feature");
println!(" Value: {:?}", result.value);
println!(" Source: {:?}", result.source);
println!(" On: {}", result.on);

// Check attributes being used
let mut attrs = Vec::new();
attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String("test-123".to_string())
));

let result_with_attrs = client.feature_result("my-feature", Some(attrs));
println!("With attributes: {:?}", result_with_attrs.value);

Issue: Auto-refresh not working

use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};

let refresh_count = Arc::new(AtomicUsize::new(0));
let counter = refresh_count.clone();

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.auto_refresh(true)
.refresh_interval(Duration::from_secs(10))
.add_on_refresh(Box::new(move || {
let count = counter.fetch_add(1, Ordering::SeqCst);
println!("Refresh #{} at {:?}", count + 1, std::time::SystemTime::now());
}))
.build()
.await?;

// Wait and watch for refreshes
tokio::time::sleep(Duration::from_secs(35)).await;
println!("Total refreshes: {}", refresh_count.load(Ordering::SeqCst));

Testing and QA

Testing with Forced Values

// In your test environment, use features_json to control values
use serde_json::json;

let test_features = json!({
"feature-under-test": {
"defaultValue": true
},
"config-value": {
"defaultValue": "test-mode"
}
});

let client = GrowthBookClientBuilder::new()
.api_url("http://localhost:8080".to_string())
.client_key("test-key".to_string())
.features_json(test_features)?
.auto_refresh(false) // Disable refresh in tests
.build()
.await?;

// Now you can test with predictable feature values
assert!(client.is_on("feature-under-test", None));

Unit Testing Helpers

#[cfg(test)]
mod tests {
use super::*;

async fn create_test_client() -> GrowthBookClient {
let features = json!({
"test-feature": {
"defaultValue": true
}
});

GrowthBookClientBuilder::new()
.api_url("http://test".to_string())
.client_key("test".to_string())
.features_json(features).unwrap()
.auto_refresh(false)
.build()
.await
.unwrap()
}

#[tokio::test]
async fn test_feature_enabled() {
let client = create_test_client().await;
assert!(client.is_on("test-feature", None));
}

#[tokio::test]
async fn test_with_attributes() {
let client = create_test_client().await;

let mut attrs = Vec::new();
attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String("test-user".to_string())
));

let result = client.is_on("test-feature", Some(attrs));
assert!(result);
}
}

Integration Examples

This section provides real-world integration examples with popular Rust frameworks.

Actix Web Integration

A complete example of integrating GrowthBook with Actix Web:

use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
use growthbook_rust::client::{GrowthBookClient, GrowthBookClientBuilder};
use growthbook_rust::model_public::{GrowthBookAttribute, GrowthBookAttributeValue};
use std::sync::Arc;
use std::time::Duration;

// Application state
struct AppState {
gb_client: Arc<GrowthBookClient>,
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init();

// Initialize GrowthBook client (singleton)
let gb_client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.auto_refresh(true)
.refresh_interval(Duration::from_secs(30))
.on_experiment_viewed(Box::new(|exp_result| {
log::info!("Experiment viewed: {} -> {}",
exp_result.key, exp_result.variation_id);
}))
.build()
.await
.expect("Failed to initialize GrowthBook");

let gb_client = Arc::new(gb_client);

// Start HTTP server
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(AppState {
gb_client: gb_client.clone(),
}))
.route("/", web::get().to(index))
.route("/api/features", web::get().to(get_features))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}

// Handler with per-request attributes
async fn index(
data: web::Data<AppState>,
req: HttpRequest,
) -> HttpResponse {
// Extract user attributes from request
let user_id = req.headers()
.get("X-User-ID")
.and_then(|v| v.to_str().ok())
.unwrap_or("anonymous");

let mut attrs = Vec::new();
attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String(user_id.to_string())
));
attrs.push(GrowthBookAttribute::new(
"url".to_string(),
GrowthBookAttributeValue::String(req.uri().to_string())
));

// Check features with user-specific attributes
let show_new_ui = data.gb_client.is_on("new-ui", Some(attrs.clone()));
let theme_result = data.gb_client.feature_result("theme", Some(attrs));
let theme = theme_result.value_as::<String>()
.unwrap_or("light".to_string());

HttpResponse::Ok().json(serde_json::json!({
"new_ui": show_new_ui,
"theme": theme,
"user_id": user_id,
}))
}

// Handler to inspect all features (useful for debugging)
async fn get_features(data: web::Data<AppState>) -> HttpResponse {
// Return feature flags for debugging
HttpResponse::Ok().json(serde_json::json!({
"status": "ok",
"message": "Features loaded"
}))
}

Axum Integration

Modern async web framework integration:

use axum::{
extract::{Extension, Path},
http::{Request, StatusCode},
response::{IntoResponse, Json},
routing::{get, post},
Router,
};
use growthbook_rust::client::{GrowthBookClient, GrowthBookClientBuilder};
use growthbook_rust::model_public::{GrowthBookAttribute, GrowthBookAttributeValue};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;

#[derive(Clone)]
struct AppState {
gb_client: Arc<GrowthBookClient>,
}

#[tokio::main]
async fn main() {
// Initialize tracing
tracing_subscriber::fmt::init();

// Create GrowthBook client
let gb_client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.auto_refresh(true)
.refresh_interval(Duration::from_secs(30))
.build()
.await
.expect("Failed to create GrowthBook client");

let state = AppState {
gb_client: Arc::new(gb_client),
};

// Build router
let app = Router::new()
.route("/", get(root))
.route("/user/:id", get(user_features))
.route("/experiment", post(run_experiment))
.layer(Extension(state));

// Run server
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
tracing::info!("Listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

async fn root(Extension(state): Extension<AppState>) -> impl IntoResponse {
let is_maintenance = state.gb_client.is_on("maintenance-mode", None);

if is_maintenance {
return (
StatusCode::SERVICE_UNAVAILABLE,
"Maintenance mode enabled",
).into_response();
}

(StatusCode::OK, "Service operational").into_response()
}

#[derive(Debug, Serialize)]
struct UserFeatures {
user_id: String,
premium_enabled: bool,
theme: String,
max_uploads: i32,
}

async fn user_features(
Path(user_id): Path<String>,
Extension(state): Extension<AppState>,
) -> Json<UserFeatures> {
// Build user attributes
let mut attrs = Vec::new();
attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String(user_id.clone())
));

// Evaluate features for this user
let premium_enabled = state.gb_client.is_on("premium-features", Some(attrs.clone()));

let theme_result = state.gb_client.feature_result("theme", Some(attrs.clone()));
let theme = theme_result.value_as::<String>()
.unwrap_or("default".to_string());

let uploads_result = state.gb_client.feature_result("max-uploads", Some(attrs));
let max_uploads = uploads_result.value_as::<i32>()
.unwrap_or(5);

Json(UserFeatures {
user_id,
premium_enabled,
theme,
max_uploads,
})
}

#[derive(Debug, Deserialize)]
struct ExperimentRequest {
user_id: String,
feature_key: String,
}

async fn run_experiment(
Extension(state): Extension<AppState>,
Json(payload): Json<ExperimentRequest>,
) -> impl IntoResponse {
let mut attrs = Vec::new();
attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String(payload.user_id.clone())
));

let result = state.gb_client.feature_result(&payload.feature_key, Some(attrs));

Json(serde_json::json!({
"feature_key": payload.feature_key,
"user_id": payload.user_id,
"value": result.value,
"on": result.on,
"source": format!("{:?}", result.source),
}))
}

Rocket Integration

#[macro_use] extern crate rocket;

use rocket::{State, http::Status};
use rocket::serde::json::Json;
use growthbook_rust::client::{GrowthBookClient, GrowthBookClientBuilder};
use growthbook_rust::model_public::{GrowthBookAttribute, GrowthBookAttributeValue};
use std::sync::Arc;
use std::time::Duration;

struct GBState {
client: Arc<GrowthBookClient>,
}

#[get("/")]
async fn index(state: &State<GBState>) -> Result<String, Status> {
if state.client.is_on("maintenance-mode", None) {
return Err(Status::ServiceUnavailable);
}
Ok("Hello, World!".to_string())
}

#[get("/feature/<user_id>")]
async fn check_feature(
user_id: String,
state: &State<GBState>,
) -> Json<serde_json::Value> {
let mut attrs = Vec::new();
attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String(user_id.clone())
));

let enabled = state.client.is_on("beta-feature", Some(attrs));

Json(serde_json::json!({
"user_id": user_id,
"beta_enabled": enabled,
}))
}

#[launch]
async fn rocket() -> _ {
let gb_client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.auto_refresh(true)
.build()
.await
.expect("Failed to init GrowthBook");

rocket::build()
.manage(GBState {
client: Arc::new(gb_client),
})
.mount("/", routes![index, check_feature])
}

CLI Application Example

Using GrowthBook in a command-line application:

use growthbook_rust::client::GrowthBookClientBuilder;
use growthbook_rust::model_public::{GrowthBookAttribute, GrowthBookAttributeValue};
use clap::Parser;
use std::time::Duration;

#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
/// User ID
#[clap(short, long)]
user_id: String,

/// Command to run
#[clap(short, long)]
command: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();

let args = Args::parse();

// Initialize GrowthBook
let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.auto_refresh(false) // No auto-refresh for CLI
.build()
.await?;

// Create user attributes
let mut attrs = Vec::new();
attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String(args.user_id.clone())
));
attrs.push(GrowthBookAttribute::new(
"cli".to_string(),
GrowthBookAttributeValue::Bool(true)
));

// Check feature flags
let can_use_beta_commands = client.is_on("cli-beta-commands", Some(attrs.clone()));

match args.command.as_str() {
"beta-feature" if can_use_beta_commands => {
println!("✅ Beta feature enabled for user {}", args.user_id);
run_beta_feature();
}
"beta-feature" => {
println!("❌ Beta feature not available for user {}", args.user_id);
}
"standard" => {
println!("Running standard command");
run_standard_command();
}
_ => {
println!("Unknown command: {}", args.command);
}
}

Ok(())
}

fn run_beta_feature() {
println!("Executing beta feature...");
// Beta feature logic
}

fn run_standard_command() {
println!("Executing standard command...");
// Standard logic
}

Background Worker / Job Processor

Using GrowthBook in async background workers:

use growthbook_rust::client::{GrowthBookClient, GrowthBookClientBuilder};
use growthbook_rust::model_public::{GrowthBookAttribute, GrowthBookAttributeValue};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;

struct JobProcessor {
gb_client: Arc<GrowthBookClient>,
}

impl JobProcessor {
async fn new() -> Result<Self, Box<dyn std::error::Error>> {
let gb_client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.auto_refresh(true)
.refresh_interval(Duration::from_secs(60))
.build()
.await?;

Ok(Self {
gb_client: Arc::new(gb_client),
})
}

async fn process_job(&self, job: Job) {
// Create attributes for this job
let mut attrs = Vec::new();
attrs.push(GrowthBookAttribute::new(
"userId".to_string(),
GrowthBookAttributeValue::String(job.user_id.clone())
));
attrs.push(GrowthBookAttribute::new(
"jobType".to_string(),
GrowthBookAttributeValue::String(job.job_type.clone())
));

// Check if new processing algorithm is enabled
let use_new_algorithm = self.gb_client.is_on(
"new-job-processing",
Some(attrs.clone())
);

if use_new_algorithm {
log::info!("Using new processing algorithm for job {}", job.id);
self.process_with_new_algorithm(&job).await;
} else {
log::info!("Using standard processing for job {}", job.id);
self.process_with_standard_algorithm(&job).await;
}

// Check rate limits from feature flags
let rate_limit_result = self.gb_client.feature_result(
"job-rate-limit",
Some(attrs)
);

if let Ok(rate_limit) = rate_limit_result.value_as::<i32>() {
log::info!("Rate limit for this job: {}", rate_limit);
// Apply rate limiting
}
}

async fn process_with_new_algorithm(&self, job: &Job) {
// New algorithm logic
log::info!("Processing job {} with new algorithm", job.id);
}

async fn process_with_standard_algorithm(&self, job: &Job) {
// Standard logic
log::info!("Processing job {} with standard algorithm", job.id);
}
}

#[derive(Debug)]
struct Job {
id: String,
user_id: String,
job_type: String,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();

let processor = JobProcessor::new().await?;

// Simulate job queue
loop {
// Fetch job from queue (Redis, RabbitMQ, etc.)
let job = Job {
id: uuid::Uuid::new_v4().to_string(),
user_id: "user-123".to_string(),
job_type: "data-processing".to_string(),
};

processor.process_job(job).await;

sleep(Duration::from_secs(5)).await;
}
}

Advanced Usage

Multiple SDK Instances

You can create multiple GrowthBook clients for different environments or projects:

use std::collections::HashMap;

struct MultiTenantGrowthBook {
clients: HashMap<String, Arc<GrowthBookClient>>,
}

impl MultiTenantGrowthBook {
async fn new(tenants: Vec<(&str, &str, &str)>) -> Result<Self, Box<dyn std::error::Error>> {
let mut clients = HashMap::new();

for (tenant_id, api_url, client_key) in tenants {
let client = GrowthBookClientBuilder::new()
.api_url(api_url.to_string())
.client_key(client_key.to_string())
.auto_refresh(true)
.build()
.await?;

clients.insert(tenant_id.to_string(), Arc::new(client));
}

Ok(Self { clients })
}

fn get_client(&self, tenant_id: &str) -> Option<&Arc<GrowthBookClient>> {
self.clients.get(tenant_id)
}
}

// Usage
let multi_tenant = MultiTenantGrowthBook::new(vec![
("tenant-a", "https://cdn.growthbook.io", "sdk-key-a"),
("tenant-b", "https://cdn.growthbook.io", "sdk-key-b"),
]).await?;

if let Some(client) = multi_tenant.get_client("tenant-a") {
let enabled = client.is_on("feature", None);
}

TypeScript / Rust Interop

If you're building a hybrid application with TypeScript frontend and Rust backend, you can use the same SDK concepts across both:

Rust Backend:

// backend/src/main.rs
let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.build()
.await?;

let features_for_frontend = serde_json::json!({
"dark_mode": client.is_on("dark-mode", Some(user_attrs)),
"theme": client.feature_result("theme", Some(user_attrs)).value,
});

// Send to frontend
HttpResponse::Ok().json(features_for_frontend)

TypeScript Frontend:

// frontend/src/features.ts
interface Features {
dark_mode: boolean;
theme: string;
}

const features: Features = await fetch('/api/features').then(r => r.json());

if (features.dark_mode) {
enableDarkMode();
}

Performance Considerations

Memory Usage

  • Each client instance maintains an in-memory cache of features
  • Auto-refresh spawns a background task
  • Features are deserialized from JSON on each fetch

Optimization Tips:

// ✅ Good: Single client instance, reused across requests
lazy_static! {
static ref GB_CLIENT: Arc<GrowthBookClient> = {
// Initialize once at startup
tokio::runtime::Runtime::new()
.unwrap()
.block_on(async {
Arc::new(
GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.build()
.await
.expect("Failed to create GrowthBook client")
)
})
};
}

// ❌ Bad: Creating new client on each request
async fn handler() {
let client = GrowthBookClientBuilder::new() // DON'T DO THIS
.build()
.await
.unwrap();
}

Network Performance

  • Features are cached based on TTL
  • Consider longer refresh intervals for stable features
// For frequently changing features
let client = GrowthBookClientBuilder::new()
.refresh_interval(Duration::from_secs(30)) // 30 seconds
.build()
.await?;

// For stable features
let client = GrowthBookClientBuilder::new()
.refresh_interval(Duration::from_secs(300)) // 5 minutes
.build()
.await?;

Troubleshooting

Common Error Messages

"Failed to fetch features"

// Possible causes:
// 1. Wrong API URL or client key
// 2. Network connectivity issues
// 3. API is down

// Solution: Check configuration and network
let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string()) // Verify this
.client_key("sdk-abc123".to_string()) // Verify this
.build()
.await
.map_err(|e| {
eprintln!("Client creation failed: {}", e);
e
})?;

"Decryption failed"

// Cause: Wrong decryption key
// Solution: Verify the key from GrowthBook dashboard
let correct_key = env::var("GROWTHBOOK_DECRYPTION_KEY")?;
let client = GrowthBookClientBuilder::new()
.decryption_key(correct_key)
.build()
.await?;

"Feature not found"

let result = client.feature_result("my-feature", None);
if result.value.is_none() {
// Feature doesn't exist in your GrowthBook project
// Check the feature key spelling
eprintln!("Feature 'my-feature' not found");
}

Enable Verbose Logging

# Maximum verbosity
RUST_LOG=trace cargo run

# GrowthBook-specific logs
RUST_LOG=growthbook_rust=debug cargo run

# Filter by module
RUST_LOG=growthbook_rust::client=debug cargo run

Health Check Endpoint

use axum::{Extension, Json};

async fn health_check(
Extension(state): Extension<AppState>,
) -> Json<serde_json::Value> {
// Check if GrowthBook is accessible
let test_result = state.gb_client.feature_result("health-check", None);

Json(serde_json::json!({
"status": "ok",
"growthbook": {
"initialized": true,
"features_loaded": !test_result.value.is_none(),
}
}))
}

Migration from Other SDKs

From Node.js/JavaScript SDK

JavaScript:

const gb = new GrowthBook({
apiHost: "https://cdn.growthbook.io",
clientKey: "sdk-abc123",
attributes: { userId: "123" }
});

await gb.init();
const enabled = gb.isOn("my-feature");

Rust:

let mut attrs = HashMap::new();
attrs.insert("userId".to_string(),
GrowthBookAttributeValue::String("123".to_string()));

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.attributes(attrs)
.build()
.await?;

let enabled = client.is_on("my-feature", None);

From Python SDK

Python:

gb = GrowthBook(
api_host="https://cdn.growthbook.io",
client_key="sdk-abc123",
attributes={"userId": "123"}
)
gb.load_features()
enabled = gb.is_on("my-feature")

Rust:

let mut attrs = HashMap::new();
attrs.insert("userId".to_string(),
GrowthBookAttributeValue::String("123".to_string()));

let client = GrowthBookClientBuilder::new()
.api_url("https://cdn.growthbook.io".to_string())
.client_key("sdk-abc123".to_string())
.attributes(attrs)
.build()
.await?;

let enabled = client.is_on("my-feature", None);

Further Reading

Supported Features

FeaturesAll versions

ExperimentationAll versions

Encrypted Features≥ v0.0.1

Prerequisites≥ v0.0.1

SemVer Targeting≥ v0.0.1

v2 Hashing≥ v0.0.1