# Performance Considerations for Kaneo: Optimizing Your Self-Hosted Project Management Platform

> Optimize your self-hosted Kaneo project management platform with performance insights. Learn how lightweight architecture, Drizzle ORM, Redis, and S3 enhance speed.

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

---

**Kaneo delivers high performance through lightweight architecture choices including Drizzle ORM with indexed PostgreSQL schemas, Redis-backed WebSocket scaling, TanStack Query deduplication, and S3-compatible asset streaming.**

Kaneo is a lightweight, self-hosted project management platform built as a monorepo with a **Node.js API (Hono)** and **React 19 frontend (Vite)**. Understanding the performance considerations for Kaneo ensures your deployment remains responsive as team size and project complexity grow. The codebase deliberately avoids heavyweight abstractions, providing clear optimization levers across database queries, real-time communication, and asset handling.

## Database Schema and Query Optimization

**PostgreSQL** serves as the single source of truth in Kaneo, making schema design and query efficiency critical for scaling. The platform uses **Drizzle ORM** with explicitly defined indexes on foreign-key columns to prevent table scans as project data accumulates.

In [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts), primary keys utilize **CUID2** (`createId()`) for ultra-fast generation and collision resistance. Foreign-key relationships include explicit `index("…_idx")` declarations to accelerate join operations. Implement **LIMIT-based pagination** instead of loading entire collections to maintain sub-100ms query times on large datasets.

```typescript
// apps/api/src/project/controllers/list-projects.ts
import { db } from "@/database";
import { projectTable } from "@/database/schema";
import { eq, desc } from "drizzle-orm";

export async function listProjects(userId: string, page = 1, pageSize = 20) {
  const offset = (page - 1) * pageSize;
  const projects = await db
    .select()
    .from(projectTable)
    .where(eq(projectTable.ownerId, userId))
    .orderBy(desc(projectTable.createdAt))
    .limit(pageSize)
    .offset(offset);
  return projects;
}

```

Leverage Drizzle's `$onUpdate` and default timestamp handling to minimize unnecessary UPDATE statements. Add indexes immediately when introducing searchable columns to prevent sequential scans on growing tables.

## WebSocket Scaling and Real-Time Performance

