# Performance Considerations for Kaneo: 6 Optimization Strategies for Self-Hosted Deployments

> Discover 6 optimization strategies for self-hosted Kaneo deployments. Improve database queries, WebSocket scaling, caching, and asset handling for peak performance.

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

---

**Kaneo's lightweight monorepo architecture—built with Hono, React 19, and Vite—offers multiple tuning levers for database queries, WebSocket scaling, caching, and asset handling to maintain high performance as your project management workload grows.**

Kaneo (usekaneo/kaneo) is designed as a fast, self-hosted alternative to bloated SaaS project management tools. Its deliberate minimalism—Node.js API, React front-end, and PostgreSQL datastore—means performance bottlenecks are predictable and fixable. This guide covers the concrete optimization strategies embedded in Kaneo's source code, with specific file references and runnable configurations.

---

## Database Schema Optimization with Drizzle ORM

PostgreSQL serves as Kaneo's single source of truth. Poor indexing or unbounded queries quickly become bottlenecks as project data accumulates.

Kaneo's schema in [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts) addresses this head-on:

- **Explicit indexes on foreign keys** — Every relational column uses `index("…_idx")` for fast joins
- **CUID2 primary keys** — Ultra-fast `createId()` from `paralleldrive/cuid2` instead of UUIDv4
- **Automatic timestamps** — `$onUpdate` and default values minimize extra UPDATE statements

### Pagination Pattern for Large Collections

Avoid loading entire collections. Use `LIMIT`/`OFFSET` pagination as implemented in the project list controller:

```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;
}

```

**Action items:**
- Add `index()` declarations when introducing searchable columns
- Monitor query plans with `EXPLAIN ANALYZE` for sequential scans
- Consider cursor-based pagination for real-time infinite scroll scenarios

---

## WebSocket Scaling: From In-Memory to Redis Pub/Sub

Real-time updates—task changes, comments, notifications—flow through WebSockets. Kaneo's broadcast architecture adapts to your deployment size.

**Default behavior:** Single-process in-memory broadcast (suitable for single-container deployments)

**Production scaling:** Set `REDIS_URL` to activate `RedisBroadcastAdapter`

The adaptive logic lives in [`apps/api/src/ws/broadcast.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/ws/broadcast.ts):

```typescript
// Behavior verified in: tests/api/ws/broadcast.test.ts

```

Configuration is environment-driven:

```env

# .env

REDIS_URL=redis://redis:6379

```

**Redis tuning for Kaneo:**
- Set `maxmemory-policy allkeys-lru` or `allkeys-lfu` for predictable eviction
- Enable persistence only if message durability matters (Kaneo broadcasts are ephemeral state syncs)
- Monitor `connected_clients` and `pubsub_channels` metrics

---

## Frontend Caching with TanStack Query

Kaneo's React 19 front-end in `apps/web` uses TanStack Query for automatic request deduplication and caching. Misconfigured query keys waste this benefit.

Pattern from [`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):

```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],  // Stable, hierarchical keys
    queryFn: () => listProjects(page),
    staleTime: 5 * 60_000,         // 5 minutes before refetch
    keepPreviousData: true,        // Smooth pagination transitions
  });
}

```

**Caching best practices:**
- Structure keys hierarchically: `["project", projectId, "tasks"]` not `["projectTasks", projectId]`
- Set `staleTime` based on data volatility—rarely-changing project metadata can cache longer
- Invalidate precisely: `queryClient.invalidateQueries({ queryKey: ["projects"] })` after mutations

---

## Streaming Uploads for Task Images

Task attachments bypass the API server to reduce I/O latency. Kaneo generates pre-signed S3-compatible URLs for direct browser-to-storage uploads.

Implementation in [`apps/web/src/lib/upload-task-image.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/lib/upload-task-image.ts):

```typescript
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);
  
  // Streams directly to S3-compatible storage
  await fetch(url, { method: "POST", body: form });
}

```

**Optimization opportunities:**
- Enable multipart/resumable uploads for files >100MB
- Configure CDN caching headers on your S3 bucket (`Cache-Control: public, max-age=31536000`)
- Use image processing pipelines (lambda/edge functions) for automatic resizing

---

## Build Optimization and Bundle Splitting

Kaneo's TurboRepo monorepo runs concurrent builds for API and web packages. Vite handles code-splitting for the React application.

Configuration in [`apps/web/vite.config.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/vite.config.ts):

```typescript
// Vite handles code-splitting automatically; manual chunks for vendor separation:
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Split heavy dependencies if bundle analysis shows bloat
          'vendor-react': ['react', 'react-dom'],
        },
      },
    },
  },
});

```

**CI/CD performance:**
- `pnpm build` leverages Turbo's remote caching when available
- Biome runs linting and autofix in parallel—no ESLint overhead
- Keep server-only code free of heavy client libraries to reduce cold starts

---

## Authentication Overhead and Session Caching

Better Auth validates JWTs on every request using HMAC-SHA256 (fast by design). The implementation in [`apps/api/src/auth/better-auth.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/auth/better-auth.ts) uses a 32-byte `AUTH_SECRET`:

```env
AUTH_SECRET=your-random-32-byte-string

```

For high-traffic deployments, add Redis session caching:

```typescript
// With REDIS_URL set, extend better-auth with session storage adapter
// Reduces database lookups per request from 1-2 queries to 0

```

**Token lifecycle tuning:**
- Keep access token lifetimes short (15-60 minutes)
- Rotate `AUTH_SECRET` only during maintenance windows—invalidates all sessions
- Cache user permissions in Redis with TTL matching token lifetime

---

## Summary

- **Index foreign keys and paginate aggressively** — Kaneo's Drizzle schema provides the foundation; add indexes for your custom queries
- **Scale WebSockets horizontally with `REDIS_URL`** — Single environment variable enables multi-instance deployments
- **Leverage TanStack Query's cache hierarchy** — Stable keys and appropriate `staleTime` eliminate redundant requests
- **Stream uploads directly to object storage** — Pre-signed URLs keep the API responsive under file upload load
- **Monitor bundle size and build times** — Vite and Turbo provide the tools; profile before optimizing
- **Cache sessions in Redis for high-RPS APIs** — Optional but impactful for authentication-heavy workloads

---

## Frequently Asked Questions

### Does Kaneo require Redis for production use?

No—Redis is optional but strongly recommended for multi-container deployments. Without `REDIS_URL`, WebSocket broadcasts remain in-memory and confined to a single process. For single-instance Docker deployments, this is sufficient. The test suite in [`tests/api/ws/broadcast.test.ts`](https://github.com/usekaneo/kaneo/blob/main/tests/api/ws/broadcast.test.ts) validates both configurations.

### How does Kaneo handle large file uploads without server timeouts?

The API never buffers file contents. Instead, [`apps/web/src/lib/upload-task-image.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/web/src/lib/upload-task-image.ts) requests a pre-signed S3-compatible URL from the API, then streams the file directly from the browser to object storage. This bypasses Node.js memory constraints and keeps API response times consistent regardless of file size.

### What database performance metrics should I monitor for Kaneo?

Focus on PostgreSQL slow query logs for sequential scans on large tables, connection pool saturation (default pools vary by hosting), and table bloat on frequently-updated tables like `taskTable` and `commentTable`. Kaneo's CUID2 primary keys and indexed foreign keys in [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts) provide a performant baseline.

### Can I use a CDN with Kaneo's uploaded images?

Yes—configure your S3-compatible storage bucket with a custom domain and CloudFlare, AWS CloudFront, or similar. Kaneo stores and serves image URLs as opaque strings; the pre-signed upload flow in [`upload-task-image.ts`](https://github.com/usekaneo/kaneo/blob/main/upload-task-image.ts) accepts any S3-compatible endpoint. Set long cache headers on the bucket and invalidate via API when images are replaced.