Skip to content

Delivery Strategies

VaultSandbox Client supports two email delivery strategies: Server-Sent Events (SSE) for real-time updates and Polling for compatibility. The SDK intelligently chooses the best strategy automatically or allows manual configuration.

When you wait for emails or subscribe to new email notifications, the SDK needs to know when emails arrive. It does this using one of two strategies:

  1. SSE (Server-Sent Events): Real-time push notifications from the server
  2. Polling: Periodic checking for new emails with adaptive backoff
FeatureSSEPolling
LatencyNear-instant (~100ms)Poll interval (default: 2s)
Server LoadLower (persistent connection)Higher (repeated requests)
Network TrafficLower (only when emails arrive)Higher (constant polling)
CompatibilityRequires persistent connectionsWorks everywhere
Firewall/ProxyMay be blockedAlways works
Battery ImpactLower (push-based)Higher (constant requests)

The default StrategyAuto automatically selects the best delivery method:

  1. Tries SSE first - Attempts to establish an SSE connection
  2. Falls back to polling - If SSE fails within 5 seconds, uses polling
  3. Adapts to environment - Works seamlessly in different network conditions
package main
import (
"context"
"os"
"time"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func main() {
ctx := context.Background()
// Auto strategy (default)
client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
// vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyAuto), // Default, can be omitted
)
if err != nil {
panic(err)
}
defer client.Close()
// SDK will automatically choose the best strategy
inbox, err := client.CreateInbox(ctx)
if err != nil {
panic(err)
}
email, err := inbox.WaitForEmail(ctx,
vaultsandbox.WithWaitTimeout(10*time.Second),
)
if err != nil {
panic(err)
}
}
  • Gateway supports SSE
  • Network allows persistent connections
  • SSE connection established within 5 seconds
  • No restrictive proxy/firewall
  • Gateway doesn’t support SSE
  • SSE connection fails
  • SSE doesn’t connect within timeout
  • Behind restrictive proxy/firewall
  • Network requires periodic reconnection

Server-Sent Events provide real-time push notifications when emails arrive.

  • Near-instant delivery: Emails appear within milliseconds
  • Lower server load: Single persistent connection
  • Efficient: Only transmits when emails arrive
  • Battery-friendly: No constant polling
client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategySSE),
)

The SSE strategy uses these configuration values (defined in the delivery package):

ConstantValueDescription
SSEReconnectInterval5sBase interval before reconnection attempts
SSEMaxReconnectAttempts10Maximum consecutive reconnection attempts
SSEBackoffMultiplier2Multiplier for exponential backoff

SSE uses exponential backoff for reconnections:

1st attempt: SSEReconnectInterval (5s)
2nd attempt: SSEReconnectInterval * 2 (10s)
3rd attempt: SSEReconnectInterval * 4 (20s)
...up to SSEMaxReconnectAttempts
package main
import (
"context"
"fmt"
"os"
"time"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func main() {
ctx := context.Background()
client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategySSE),
)
if err != nil {
panic(err)
}
defer client.Close()
inbox, err := client.CreateInbox(ctx)
if err != nil {
panic(err)
}
defer inbox.Delete(ctx)
// Create cancellable context for watching
watchCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// Real-time watching (uses SSE)
for email := range inbox.Watch(watchCtx) {
fmt.Printf("Instant notification: %s\n", email.Subject)
if strings.Contains(email.Subject, "Welcome") {
break
}
}
}
  • Real-time monitoring: When you need instant email notifications
  • Long-running tests: Reduces overall test time
  • High email volume: More efficient than polling
  • Development/local: Fast feedback during development
  • Requires persistent HTTP connection support
  • May not work behind some corporate proxies
  • Some cloud environments may close long-lived connections
  • Requires server-side SSE support

When using SSE, the SDK automatically handles adding new inboxes after the connection is established. If you call client.CreateInbox or client.ImportInbox while SSE is already connected, the SDK will:

  1. Immediately trigger a reconnection (without exponential backoff)
  2. Include the new inbox in the updated connection
  3. Sync all inboxes after reconnection to catch any emails that arrived during the brief reconnection window

This means you can safely add inboxes dynamically without any manual intervention:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create initial inbox and start watching
inbox1, _ := client.CreateInbox(ctx)
go func() {
for email := range inbox1.Watch(ctx) {
handler(email)
}
}()
// Later, add another inbox - events start flowing automatically
inbox2, _ := client.CreateInbox(ctx)
go func() {
for email := range inbox2.Watch(ctx) {
handler(email)
}
}()
// Both inboxes now receive real-time events

