Pixeltable vs Convex

Same video-intelligence app, two implementations. Pick Convex when reactivity is the point — the UI re-renders on write. Pick Pixeltable when the pipeline is the product. A REST-shaped benchmark is Convex’s worst event; discount this column accordingly.

pip install 'pixeltable[serve]'
See how it works

The trade

SidePixeltableConvex
At a glance
  • 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
  • 429 app lines plus 252 in compute-service, and the easiest install of the three
  • npx convex dev: anonymous local backend, no account, no Docker
  • 109 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

What we measured

Same app. Convex wins install, ingest, and transcript search. Pixeltable wins in-platform media. REST is Convex’s worst event.

FeaturePixeltableConvex
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
681 lines (429 + 252 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; 109 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
66.7s, 9.04× realtime; first video 5.3s
53.7s, 11.25× realtime; first video 3.2s
Transcript search p50
18.5ms
11.8ms
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

All three beat realtime on this laptop

20 videos, 10 minutes of footage, CPU, local models — not Cloud. Bold is best on that row.

Ingest

FeaturePixeltableConvex
Wall time66.7s53.7s
Faster than realtime9.04x11.25x
First video5.3s3.2s
Median video3.1s2.4s

Search

FeaturePixeltableConvex
Frame search p5039.0ms26.6ms
Frame search p9542.9ms40.7ms
Transcript search p5018.5ms11.8ms
Transcript search p9521.6ms17.7ms

20 videos, 603.7 seconds of footage, 603 frames, 70 transcript chunks, one laptop, CPU, local models (Pixeltable 0.7.8). All three finished 20 of 20, all faster than realtime. Supabase led ingest (11.64× vs Pixeltable’s 9.04×); frame search is 26–39ms and most of that is embedding the query, not the index. 603 vectors is not a million-row benchmark, hosted Pixeltable Cloud was not in this run, and none of this is a cost comparison.

Adding a column to live data

One computed title-embedding index on a populated catalog. Lines compound; these seconds do not.

FeaturePixeltableConvex
Schema change3.88s5.98s
Backfillsame step1.39s
Total3.88s7.37s
Lines written153
Files touched12
  • Supabase is the fastest in wall time. At two dozen rows the backfill is noise; anyone quoting these seconds as a scaling result is quoting noise.
  • Reverting is not symmetric. Convex needs a second migration (17 of its 53 lines) because pushing a schema that no longer declares the field is rejected while documents still carry it.
  • After pxt schema update, an insert against the already-registered route answers 409 until pxt service update. Reads keep working. The other two resolve the table on every request.
  • Backfill time is the part that scales, and this corpus cannot show it. Pixeltable’s backfill is work proportional to the rows that changed; a backfill script is work proportional to the table.

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

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

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

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

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

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

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 which platform

Choose Pixeltable when

  • 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.

Choose Convex when

  • 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 (109 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

Frequently asked questions

One file. The whole pipeline.

Declare the tables. Apply the schema. Insert a row. Serve the same file.

pip install 'pixeltable[serve]'
See how it worksGet expert guidance