Skip to content

Monitoring the Embedding Queue

This recipe shows how to watch pgEdge Vectorizer's queue of chunks waiting for embeddings. The triggers that the extension adds to a vectorized table queue an item for each new chunk, and a background worker takes the items from the queue and embeds them. The extension provides three views of the queue: queue_status counts the items by chunk table and status, pending_count counts the pending items across the database, and failed_items lists the items that have failed. The script reads the views before and after the worker has embedded three rows.

These examples are for pgEdge Vectorizer 1.1.

The script prints the following output:

Before the commit:
pending: 3
After the worker has finished:
completed: 3
Pending items in the queue: 0
Failed items for the table: 0

The pending_count view covers every vectorized table in the database, so on a server where other tables are being vectorized it can report more items than the script queues.

Preparing the Ingredients

The script needs a PostgreSQL server with the vector and pgedge_vectorizer extensions, and an embedding model that the server can reach. The Vectorizing a Table recipe lists the server settings that the examples use; the background worker must process the queue in the database that the script connects to.

Configuring psql

The script prints plain lines, and it hides the NOTICE messages that the Vectorizer functions raise. A failed step must not stop the script before the cleanup at the end, so the script turns off ON_ERROR_STOP until the cleanup starts. The following code shows the settings:

-- Print plain lines, and hide the NOTICE messages that the Vectorizer
-- functions raise as they work.
\pset format unaligned
\pset tuples_only on
SET client_min_messages = warning;

-- Keep going after a failed step, so that the cleanup at the end of the
-- script always runs; the script fails at the end instead.
\set ON_ERROR_STOP off

Vectorizing an Empty Table

The script vectorizes the table before it holds any rows, so that the trigger queues each row as the script inserts it. The following code creates the table, and calls enable_vectorization() for its text column:

-- Vectorize an empty table, so that the trigger queues each row as the
-- next step inserts it.
CREATE TABLE cookbook_queue_status (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    content text NOT NULL
);

-- The function returns void, so the query selects no columns, which
-- prints nothing.
SELECT FROM pgedge_vectorizer.enable_vectorization(
    'cookbook_queue_status', 'content');

Queueing the Rows

The trigger chunks each row as the INSERT adds it, and queues an item for each chunk, in the same transaction as the INSERT. The background worker cannot see the items until the transaction commits, so a query in the same transaction sees all three items as pending, whatever the worker is doing. Once the transaction commits, the worker can take the items at any moment, so a count read after the commit varies from run to run. The following code inserts the rows and reads the queue_status view before the commit:

-- The trigger chunks each new row and adds a queue item for each chunk.
-- The background worker cannot see the items until the transaction
-- commits, so the queue holds exactly three pending items for the table
-- when the transaction reads it.
BEGIN;

INSERT INTO cookbook_queue_status (content) VALUES
    ('Knead the bread dough for ten minutes, then leave it to rise in a warm kitchen.'),
    ('Jupiter is the largest planet in the solar system, with dozens of moons.'),
    ('PostgreSQL stores data in tables and answers questions written in SQL.');

\echo Before the commit:
SELECT status || ': ' || count
FROM pgedge_vectorizer.queue_status
WHERE chunk_table = 'cookbook_queue_status_content_chunks';

COMMIT;

Waiting for the Embeddings

The script waits in a DO block that checks the queue once a second. The block stops when no item for the chunk table is pending or in progress, and raises an error if an item fails or the wait passes five minutes; a model that is not yet in memory can take some time to answer its first request. The following code waits for the worker:

-- Wait until the background worker has embedded every queued chunk of the
-- table, and fail if an item fails or the wait takes over five minutes. A
-- cold embedding model can take a while to answer its first request.
DO $$
DECLARE
    deadline timestamptz := clock_timestamp() + interval '300 seconds';
    failure text;
