# Kaneo Performance Tuning: A Complete Guide to Database, Real-Time, and API Optimization

> Optimize your Kaneo application with this complete guide to database, real-time, and API tuning. Learn effective strategies for maximum performance.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: performance
- Published: 2026-08-02

---

**Kaneo achieves high performance through strategic PostgreSQL indexing, Redis-backed pub/sub for WebSocket scaling, lightweight Hono request handling, and intelligent TanStack Query caching.**

Kaneo is an open-source project management tool built as a **pnpm monorepo** that separates concerns between a backend API (`apps/api`) and a React frontend (`apps/web`). This article examines the specific architectural decisions and code patterns that enable **Kaneo performance tuning** across three critical layers: database access, real-time updates, and API efficiency. All findings derive directly from the usekaneo/kaneo source code.

## Database Layer: PostgreSQL with Drizzle ORM

Kaneo's data persistence relies on **PostgreSQL** accessed through **Drizzle ORM**. The schema definition lives in a single comprehensive file where performance optimizations are declared explicitly.

### Strategic Index Usage

In [`/apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main//apps/api/src/database/schema.ts), indexes target the exact query patterns the application generates:

```typescript
// Foreign key indexes for join performance
index("task_projectId_idx").on(taskTable.projectId),
index("task_dueDate_idx").on(taskTable.dueDate),
index("workspace_member_userId_idx").on(workspaceMemberTable.userId),

// Composite unique constraints for data integrity
uniqueIndex("task_project_number_unique").on(
  taskTable.projectId,
  taskTable.taskNumber
),

```

These indexes enable **index-only scans** instead of full table scans when filtering by `projectId`, `workspaceId`, or user-related columns. The `task_project_number_unique` constraint additionally prevents expensive duplicate-checking logic in application code.

### Cascade Rules for Cheap Referential Integrity

Deletion and update operations use database-level cascades defined in the schema:

```typescript
references(() => projectTable.id, { onDelete: "cascade" })

```

This pushes referential integrity work to PostgreSQL's optimized engine rather than requiring multiple round-trip queries from the application layer.

### Pagination in Query Controllers

The [`get-tasks.ts`](https://github.com/usekaneo/kaneo/blob/main/get-tasks.ts) controller demonstrates **limit/offset pagination** that prevents unbounded result sets:

```typescript
// apps/api/src/task/controllers/get-tasks.ts
export async function getTasks({ projectId, limit, offset }) {
  return db
    .select()
    .from(taskTable)
    .where(eq(taskTable.projectId, projectId))
    .limit(limit)
    .offset(offset);
}

```

**Key takeaway:** Always paginate list endpoints. The `limit` and `offset` parameters are enforced at the database level, not just as API conventions.

## Real-Time Layer: Redis Pub/Sub for WebSocket Scaling

Kaneo's WebSocket infrastructure supports **horizontal scaling** through a pluggable broadcast adapter pattern.

### Redis Broadcast Adapter Architecture

The `RedisBroadcastAdapter` in [`/apps/api/src/ws/redis-broadcast-adapter.ts`](https://github.com/usekaneo/kaneo/blob/main//apps/api/src/ws/redis-broadcast-adapter.ts) replaces the default in-memory adapter when Redis is available:

```typescript
// Publishing to all subscribed servers
async broadcast(event: string, data: unknown) {
  const redisPub = await getRedisPub();
  await redisPub.publish(
    CHANNEL_PATTERN,
    JSON.stringify({ event, data, senderId: this.serverId })
  );
}

```

Each server instance subscribes to the same `CHANNEL_PATTERN`. Messages published by any instance reach all others through Redis, eliminating the need for inter-server polling or sticky sessions.

### Lazy Connection Management

Redis connections are created on-demand and properly closed on shutdown:

```typescript
// apps/api/src/redis/index.ts
export async function getRedisPub() {
  if (!redisPub) {
    redisPub = createClient({ url: getRedisUrl() });
    await redisPub.connect();
  }
  return redisPub;
}

export async function closeRedis() {
  await redisPub?.quit();
  await redisSub?.quit();
  redisPub = redisSub = undefined;
}

```

**Configuration requirement:** Set `REDIS_URL` (or `REDIS_HOST`/`REDIS_PORT`) so `isRedisConfigured()` returns `true`. Without this, the system falls back to the in-memory `BroadcastAdapter`, which works for single-instance deployments but breaks real-time sync across multiple servers.

## API Layer: Hono and Valibot for Minimal Overhead

The API server prioritizes **request processing speed** through lightweight dependencies.

### Hono's Minimal Middleware Stack

Unlike heavier frameworks, Hono processes HTTP requests with minimal allocation and middleware traversal. Route handlers remain thin, delegating to controller functions:

```typescript
// Route definition delegates to controller
app.get("/api/tasks", async (c) => {
  const { projectId, limit, offset } = parseQuery(c.req.query());
  const tasks = await getTasks({ projectId, limit, offset });
  return c.json(tasks);
});

```

### Valibot for Zero-Cost Validation

Input validation uses **Valibot**, which validates without instantiating heavy schema objects:

```typescript
import * as v from "valibot";

const TaskQuerySchema = v.object({
  projectId: v.string(),
  limit: v.number(v.integer(), v.minValue(1), v.maxValue(100)),
  offset: v.number(v.integer(), v.minValue(0)),
});

export function parseQuery(input: unknown) {
  return v.parse(TaskQuerySchema, input);
}

```

This keeps garbage collection pressure low during high-throughput scenarios.

## Frontend Layer: TanStack Query for Intelligent Caching

The React frontend in `apps/web` leverages **TanStack Query** to minimize redundant API calls and smooth user interactions.

### Pagination with Data Prefetching

The `useTasks` hook implements **stale-while-revalidate** caching with pagination support:

```typescript
import { useQuery } from "@tanstack/react-query";
import { getTasks } from "@/fetchers/task/get-tasks";

export function useTasks(projectId: string, page: number, pageSize: number) {
  return useQuery({
    queryKey: ["tasks", projectId, page],
    queryFn: () => getTasks({
      projectId,
      limit: pageSize,
      offset: page * pageSize
    }),
    keepPreviousData: true, // Smooths page transitions
  });
}

```

The `keepPreviousData: true` option displays stale data while fetching new results, eliminating loading spinners during pagination.

### Proactive Prefetching

Navigation performance improves by prefetching data before the user requests it:

```typescript
// Prefetch next page when hovering pagination control
queryClient.prefetchQuery({
  queryKey: ["tasks", projectId, nextPage],
  queryFn: () => getTasks({
    projectId,
    limit: pageSize,
    offset: nextPage * pageSize
  }),
});

```

### Cache Tuning for Immutable Data

Data that changes infrequently (workspace lists, user profiles) benefits from extended stale times:

```typescript
useQuery({
  queryKey: ["workspaces"],
  queryFn: fetchWorkspaces,
  staleTime: 5 * 60 * 1000, // 5 minutes
});

```

## Practical Performance Checklist

Apply these specific actions to optimize your Kaneo deployment:

1. **Verify indexes for new filters** — Before deploying query changes, confirm [`schema.ts`](https://github.com/usekaneo/kaneo/blob/main/schema.ts) contains matching indexes. Add them with `index("table_column_idx").on(table.column)` if absent.

2. **Enforce pagination everywhere** — API endpoints accepting list parameters must implement `limit`/`offset`. Default to reasonable maximums (e.g., 100 records).

3. **Configure Redis in production** — Set environment variables so `isRedisConfigured()` activates the Redis broadcast adapter. Essential for multi-instance deployments.

4. **Extend staleTime for stable data** — Identify slowly-changing entities and increase their TanStack Query `staleTime` to reduce API traffic.

5. **Eliminate N+1 queries** — Use Drizzle's relation helpers and explicit joins instead of looping through results with individual queries:

```typescript
// Preferred: single query with join
await db
  .select()
  .from(taskTable)
  .where(eq(taskTable.projectId, projectId))
  .leftJoin(userTable, eq(taskTable.assigneeId, userTable.id));

// Avoid: querying users separately for each task

```

## Summary

- **Database performance** relies on explicit indexes in [`schema.ts`](https://github.com/usekaneo/kaneo/blob/main/schema.ts), pagination in controllers, and cascade rules for efficient deletions.
- **Real-time scaling** requires Redis configuration to activate the `RedisBroadcastAdapter`, enabling horizontal WebSocket distribution.
- **API efficiency** comes from Hono's minimal overhead and Valibot's lightweight validation.
- **Frontend responsiveness** benefits from TanStack Query's automatic caching, prefetching, and configurable stale times.

## Frequently Asked Questions

### How do I enable Redis for WebSocket scaling in Kaneo?

Set the `REDIS_URL` environment variable (or `REDIS_HOST` and `REDIS_PORT` separately) before starting the API server. The `isRedisConfigured()` function in [`/apps/api/src/redis/index.ts`](https://github.com/usekaneo/kaneo/blob/main//apps/api/src/redis/index.ts) detects this configuration and automatically instantiates the `RedisBroadcastAdapter` instead of the in-memory fallback.

### What database indexes does Kaneo use for task queries?

Kaneo defines `task_projectId_idx` and `task_dueDate_idx` in [`schema.ts`](https://github.com/usekaneo/kaneo/blob/main/schema.ts) to accelerate filtering by project and due date. The composite unique index `task_project_number_unique` also optimizes lookups by task number within a project while enforcing uniqueness.

### Why does Kaneo use Valibot instead of Zod for validation?

Valibot provides equivalent type-safe validation without the runtime overhead of instantiating schema objects. This reduces memory allocation per request, which compounds into meaningful performance gains under high throughput.

### How does the frontend prevent loading states during pagination?

The `useTasks` hook sets `keepPreviousData: true` in its TanStack Query configuration. This displays the previous page's data while fetching the new page, eliminating visible loading states and improving perceived performance.