Skip to content

Delivery Strategies

VaultSandbox Client supports two email delivery strategies: Server-Sent Events (SSE) for real-time updates and Polling for compatibility. SSE is the default strategy, providing near-instant email delivery with minimal latency.

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)

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

SSE is the default strategy, so you don’t need to specify it explicitly:

// SSE is used by default
client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
)
// Or explicitly specify SSE
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, use the default SSE strategy:

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

Best for:

  • General testing
  • Local development
  • Real-time monitoring dashboards
  • High-volume email testing
  • Latency-sensitive tests

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),
)
}

SSE with reasonable defaults:

func newProductionClient() (*vaultsandbox.Client, error) {
return vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
// SSE is the default, no need to specify
vaultsandbox.WithTimeout(60*time.Second),
)
}
func createClient() (*vaultsandbox.Client, error) {
opts := []vaultsandbox.Option{
vaultsandbox.WithBaseURL(os.Getenv("VAULTSANDBOX_URL")),
}
// Use polling in CI environments for maximum compatibility
if os.Getenv("CI") != "" {
opts = append(opts, vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling))
}
// Otherwise, use the default SSE strategy
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)
}

If SSE connection fails, consider using the polling strategy instead:

import (
"errors"
vaultsandbox "github.com/vaultsandbox/client-go"
)
// Default SSE strategy
client, err := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
)
if err != nil {
var netErr *vaultsandbox.NetworkError
if errors.As(err, &netErr) {
fmt.Printf("Network error: %v\n", netErr.Err)
// Consider using polling strategy in environments where SSE is blocked
}
}
// Switch to polling if SSE doesn't work in your environment
client, err = vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling),
)

If emails arrive slowly with polling, use the default SSE strategy:

// Use SSE (default) for real-time delivery
client, _ := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
// SSE is the default, provides near-instant delivery
)

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")
}
}

Use the default SSE strategy for best performance:

// Good: Use the default SSE strategy
client, _ := vaultsandbox.New(os.Getenv("VAULTSANDBOX_API_KEY"))
// Only specify polling when needed for compatibility
ciClient, _ := vaultsandbox.New(
os.Getenv("VAULTSANDBOX_API_KEY"),
vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling), // CI may need guaranteed compatibility
)

Configure polling for CI environments where SSE may be blocked:

func createClient() (*vaultsandbox.Client, error) {
opts := []vaultsandbox.Option{}
if os.Getenv("CI") != "" {
// CI: Use polling for guaranteed compatibility
opts = append(opts, vaultsandbox.WithDeliveryStrategy(vaultsandbox.StrategyPolling))
}
// Otherwise, the default SSE strategy is used
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
}