BEGIN
    LOOP
        SELECT error_message INTO failure
        FROM pgedge_vectorizer.failed_items
        WHERE chunk_table = 'cookbook_queue_status_content_chunks'
        LIMIT 1;
        IF FOUND THEN
            RAISE EXCEPTION 'embedding failed: %', failure;
        END IF;

        EXIT WHEN NOT EXISTS (
            SELECT 1
            FROM pgedge_vectorizer.queue_status
            WHERE chunk_table = 'cookbook_queue_status_content_chunks'
              AND status IN ('pending', 'processing'));

        IF clock_timestamp() > deadline THEN
            -- Report the latest error that the worker recorded for the
            -- table; a request that hangs records none.
            SELECT error_message INTO failure
            FROM pgedge_vectorizer.queue
            WHERE chunk_table = 'cookbook_queue_status_content_chunks'
              AND error_message IS NOT NULL
            ORDER BY next_retry_at DESC NULLS LAST, id DESC
            LIMIT 1;
            RAISE EXCEPTION 'timed out waiting for the embeddings: %',
                coalesce(failure, 'no error recorded; check that '
                    || 'pgedge_vectorizer.databases lists this database '
                    || 'and that pgedge_vectorizer.api_url reaches the '
                    || 'embedding provider');
        END IF;
        PERFORM pg_sleep(1);
    END LOOP;
END
$$;

Reading the Queue Views

An item moves from pending to processing while the worker embeds its chunk, and to completed once the embedding is stored. When the worker cannot embed a chunk, it sets the item back to pending with a delay that doubles with each attempt, and marks the item failed once the item has used up its attempts; the pgedge_vectorizer.max_retries setting sets the number of attempts for new items. The failed_items view shows the error from the last attempt. The queue_status view also reports the oldest and newest item, and the average processing time, for each table and status. The following code reads the three views after the wait:

-- queue_status counts the items of each chunk table by status; the
-- worker has now marked all three items as completed.
\echo After the worker has finished:
SELECT status || ': ' || count
FROM pgedge_vectorizer.queue_status
WHERE chunk_table = 'cookbook_queue_status_content_chunks';

-- pending_count covers the whole queue, for every vectorized table in
-- the database.
SELECT 'Pending items in the queue: ' || pending_items
FROM pgedge_vectorizer.pending_count;

-- failed_items lists the items that have used up their retries, with the
-- error from the last attempt.
SELECT 'Failed items for the table: ' || count(*)
FROM pgedge_vectorizer.failed_items
WHERE chunk_table = 'cookbook_queue_status_content_chunks';

Cleaning Up

The disable_vectorization() function drops the triggers and, with drop_chunk_table, the chunk table, and it removes the table's pending items from the queue. The completed items stay in the queue until they are older than the pgedge_vectorizer.auto_cleanup_hours setting (24 hours by default) allows, and the clear_completed() function likewise deletes completed items only once they are older than 24 hours by default. Calling clear_completed(0) removes them at once, but for every vectorized table in the database, so the script deletes only the items for its own chunk table instead. The script then checks psql's LAST_ERROR_SQLSTATE variable, and raises an error if an earlier step failed, so that psql exits with a non-zero status. The following code removes everything that the script created:

-- Stop at the first error from here on, so that a failed cleanup fails
-- the script.
\set ON_ERROR_STOP on

-- Drop the triggers and the chunk table, and then remove the table's
-- completed queue items, which disable_vectorization() leaves behind.
SELECT FROM pgedge_vectorizer.disable_vectorization(
    'cookbook_queue_status', 'content', drop_chunk_table => true);
DELETE FROM pgedge_vectorizer.queue
WHERE chunk_table = 'cookbook_queue_status_content_chunks';
DROP TABLE cookbook_queue_status;

-- Fail the script if any earlier step failed; psql keeps the SQLSTATE of
-- the most recent error, or 00000 if there was none.
SELECT :'LAST_ERROR_SQLSTATE' <> '00000' AS failed \gset
\if :failed
DO $$ BEGIN RAISE EXCEPTION 'a step of the recipe failed'; END $$;
\endif

Running the Example

The script is available in the cookbook repository. Set the PG* environment variables for your server, and then run the following command from the recipe's sql directory:

psql -X -q -v ON_ERROR_STOP=1 -f main.sql

Viewing the Complete Program

The following code shows the complete script:

-- Ellie's Postgres Cookbook
-- Monitoring pgEdge Vectorizer's embedding queue with the queue_status,
-- pending_count and failed_items views.
--
-- Copyright (c) 2026, pgEdge, Inc.
-- This code is released under the PostgreSQL License.

-- Print plain lines, and hide the NOTICE messages that the Vectorizer
-- functions raise as they work.
\pset format unaligned
\pset tuples_only on
SET client_min_messages = warning;

-- Keep going after a failed step, so that the cleanup at the end of the
-- script always runs; the script fails at the end instead.
\set ON_ERROR_STOP off