Real-time task updates and comments rely on WebSocket broadcasts that require different strategies depending on deployment scale. By default, Kaneo uses an **in-memory broadcast adapter** suitable for single-instance deployments, as verified in [`tests/api/ws/broadcast.test.ts`](https://github.com/usekaneo/kaneo/blob/main/tests/api/ws/broadcast.test.ts).

For horizontal scaling across multiple containers, configure the **Redis pub/sub adapter** by setting `REDIS_URL` in your environment. When present, [`apps/api/src/ws/broadcast.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/broadcast.ts) automatically instantiates `RedisBroadcastAdapter`, enabling event synchronization across API instances.

```env

# .env

REDIS_URL=redis://redis:6379

```

Deploy a dedicated Redis instance and tune the `maxmemory-policy` eviction strategy to prevent memory pressure during high-frequency update bursts. This eliminates the single-process bottleneck while maintaining sub-millisecond latency for real-time notifications.

## Frontend Caching and Query Deduplication

The React frontend utilizes **TanStack Query** to eliminate redundant network requests through automatic deduplication and intelligent caching. Proper cache configuration prevents re-fetching identical data across components, reducing bandwidth consumption and server load.

Structure query keys hierarchically (e.g., `["project", projectId]`) and configure `staleTime` and `cacheTime` parameters based on data volatility. After mutations, invalidate only affected queries to maintain cache consistency without triggering full refetches.

```typescript
// apps/web/src/hooks/queries/project/use-projects.ts
import { useQuery } from "@tanstack/react-query";
import { listProjects } from "@/fetchers/project/list-projects";

export function useProjects(page: number) {
  return useQuery({
    queryKey: ["projects", page],
    queryFn: () => listProjects(page),
    staleTime: 5 * 60_000, // 5 minutes
    keepPreviousData: true,
  });
}

```

Set `staleTime` to 300,000ms (5 minutes) for relatively static project metadata while using shorter windows for rapidly changing task lists. This pattern, implemented in [`apps/web/src/hooks/queries/project/use-project.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/hooks/queries/project/use-project.ts), minimizes API round-trips during navigation.

## Static Asset Handling and Upload Performance

Task image uploads utilize **S3-compatible storage** with pre-signed URLs to bypass the API server for file I/O. The client streams files directly to object storage via [`apps/web/src/lib/upload-task-image.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/lib/upload-task-image.ts), eliminating memory pressure on the Node.js process.

```typescript
// apps/web/src/lib/upload-task-image.ts
import { getPresignedUploadUrl } from "@/fetchers/task/get-presigned-url";

export async function uploadTaskImage(file: File, taskId: string) {
  const { url, fields } = await getPresignedUploadUrl(taskId);
  const form = new FormData();
  Object.entries(fields).forEach(([k, v]) => form.append(k, v));
  form.append("file", file);
  await fetch(url, { method: "POST", body: form });
}

```

Enable **multipart upload** for files exceeding 5MB to improve reliability on unstable connections. Configure CDN caching headers on your S3-compatible backend to serve task attachments with edge caching, reducing latency for distributed teams.

## Build Optimization and Bundle Configuration

The monorepo structure uses **TurboRepo** to orchestrate concurrent API and web server builds. **Vite** handles code-splitting for the React 19 frontend, with configuration defined in [`apps/web/vite.config.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/vite.config.ts). **Biome** provides fast linting and autofixing during CI pipelines.

Keep server-only code free of heavy client-side libraries to minimize cold-start times. Use Vite's `manualChunks` configuration to split vendor bundles if initial load times exceed performance budgets. Avoid importing visualization or charting libraries in API routes to prevent unnecessary memory overhead.

## Authentication Overhead and Session Management

**Better Auth** validates JWT tokens on every request using HMAC-SHA256 verification with a 32-byte secret (`AUTH_SECRET`). While cryptographic verification is computationally efficient, database lookups for session validation can introduce latency under high concurrency.

Cache authenticated sessions in Redis when `REDIS_URL` is configured to eliminate repeated database queries. Set short token lifetimes (15-30 minutes) with refresh token rotation scheduled during low-traffic windows. The authentication implementation in [`apps/api/src/auth/better-auth.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/auth/better-auth.ts) uses optimized JWT verification that typically adds less than 1ms per request.

## Summary

- **Index foreign keys and searchable columns** in [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts) using Drizzle's explicit index declarations to prevent table scans.
- **Scale WebSockets horizontally** by setting `REDIS_URL` to enable the Redis pub/sub broadcast adapter across multiple API instances.
- **Configure TanStack Query caching** with stable query keys and appropriate `staleTime` values to reduce redundant network traffic.
- **Stream large uploads** through S3 pre-signed URLs via [`apps/web/src/lib/upload-task-image.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/lib/upload-task-image.ts) to minimize API server load.
- **Keep dependencies lean** in server code to maintain fast cold-start times and efficient memory usage.

## Frequently Asked Questions

### How does Kaneo handle WebSocket scaling across multiple instances?

By default, Kaneo uses an in-memory broadcast adapter suitable for single-container deployments. When you provide a `REDIS_URL` environment variable, [`apps/api/src/ws/broadcast.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/broadcast.ts) automatically switches to a Redis pub/sub adapter, enabling event synchronization across horizontally scaled API instances.

### What database indexing strategy does Kaneo use?

Kaneo defines all tables in [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts) with explicit indexes on foreign-key columns using Drizzle ORM's `index("…_idx")` syntax. Primary keys use CUID2 for fast generation. You should add additional indexes when introducing new searchable fields to maintain query performance.

### How can I optimize image uploads in Kaneo?

The platform streams task images directly to S3-compatible storage using pre-signed URLs generated via [`apps/web/src/lib/upload-task-image.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/lib/upload-task-image.ts). Enable multipart uploads for large files and configure CDN caching on your object storage to reduce delivery latency.

### Does Kaneo support horizontal scaling?

Yes. Kaneo supports horizontal scaling through Redis-backed WebSocket broadcasting and stateless API design. Deploy multiple API container instances behind a load balancer, configure `REDIS_URL` for shared state, and use PostgreSQL for persistent storage to scale beyond single-node limitations.