Performance Best Practices
Follow these guidelines to optimize table performance in your SpacetimeDB modules.
Use Indexes for Lookups
Generally prefer indexed lookups over full table scans:
✅ Good - Using an index:
- TypeScript
- C#
- Rust
- C++
// Fast: Uses unique index on name
ctx.db.player.name.filter('Alice')// Fast: Uses unique index on name
ctx.Db.Player.Name.Filter("Alice")// Fast: Uses unique index on name
ctx.db.player().name().filter("Alice")C++ support is currently in beta and subject to change. SpacetimeDB C++ 2.0 is coming soon, but C++ server modules are currently pinned to v1.12.0. If you are following the C++ tab in this guide, use the v1.12.0 release track for now.
// Fast: Uses index on name
auto results = ctx.db[player_name].filter("Alice");❌ Avoid - Full table scan:
- TypeScript
- C#
- Rust
- C++
// Slow: Iterates through all rows
Array.from(ctx.db.player.iter())
.find(p => p.name === 'Alice')// Slow: Iterates through all rows
ctx.Db.Player.Iter()
.FirstOrDefault(p => p.Name == "Alice")// Slow: Iterates through all rows
ctx.db.player()
.iter()
.find(|p| p.name == "Alice")// Slow: Iterates through all rows
for (const auto& p : ctx.db[player]) {
if (p.name == "Alice") {
// Found it
break;
}
}Add indexes to columns you frequently filter or join on. See Indexes for details.
Keep Tables Focused
Break large tables into smaller, more focused tables when appropriate:
Instead of one large table:
- TypeScript
- C#
- Rust
- C++
const player = table(
{ name: 'player' },
{
id: t.u32(),
name: t.string(),
// Game state
position_x: t.f32(),
position_y: t.f32(),
health: t.u32(),
// Statistics (rarely accessed)
total_kills: t.u32(),
total_deaths: t.u32(),
play_time_seconds: t.u64(),
// Settings (rarely changed)
audio_volume: t.f32(),
graphics_quality: t.u8(),
}
);[SpacetimeDB.Table]
public partial struct Player
{
public uint Id;
public string Name;
// Game state
public float PositionX;
public float PositionY;
public uint Health;
// Statistics (rarely accessed)
public uint TotalKills;
public uint TotalDeaths;
public ulong PlayTimeSeconds;
// Settings (rarely changed)
public float AudioVolume;
public byte GraphicsQuality;
}#[spacetimedb::table(accessor = player)]
pub struct Player {
id: u32,
name: String,
// Game state
position_x: f32,
position_y: f32,
health: u32,
// Statistics (rarely accessed)
total_kills: u32,
total_deaths: u32,
play_time_seconds: u64,
// Settings (rarely changed)
audio_volume: f32,
graphics_quality: u8,
}// Instead of one large table with all fields:
struct Player {
uint32_t id;
std::string name;
// Game state
float position_x;
float position_y;
uint32_t health;
// Statistics (rarely accessed)
uint32_t total_kills;
uint32_t total_deaths;
uint64_t play_time_seconds;
// Settings (rarely changed)
float audio_volume;
uint8_t graphics_quality;
};Consider splitting into multiple tables:
- TypeScript
- C#
- Rust
- C++
const player = table(
{ name: 'player' },
{
id: t.u32().primaryKey(),
name: t.string().unique(),
}
);
const playerState = table(
{ name: 'player_state' },
{
player_id: t.u32().unique(),
position_x: t.f32(),
position_y: t.f32(),
health: t.u32(),
}
);
const playerStats = table(
{ name: 'player_stats' },
{
player_id: t.u32().unique(),
total_kills: t.u32(),
total_deaths: t.u32(),
play_time_seconds: t.u64(),
}
);
const playerSettings = table(
{ name: 'player_settings' },
{
player_id: t.u32().unique(),
audio_volume: t.f32(),
graphics_quality: t.u8(),
}
);[SpacetimeDB.Table]
public partial struct Player
{
[SpacetimeDB.PrimaryKey]
public uint Id;
[SpacetimeDB.Unique]
public string Name;
}
[SpacetimeDB.Table]
public partial struct PlayerState
{
[SpacetimeDB.Unique]
public uint PlayerId;
public float PositionX;
public float PositionY;
public uint Health;
}
[SpacetimeDB.Table]
public partial struct PlayerStats
{
[SpacetimeDB.Unique]
public uint PlayerId;
public uint TotalKills;
public uint TotalDeaths;
public ulong PlayTimeSeconds;
}
[SpacetimeDB.Table]
public partial struct PlayerSettings
{
[SpacetimeDB.Unique]
public uint PlayerId;
public float AudioVolume;
public byte GraphicsQuality;
}#[spacetimedb::table(accessor = player)]
pub struct Player {
#[primary_key]
id: u32,
#[unique]
name: String,
}
#[spacetimedb::table(accessor = player_state)]
pub struct PlayerState {
#[unique]
player_id: u32,
position_x: f32,
position_y: f32,
health: u32,
}
#[spacetimedb::table(accessor = player_stats)]
pub struct PlayerStats {
#[unique]
player_id: u32,
total_kills: u32,
total_deaths: u32,
play_time_seconds: u64,
}
#[spacetimedb::table(accessor = player_settings)]
pub struct PlayerSettings {
#[unique]
player_id: u32,
audio_volume: f32,
graphics_quality: u8,
}struct Player {
uint32_t id;
std::string name;
};
SPACETIMEDB_STRUCT(Player, id, name)
SPACETIMEDB_TABLE(Player, player, Public)
FIELD_PrimaryKey(player, id)
struct PlayerState {
uint32_t player_id;
float position_x;
float position_y;
uint32_t health;
};
SPACETIMEDB_STRUCT(PlayerState, player_id, position_x, position_y, health)
SPACETIMEDB_TABLE(PlayerState, player_state, Public)
FIELD_Unique(player_state, player_id)
struct PlayerStats {
uint32_t player_id;
uint32_t total_kills;
uint32_t total_deaths;
uint64_t play_time_seconds;
};
SPACETIMEDB_STRUCT(PlayerStats, player_id, total_kills, total_deaths, play_time_seconds)
SPACETIMEDB_TABLE(PlayerStats, player_stats, Public)
FIELD_Unique(player_stats, player_id)
struct PlayerSettings {
uint32_t player_id;
float audio_volume;
uint8_t graphics_quality;
};
SPACETIMEDB_STRUCT(PlayerSettings, player_id, audio_volume, graphics_quality)
SPACETIMEDB_TABLE(PlayerSettings, player_settings, Public)
FIELD_Unique(player_settings, player_id)Benefits:
- Reduces data transferred to clients who don't need all fields
- Allows more targeted subscriptions
- Improves update performance by touching fewer rows
- Makes the schema easier to understand and maintain
Choose Appropriate Types
Use the smallest integer type that fits your data range:
- TypeScript
- C#
- Rust
- C++
// If you only need 0-255, use u8 instead of u64
level: t.u8(), // Not t.u64()
player_count: t.u16(), // Not t.u64()
entity_id: t.u32(), // Not t.u64()// If you only need 0-255, use byte instead of ulong
public byte Level; // Not ulong
public ushort PlayerCount; // Not ulong
public uint EntityId; // Not ulong// If you only need 0-255, use u8 instead of u64
level: u8, // Not u64
player_count: u16, // Not u64
entity_id: u32, // Not u64// If you only need 0-255, use byte instead of uint64_t
uint8_t level; // Not uint64_t
uint16_t player_count; // Not uint64_t
uint32_t entity_id; // Not uint64_t
This reduces:
- Memory usage
- Network bandwidth
- Storage requirements
Consider Table Visibility
Private tables avoid unnecessary client synchronization overhead:
- TypeScript
- C#
- Rust
- C++
// Public table - clients can subscribe and receive updates
const player = table(
{ name: 'player', public: true },
{ /* ... */ }
);
// Private table - only visible to module and owner
// Better for internal state, caches, or sensitive data
const internalState = table(
{ name: 'internal_state' },
{ /* ... */ }
);// Public table - clients can subscribe and receive updates
[SpacetimeDB.Table(Public = true)]
public partial struct Player { /* ... */ }
// Private table - only visible to module and owner
// Better for internal state, caches, or sensitive data
[SpacetimeDB.Table]
public partial struct InternalState { /* ... */ }// Public table - clients can subscribe and receive updates
#[spacetimedb::table(accessor = player, public)]
pub struct Player { /* ... */ }
// Private table - only visible to module and owner
// Better for internal state, caches, or sensitive data
#[spacetimedb::table(accessor = internal_state)]
pub struct InternalState { /* ... */ }// Public table - clients can subscribe and receive updates
struct Player { /* ... */ };
SPACETIMEDB_STRUCT(Player, /* ... fields ... */)
SPACETIMEDB_TABLE(Player, player, Public)
// Private table - only visible to module and owner
// Better for internal state, caches, or sensitive data
struct InternalState {/* ... */ };
SPACETIMEDB_STRUCT(InternalState, /* ... fields ... */ )
SPACETIMEDB_TABLE(InternalState, internal_state, Private)Make tables public only when clients need to access them. Private tables:
- Don't consume client bandwidth
- Don't require client-side storage
- Are hidden from non-owner queries
Batch Operations
When inserting or updating multiple rows, batch them in a single reducer call rather than making multiple reducer calls:
✅ Good - Batch operation:
- TypeScript
- C#
- Rust
- C++
export const spawn_enemies = spacetimedb.reducer({ count: t.u32() }, (ctx, { count }) => {
for (let i = 0; i < count; i++) {
ctx.db.enemy.insert({
id: 0, // auto_inc
health: 100,
});
}
});[SpacetimeDB.Reducer]
public static void SpawnEnemies(ReducerContext ctx, uint count)
{
for (uint i = 0; i < count; i++)
{
ctx.Db.Enemy.Insert(new Enemy
{
Id = 0, // auto_inc
Health = 100
});
}
}#[spacetimedb::reducer]
fn spawn_enemies(ctx: &ReducerContext, count: u32) {
for i in 0..count {
ctx.db.enemy().insert(Enemy {
id: 0, // auto_inc
health: 100,
});
}
}// Good - Batch operation in a single reducer call
void spawn_enemies(ReducerContext& ctx, uint32_t count) {
for (uint32_t i = 0; i < count; ++i) {
Enemy new_enemy;
new_enemy.health = 100;
ctx.db[enemy].insert(new_enemy);
}
}❌ Avoid - Multiple calls:
- TypeScript
- C#
- Rust
- C++
// Client makes 10 separate reducer calls
for (let i = 0; i < 10; i++) {
connection.reducers.spawnEnemy();
}// Client makes 10 separate reducer calls
for (int i = 0; i < 10; i++)
{
connection.Reducers.SpawnEnemy();
}// Client makes 10 separate reducer calls
for i in 0..10 {
connection.reducers.spawn_enemy();
}// Client making many separate reducer calls
for (int i = 0; i < 10; ++i) {
connection.reducers.spawn_enemy();
}Batch operations are more efficient because:
- Single transaction reduces overhead
- Reduced network round trips
- Better database performance
Monitor Table Growth
Be mindful of unbounded table growth:
- Implement cleanup reducers for temporary data
- Archive or delete old records
- Use schedule tables to automatically expire data
- Consider pagination for large result sets
Next Steps
- Learn about Indexes to optimize queries
- Explore Subscriptions for efficient client data sync
- Review Reducers for efficient data modification patterns