-- Vectorize an empty table, so that the trigger queues each row as the
-- next step inserts it.
CREATE TABLE cookbook_queue_status (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    content text NOT NULL
);

-- The function returns void, so the query selects no columns, which
-- prints nothing.
SELECT FROM pgedge_vectorizer.enable_vectorization(
    'cookbook_queue_status', 'content');

-- The trigger chunks each new row and adds a queue item for each chunk.
-- The background worker cannot see the items until the transaction
-- commits, so the queue holds exactly three pending items for the table
-- when the transaction reads it.
BEGIN;

INSERT INTO cookbook_queue_status (content) VALUES
    ('Knead the bread dough for ten minutes, then leave it to rise in a warm kitchen.'),
    ('Jupiter is the largest planet in the solar system, with dozens of moons.'),
    ('PostgreSQL stores data in tables and answers questions written in SQL.');

\echo Before the commit:
SELECT status || ': ' || count
FROM pgedge_vectorizer.queue_status
WHERE chunk_table = 'cookbook_queue_status_content_chunks';

COMMIT;

-- Wait until the background worker has embedded every queued chunk of the
-- table, and fail if an item fails or the wait takes over five minutes. A
-- cold embedding model can take a while to answer its first request.
DO $$
DECLARE
    deadline timestamptz := clock_timestamp() + interval '300 seconds';
    failure text;
BEGIN
    LOOP
        SELECT error_message INTO failure
        FROM pgedge_vectorizer.failed_items
        WHERE chunk_table = 'cookbook_queue_status_content_chunks'
        LIMIT 1;
        IF FOUND THEN
            RAISE EXCEPTION 'embedding failed: %', failure;
        END IF;

        EXIT WHEN NOT EXISTS (
            SELECT 1
            FROM pgedge_vectorizer.queue_status
            WHERE chunk_table = 'cookbook_queue_status_content_chunks'
              AND status IN ('pending', 'processing'));

        IF clock_timestamp() > deadline THEN
            -- Report the latest error that the worker recorded for the
            -- table; a request that hangs records none.
            SELECT error_message INTO failure
            FROM pgedge_vectorizer.queue
            WHERE chunk_table = 'cookbook_queue_status_content_chunks'
              AND error_message IS NOT NULL
            ORDER BY next_retry_at DESC NULLS LAST, id DESC
            LIMIT 1;
            RAISE EXCEPTION 'timed out waiting for the embeddings: %',
                coalesce(failure, 'no error recorded; check that '
                    || 'pgedge_vectorizer.databases lists this database '
                    || 'and that pgedge_vectorizer.api_url reaches the '
                    || 'embedding provider');
        END IF;
        PERFORM pg_sleep(1);
    END LOOP;
END
$$;

-- queue_status counts the items of each chunk table by status; the
-- worker has now marked all three items as completed.
\echo After the worker has finished:
SELECT status || ': ' || count
FROM pgedge_vectorizer.queue_status
WHERE chunk_table = 'cookbook_queue_status_content_chunks';

-- pending_count covers the whole queue, for every vectorized table in
-- the database.
SELECT 'Pending items in the queue: ' || pending_items
FROM pgedge_vectorizer.pending_count;

-- failed_items lists the items that have used up their retries, with the
-- error from the last attempt.
SELECT 'Failed items for the table: ' || count(*)
FROM pgedge_vectorizer.failed_items
WHERE chunk_table = 'cookbook_queue_status_content_chunks';

-- Stop at the first error from here on, so that a failed cleanup fails
-- the script.
\set ON_ERROR_STOP on

-- Drop the triggers and the chunk table, and then remove the table's
-- completed queue items, which disable_vectorization() leaves behind.
SELECT FROM pgedge_vectorizer.disable_vectorization(
    'cookbook_queue_status', 'content', drop_chunk_table => true);
DELETE FROM pgedge_vectorizer.queue
WHERE chunk_table = 'cookbook_queue_status_content_chunks';
DROP TABLE cookbook_queue_status;

-- Fail the script if any earlier step failed; psql keeps the SQLSTATE of
-- the most recent error, or 00000 if there was none.
SELECT :'LAST_ERROR_SQLSTATE' <> '00000' AS failed \gset
\if :failed
DO $$ BEGIN RAISE EXCEPTION 'a step of the recipe failed'; END $$;
\endif

Next Steps

The following resources provide more information about the topics in this recipe: