---
title: "Pixeltable vs Convex: measured video-intelligence comparison"
description: "Same app, two implementations. Pixeltable is 129 lines in one file; Convex is 671 with a compute service. Convex wins install and reactivity. Pixeltable wins in-platform media. A REST contract is Convex’s worst event."
keywords:
  - Pixeltable vs Convex
  - Convex alternative
  - Convex AI
  - Convex vector search
  - Python AI backend
  - TypeScript vs Python backend
url: "https://www.pixeltable.com/compare/pixeltable-vs-convex"
---

# Pixeltable vs Convex

Updated: 2026-10-11

## Which is better, Pixeltable or Convex?

It depends. For an app built on media, models, and retrieval, Pixeltable: it runs ffmpeg, the models, and the API in one Python file, where Convex needs a second runtime. Convex is the better pick when the UI must re-render on every write, or when you want the quickest local install (npx convex dev). A REST-shaped benchmark is Convex’s worst event. As of 2026-10-11.

Same video-intelligence app, two implementations. Pixeltable runs the media, the models, and the API in one Python file; the Convex build also needed a compute service, because its runtime cannot execute ffmpeg. Convex wins install, reads, and concurrent load here, and its reactive client re-renders the UI on write, which Pixeltable does not do. A REST-shaped benchmark is Convex’s worst event; discount this column accordingly.

## Summary

### Pixeltable

- 129 lines, one file: the pipeline is the schema
- ffmpeg, Whisper, and CLIP run in-process; no second service
- Declared HTTP routes; a missing query is a 422 before any handler runs
- Adding a column backfills in place; processing fires for any writer

### Convex

- 425 app lines plus 246 in compute-service, and the easiest install of the three
- npx convex dev: anonymous local backend, no account, no Docker
- 105 lines in videos.ts because an action cannot write to the database
- 90 lines in http.ts because this contract asked for REST instead of the reactive client

## Comparison

| Feature | Pixeltable | Convex |
| --- | --- | --- |
| Local install | pip install; large Python deps (torch, whisper, sentence-transformers) | npx convex dev — no account, no Docker, easiest of the three |
| Total code for this app | 129 lines, 1 file | 671 lines (425 + 246 compute-service), 7 files |
| ffmpeg, Whisper, CLIP | Computed columns, same process | compute-service; the Convex runtime cannot run them |
| Writes from an action | Insert is a row; computed columns run | An action cannot write; 105 lines of mutations in videos.ts |
| HTTP API | add_query_route derives the signature; malformed requests are 422 | Five http.route blocks. Validators sit inside the function; a failure is a 500 |
| Reactive client | None here — you would write polling or a websocket | The reason most teams pick Convex; discarded by this REST contract |
| Vector search result limit | No ceiling in this implementation | vectorSearch clamps to 256 |
| Ingest, 20 videos / 10 min | 62.3s, 9.7× realtime; first video 5.1s | 47.6s, 12.7× realtime; first video 4.0s |
| Ingest, 100 videos / 63 min | 361.6s, 10.45× realtime | 231.5s, 16.32× realtime |
| Transcript search p50 (203 videos) | 17.0ms | 11.4ms |
| Reads at 203 videos, p50 | 5.1ms list, 0.7ms frame fetch | 2.4ms list, 0.4ms frame fetch; fastest read path of the three |
| Search under 8 clients, p50 | 119.5ms; the in-process embedding serializes | 65.2ms; the stateless layer stays parallel |
| Agent query p50 (local model) | 206ms; the model runs where the data is | 808ms; three compute-service round trips per question |
| Hosted model, 12 agent questions (paid endpoint) | 12/12; 8.4s p50, the request-rate scheduler serializes pacing | 12/12; 3.9s p50; zero recorded retries |
| Hosted swap, lines of retry code written | 7; the provider scheduler paces and retries | 37; pacing, Retry-After and backoff by hand |
| Add a derived column (lines) | 1 line, 1 file | 53 lines, 2 files; reverting needs a second migration |
| Processing for any writer | Yes — the pipeline is the schema | No, unless you add a scheduled action |
| Vendor checker in CI | ruff only | @convex-dev/eslint-plugin and tsc --noEmit against generated code |

## Ingest a video

Insert a video. Frames, audio, transcripts, embeddings, and scenes have to exist after that. On Pixeltable they are the schema. On the other two they live in the ingest path and in a second service.

### Pixeltable

```python
class Videos(TableModel, name='videos'):
    video: pxt.Video
    title: pxt.String
    audio = extract_audio(video, format='mp3')
    duration_sec = pxtf.video.get_duration(video)
    scenes = video.scene_detect_content(threshold=8.0)

class Frames(TableModel, name='frames', base=Videos,
             iterator=frame_iterator(Videos.video, fps=1.0)):
    still = pxtf.image.resize(frame, (320, 180))
    __indexes__ = [pxt.EmbeddingIndex(frame, embedding=VISUAL)]

class Chunks(TableModel, name='chunks', base=Videos,
             iterator=audio_splitter(Videos.audio, duration=10.0)):
    transcript = transcribe(audio_segment, model='base.en').text.astype(pxt.String)
    __indexes__ = [pxt.EmbeddingIndex(transcript, embedding=SEMANTIC)]

Videos.insert([{'video': 'lecture.mp4', 'title': 'CS101'}])
```

### Convex

```typescript
const { frames } = await compute("/extract-frames", { video_url, fps: FRAME_FPS });
const { embeddings } = await compute("/embed-clip", { images_b64: frames });
const frameRows = await Promise.all(frames.map(async (b64, i) => ({
  frameIdx: i,
  imageStorageId: await ctx.storage.store(new Blob([decodeBase64(b64)])),
  embedding: embeddings[i],
})));
await ctx.runMutation(internal.videos.insertFrames, { videoId, rows: frameRows });
```

## Search frames

Find frames of a whiteboard. Pixeltable asks the index. The other two embed the query themselves, then join or fetch rows in a second step.

### Pixeltable

```python
sim = Frames.frame.similarity(string=query)
return (
    Frames.order_by(sim, asc=False)
    .limit(limit)
    .select(
        frame_url=Frames.still,
        frame_idx=Frames.pos,
        video_title=Frames.title,
        similarity=sim,
    )
)
```

### Convex

```typescript
const { embeddings } = await compute("/embed-clip", { texts: [query] });
const hits = await ctx.vectorSearch("frames", "by_embedding", {
  vector: embeddings[0],
  limit: clamp(limit), // vectorSearch is 1-256
});
return await ctx.runQuery(internal.search.framesByIds, {
  ids: hits.map((h) => h._id),
  scores: hits.map((h) => h._score),
});
```

## Serve over HTTP

Expose search over HTTP. Pixeltable derives the route from the query. Supabase puts five paths in one function. Convex writes five REST routes only because this contract asked for REST.

### Pixeltable

```python
api = FastAPIRouter(name='api')
api.add_insert_route(Videos, path='/videos', inputs=[Videos.video, Videos.title], background=True)
api.add_query_route(path='/videos', query=list_videos, method='get')
api.add_query_route(path='/search/frames', query=search_frames, method='post')
api.add_query_route(path='/search/transcripts', query=search_transcripts, method='post')
```

### Convex

```typescript
http.route({
  path: "/search/frames",
  method: "POST",
  handler: httpAction(async (ctx, req) =>
    guarded(async (r) => {
      const body = await readJson(r);
      return json(await ctx.runAction(api.search.searchFrames, {
        query: requireString(body.query, "query"),
        limit: readLimit(body.limit),
      }));
    })(req)
  ),
});
```

## When to choose Pixeltable

- **The pipeline is the product**: Media in, models and retrieval out, in one Python file. Processing belongs to the table, so a row written from a shell is processed the same way as a row written over HTTP.
- **You do not want to operate a second runtime for ffmpeg**: The Convex runtime cannot execute ffmpeg. Three compute-service endpoints have no hosted-API substitute. That extra service is most of the orchestration hops.

## When to choose Convex

- **Reactivity is the point**: Build the same app with Convex’s reactive client instead of five REST endpoints and http.ts disappears along with both taxes. The client re-renders on write for free, and mutations are transactional.
- **You want the easiest local backend**: npx convex dev gives a working local backend with no account and no Docker. That is the easiest install of the three, and it is not close.

## Making the right choice

### Discount the REST column

- videos.ts (105 lines) and http.ts (90 lines) are taxes this contract imposes, not Convex’s native shape.
- Reproduce the numbers: https://github.com/pixeltable/pixeltable-vs-supabase-vs-convex

## FAQ

### Should I replace Convex with Pixeltable?

Yes, when the app is media, models, and retrieval: the Convex build needed a separate compute service because its runtime cannot execute ffmpeg, and Pixeltable runs all of it in one file. If the UI depends on Convex re-rendering on every write, plan for that first: Pixeltable has no realtime push.

### Why is Convex’s line count so high?

425 app lines, 671 with compute-service. Most of the app tax is two things this contract imposes: an action cannot write to the database (videos.ts), and we asked for REST (http.ts). A reactive-client implementation would not pay either.

### Is Pixeltable faster?

All three beat realtime on this laptop. Convex ingests in 47.6s against Pixeltable’s 62.3s at the 20-video tier, wins both read measurements, and holds 65ms under eight concurrent searches where Pixeltable serializes at 120-174ms. Pixeltable answers the agent query 3-4x faster locally, because the model runs where the data is. This is CPU and local models, not Cloud. The durable Pixeltable claims are structural: where media processing runs, what happens when you add a column, and that processing fires for any writer.