The reconnection is transparent and fast, so there’s no need to manually restart the client or coordinate inbox creation timing.

Polling periodically checks for new emails with adaptive backoff and jitter.

  • Universal compatibility: Works in all environments
  • Firewall-friendly: Standard HTTP requests
  • Predictable: Easy to reason about behavior
  • Resilient: Automatically recovers from transient failures
client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling),
)

The polling strategy uses these configuration values (defined in the delivery package):

ConstantValueDescription
DefaultPollingInitialInterval2sStarting interval between polls
DefaultPollingMaxBackoff30sMaximum interval between polls
DefaultPollingBackoffMultiplier1.5Multiplier for adaptive backoff
DefaultPollingJitterFactor0.3Random jitter to prevent thundering herd

The polling strategy uses sync-status-based change detection with adaptive backoff:

  1. First checks a lightweight sync endpoint for changes
  2. Only fetches full email lists when changes are detected
  3. When no changes occur, polling intervals gradually increase
  4. When changes are detected, intervals reset to initial value
  5. Random jitter is added to prevent synchronized polling across clients
Initial poll: 2s
No changes: 2s * 1.5 = 3s (+ jitter)
No changes: 3s * 1.5 = 4.5s (+ jitter)
No changes: 4.5s * 1.5 = 6.75s (+ jitter)
...up to 30s maximum
Changes detected: reset to 2s
package main
import (
"context"
"fmt"
"os"
"time"
vaultsandbox "github.com/vaultsandbox/client-go"
)
func main() {
ctx := context.Background()
client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling),
)
if err != nil {
panic(err)
}
defer client.Close()
inbox, err := client.CreateInbox(ctx)
if err != nil {
panic(err)
}
defer inbox.Delete(ctx)
// Create cancellable context for watching
watchCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
// Polling-based watching
for email := range inbox.Watch(watchCtx) {
fmt.Printf("Polled notification: %s\n", email.Subject)
if strings.Contains(email.Subject, "Welcome") {
break
}
}
}
  • Corporate networks: Restrictive firewall/proxy environments
  • CI/CD pipelines: Guaranteed compatibility
  • Rate-limited APIs: Avoid hitting request limits
  • Debugging: Predictable request timing
  • Low email volume: Polling overhead is minimal

For most use cases, let the SDK choose:

client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
// strategy defaults to StrategyAuto
)

Best for:

  • General testing
  • Unknown network conditions
  • Mixed environments (dev, staging, CI)
  • When you want it to “just work”

When you need guaranteed real-time performance:

client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategySSE),
)

Best for:

  • Local development (known to support SSE)
  • Real-time monitoring dashboards
  • High-volume email testing
  • Latency-sensitive tests

Caveat: Will fall back to polling if SSE fails to connect.

When compatibility is more important than speed:

strategy := vaultsandbox.StrategyPolling
if os.Getenv("CI") != "" {
strategy = vaultsandbox.StrategyPolling
}
client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(strategy),
)

Best for:

  • CI/CD environments (guaranteed to work)
  • Corporate networks with restrictive proxies
  • When SSE is known to be problematic
  • Rate-limited scenarios

Fast feedback with SSE:

func newDevClient() (*vaultsandbox.Client, error) {
return vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithBaseURL("http://localhost:3000"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategySSE),
)
}

Reliable polling:

func newCIClient() (*vaultsandbox.Client, error) {
return vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling),
)
}

Auto with reasonable defaults:

func newProductionClient() (*vaultsandbox.Client, error) {
return vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyAuto),
vaultsandbox.WithTimeout(60*time.Second),
)
}
func createClient() (*vaultsandbox.Client, error) {
opts := []vaultsandbox.Option{
vaultsandbox.WithBaseURL(os.Getenv("VAULTSANDBOX_URL")),
}
switch {
case os.Getenv("CI") != "":
// CI: Reliable polling
opts = append(opts, vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling))
case os.Getenv("GO_ENV") == "development":
// Dev: Fast SSE
opts = append(opts, vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategySSE))
default:
// Production: Auto
opts = append(opts, vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyAuto))
}
return vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"), opts...)
}
func measureDeliveryLatency(ctx context.Context) {
client, _ := vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"))
defer client.Close()
inbox, _ := client.CreateInbox(ctx)
defer inbox.Delete(ctx)
startTime := time.Now()
// Send email
sendTestEmail(inbox.EmailAddress())
// Wait for email
_, err := inbox.WaitForEmail(ctx,
vaultsandbox.WithWaitTimeout(30*time.Second),
)
if err != nil {
panic(err)
}
latency := time.Since(startTime)
fmt.Printf("Email delivery latency: %v\n", latency)
}
func compareStrategies(ctx context.Context) {
// Test SSE
sseClient, _ := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategySSE),
)
defer sseClient.Close()
sseInbox, _ := sseClient.CreateInbox(ctx)
sseStart := time.Now()
sendTestEmail(sseInbox.EmailAddress())
sseInbox.WaitForEmail(ctx, vaultsandbox.WithWaitTimeout(10*time.Second))
sseLatency := time.Since(sseStart)
// Test Polling
pollClient, _ := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling),
)
defer pollClient.Close()
pollInbox, _ := pollClient.CreateInbox(ctx)
pollStart := time.Now()
sendTestEmail(pollInbox.EmailAddress())
pollInbox.WaitForEmail(ctx, vaultsandbox.WithWaitTimeout(10*time.Second))
pollLatency := time.Since(pollStart)
fmt.Printf("SSE latency: %v\n", sseLatency)
fmt.Printf("Polling latency: %v\n", pollLatency)
fmt.Printf("Difference: %v\n", pollLatency-sseLatency)
sseInbox.Delete(ctx)
pollInbox.Delete(ctx)
}

When SSE fails, the auto strategy automatically falls back to polling:

import (
"errors"
vaultsandbox "github.com/vaultsandbox/client-go"
)
// Using auto strategy handles SSE failures gracefully
client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyAuto),
)
if err != nil {
// Handle initialization error
var netErr *vaultsandbox.NetworkError
if errors.As(err, &netErr) {
fmt.Printf("Network error: %v\n", netErr.Err)
}
}
// For explicit SSE strategy, the SDK will fall back to polling on failure
client, err = vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategySSE),
)
if err != nil {
var netErr *vaultsandbox.NetworkError
if errors.As(err, &netErr) {
fmt.Printf("Connection failed: %v\n", netErr.Err)
// Consider using polling strategy
}
}

If emails arrive slowly with polling:

// Solution 1: Use SSE for real-time delivery
client, _ := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategySSE), // Real-time delivery
)
// Solution 2: Use auto and let it choose
client, _ := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyAuto),
)

All wait operations respect context cancellation:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
email, err := inbox.WaitForEmail(ctx,
vaultsandbox.WithSubject("Welcome"),
)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Timed out waiting for email")
} else if errors.Is(err, context.Canceled) {
fmt.Println("Wait was canceled")
}
}

Let the SDK choose unless you have specific requirements:

// Good: Let SDK choose
client, _ := vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"))
// Only specify when needed
ciClient, _ := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling), // CI needs guaranteed compatibility
)

Configure differently for each environment:

func createClient() (*vaultsandbox.Client, error) {
opts := []vaultsandbox.Option{}
if os.Getenv("CI") != "" {
// CI: Reliable polling
opts = append(opts, vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling))
} else if os.Getenv("GO_ENV") == "development" {
// Dev: Fast SSE
opts = append(opts, vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategySSE))
} else {
// Production: Auto
opts = append(opts, vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyAuto))
}
return vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"), opts...)
}

All operations should use contexts for timeout and cancellation:

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
email, err := inbox.WaitForEmail(ctx,
vaultsandbox.WithSubjectRegex(regexp.MustCompile(`Welcome`)),
)

Always close clients and unsubscribe from subscriptions:

client, _ := vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"))
defer client.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
inbox, _ := client.CreateInbox(ctx)
defer inbox.Delete(ctx)
go func() {
for email := range inbox.Watch(ctx) {
fmt.Println(email.Subject)
}
}()

Use Go’s error handling patterns:

import "errors"
email, err := inbox.WaitForEmail(ctx, vaultsandbox.WithWaitTimeout(10*time.Second))
if err != nil {
switch {
case errors.Is(err, context.DeadlineExceeded):
fmt.Println("Timeout waiting for email")
case errors.Is(err, vaultsandbox.ErrInboxNotFound):
fmt.Println("Inbox was deleted or has expired")
default:
fmt.Printf("Unexpected error: %v\n", err)
}
return
}