{"id":29480872,"url":"https://github.com/haathie/pgmb","last_synced_at":"2026-05-21T05:02:55.086Z","repository":{"id":304194425,"uuid":"1017149518","full_name":"haathie/pgmb","owner":"haathie","description":"Postgres message broker, with a type-safe typescript client with built-in webhook \u0026 SSE support.","archived":false,"fork":false,"pushed_at":"2026-03-21T07:00:45.000Z","size":2688,"stargazers_count":7,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-21T20:59:52.713Z","etag":null,"topics":["amqp","message-broker","message-bus","message-queue","pgmq","pgq","postgres","queue","sse","webhooks"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/haathie.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-10T05:25:45.000Z","updated_at":"2026-03-21T07:00:48.000Z","dependencies_parsed_at":null,"dependency_job_id":"531fc8c9-b25b-4f07-a1f4-e7b7291e5671","html_url":"https://github.com/haathie/pgmb","commit_stats":null,"previous_names":["haathie/pgmb"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/haathie/pgmb","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/haathie%2Fpgmb","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/haathie%2Fpgmb/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/haathie%2Fpgmb/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/haathie%2Fpgmb/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/haathie","download_url":"https://codeload.github.com/haathie/pgmb/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/haathie%2Fpgmb/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33289546,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-21T02:57:32.698Z","status":"ssl_error","status_checked_at":"2026-05-21T02:57:31.990Z","response_time":62,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["amqp","message-broker","message-bus","message-queue","pgmq","pgq","postgres","queue","sse","webhooks"],"created_at":"2025-07-14T23:11:44.341Z","updated_at":"2026-05-21T05:02:55.080Z","avatar_url":"https://github.com/haathie.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# PGMB - Postgres Message Broker\n\nA heavyweight message broker \u0026 event log built on top of PostgreSQL. PGMB tries to do most of the computational work in Postgres itself, and leaves the IO work to the Javscript runtime. PGMB guarantees **at-least-once** delivery of messages \u0026 mostly ordered delivery.\n\nUsing this package you can implement:\n1. Queue and exchange like behaviour seen in AMQP systems (RabbitMQ, ActiveMQ, etc.) with retries \u0026 reliable message delivery.\n2. Batch publish messages.\n3. Automatic insert, update, delete events for any table.\n4. HTTP SSE (Server Sent Events) for an arbitrary Postgres query, with resumption support via the standard `Last-Event-ID` header.\n5. Webhooks for events, again based on arbitrary Postgres queries, with retry logic.\n\n## Benchmarks\n\nHere are benchmarks of PGMB, PGMQ and AMQP. The benchmarks were run on an EC2 server managed by AWS EKS. Each database being allocated `2 cores` and `4GB` of RAM with network mounted EBS volumes. The full details of the benchmarks can be found [here](/docs/k8s-benchmark.md).\n\n| Test | PGMB | PGMQ (delete on ack) | AMQP (RabbitMQ) |\n| :--- | ---: | ---: | ---: |\n| msgs published/s | 27321 ± 6493 | 21286 ± 6129 | 27,646 ± 356 |\n| msgs consumed/s | 16224 ± 10860 | 3201 ± 5061 | 27,463 ± 392 |\n\nOf course, these benchmarks are for fairly low powered machines, but these give out enough confidence that a Postgres queue can be used as a message broker for reasonably sized workloads. The folks at Tembo managed to squeeze out 30k messages per second, with a more powerful setup. You can find their benchmarks [here](https://hemming-in.rssing.com/chan-2212310/article8486.html?nocache=0)\n\nNote: I'm not super sure why the PGMQ benchmarks are much lower, but I suspect it's due to the fact that it uses a serial ID for messages, has an additional index + I may have not configured it for max performance.\n\n## Setup\n\nFirst, let's ensure that PGMB is installed in your Postgres database. PGMB is just SQL code, so can be installed in limited environments where you may not have superuser access. PGMB is also compatible with [PGLite](https://pglite.dev).\n\nInstall PGMB by running the following command:\n\n```sh\nnpm install @haathie/pgmb\n```\n\nNote: PGMB is an ESM module.\n\nBefore using PGMB, you'll need to run the setup script to create the required tables, functions \u0026 triggers in your database. You can do this by running:\n\n```sh\npsql postgres://\u003cuser\u003e:\u003cpass\u003e@\u003chost\u003e:\u003cport\u003e/\u003cdb\u003e -f node_modules/@haathie/pgmb/sql/pgmb.sql -1\n```\n\n``` ts\nimport { Pool } from 'pg'\nimport { PgmbClient } from '@haathie/pgmb'\n\nconst pool = new Pool({ connectionString: 'postgres://...' })\nconst pgmb = new PgmbClient({\n\tclient: pool,\n\t// Provide a unique identifier for this\n\t// instance of the PGMB client. This should\n\t// be globally unique in your system.\n\tgroupId: 'my-node',\n})\nawait pgmb.init()\n\nprocess.on('SIGINT', async () =\u003e {\n\t// gracefully end the client on process exit\n\t// ensures any current work is finished, before\n\t// exiting.\n\tawait pgmb.end()\n\tawait pool.end()\n\tprocess.exit(0)\n})\n```\n\nTypically, we'd like our events to be typed. This can be achieved by providing a type parameter to the `PgmbClient` class. For example, if our events have the following structure:\n``` ts\ninterface MyEventData {\n\ttopic: 'user-created'\n\tpayload: {\n\t\tid: number\n\t\tname: string\n\t\temail: string\n\t}\n} | {\n\ttopic: 'order-placed'\n\tpayload: {\n\t\torderId: number\n\t\tuserId: number\n\t\tamount: number\n\t}\n}\n\n// when publishing or consuming events,\n// TypeScript will now ensure type-safety\nconst pgmb = new PgmbClient\u003cMyEventData\u003e({ ... })\n```\n\n## Basic Concepts\n\nFull details about the architecture, design \u0026 performance tricks of PGMB can be found in the [docs](/docs/arch.md). At a high level, PGMB revolves around the concept of **events** \u0026 **subscriptions**, all features emerge from the interaction between these two concepts.\n\nAll events are stored in a table called `pgmb.events`. This table is an insert-only log of all events that have been published.\n\nSubscriptions are stored in the `pgmb.subscriptions` table. Each subscription defines a set of conditions that determine which events it is interested in, by the \"conditions_sql\" and \"params\" columns. \"conditions_sql\" is a SQL expression that is evaluated for each event, and if it returns true, the event is considered to match the subscription. \"params\" is a JSONB object that can be used to parameterise the \"conditions_sql\".\n\nSubscriptions are grouped by a \"group_id\", a group owns a set of subscriptions, and each instance of a PGMB client should have a globally unique \u0026 persistent \"group_id\".\n\nTo minimise the number of unique SQL queries that have to run to match events to subscriptions, PGMB automatically groups subscriptions that have the same \"conditions_sql\" together. This means to optimise, try to utilise the same SQL query to match events, and then utilise \"params\" to differentiate between different subscriptions.\nFurthermore, each subscription is uniquely stored on (group, conditions_sql, params). This means that if multiple consumers require listening to the same set of events, they share the same underlying subscription, thereby reducing duplication of work.\n\nSubscriptions can also be automatically expired after a certain period of inactivity (useful for temporary consumers like HTTP SSE connections).\n\n## Building a Queue \u0026 Exchange system\n\n``` ts\n// setup a reliable consumer. Should execute this on each boot of your\n// service that wants to consume messages.\nawait pgmb.registerReliableHandler(\n\t{\n\t\t// we'll listen to \"msg-created\" and \"msg-updated\" events.\n\t\t// note: this SQL leverages the GIN index on \"params\" column\n\t\tconditionsSql: \"s.params @\u003e jsonb_build_object('topics', ARRAY[e.topic])\",\n\t\tparams: { topics: ['msg-created', 'msg-updated'] },\n\t\t// if the subscription isn't actively handled for more than 15 minutes,\n\t\t// it'll be expired \u0026 removed. We'll put this here in case the conditions,\n\t\t// or parameters change in the future, the stale subscription will be\n\t\t// removed automatically.\n\t\texpiryInterval: '15 minutes',\n\t\t// give a unique name for this handler. It need only be unique\n\t\t// within the same subscription parameters\n\t\tname: 'log-data',\n\t\t// Each retry will include the exact same set of events\n\t\t// that were included in the original attempt.\n\t\tretryOpts: {\n\t\t\t// will retry after 1 minute, then after 5 minutes\n\t\t\tretriesS: [60, 5 * 60]\n\t\t},\n\t\t// optionally provide a splitBy function to split\n\t\t// event to be processed differently based on some attribute.\n\t\tsplitBy(ev) {\n\t\t\treturn Object.values(\n\t\t\t\t// group events by their topic\n\t\t\t\tev.items.reduce((acc, item) =\u003e {\n\t\t\t\t\tconst key = item.topic\n\t\t\t\t\tacc[key] ||= { items: [] }\n\t\t\t\t\tacc[key].items.push(item)\n\t\t\t\t\treturn acc\n\t\t\t\t}, {})\n\t\t\t)\n\t\t}\n\t},\n\tasync({ items }, { logger }) =\u003e {\n\t\tfor(const item of items) {\n\t\t\tlogger.info('Received event', item.payload)\n\t\t}\n\t}\n)\n\n// let's publish an event\nawait pgmb.publish([{ topic: 'msg-created', payload: { hello: 'world' } }])\n```\n\nAs PGMB only supports a single consumer per subscription, if you need to scale out consumption, you can partition the subscription such that an approximately equal number of events are routed to each consumer. For example:\n``` ts\n// get the worker number from environment variable\nconst workerNumber = +(process.env.WORKER_NUMBER ?? 0)\n\nconst pgmb = new PgmbClient({\n\tgroupId: `my-node-worker-${workerNumber}`,\n\t...otherOpts\n})\n\nawait pgmb.registerReliableHandler(\n\t{\n\t\t// we're partitioning by the event ID here, but it's just as\n\t\t// easy to partition by any other attribute of the event.\n\t\t// note: again this SQL leverages the GIN index on \"params\" column\n\t\tconditionsSql: `s.params @\u003e jsonb_build_object(\n\t\t\t'topics', ARRAY[e.topic],\n\t\t\t'partition', hashtext(e.id) % 3\n\t\t)`,\n\t\tparams: {\n\t\t\ttopics: ['msg-created', 'msg-updated'],\n\t\t\tpartition: workerNumber\n\t\t},\n\t\texpiryInterval: '15 minutes',\n\t\t...otherParams\n\t},\n\thandler\n)\n```\n\nAs registering topical subscriptions is a common use case, PGMB provides a helper function to create such subscriptions easily.\n\n``` ts\nimport { createTopicalSubscriptionParams } from '@haathie/pgmb'\n\nconst sub = await pgmb.registerReliableHandler(\n\tcreateTopicalSubscriptionParams({\n\t\ttopics: ['msg-created', 'msg-updated'],\n\t\tpartition: { current: workerNumber, total: 3 },\n\t\texpiryInterval: '15 minutes'\n\t}),\n\thandler\n)\n```\n\nIf your use case doesn't need reliable processing of messages, and you just want to \"fire-and-forget\" process messages as they arrive, PGMB also provides a simpler API for that:\n\n``` ts\nconst sub = await pgmb.registerFireAndForgetHandler(\n\tcreateTopicalSubscriptionParams({\n\t\ttopics: ['msg-created', 'msg-updated'],\n\t\t// can expire this much quicker, because there's no\n\t\t// state to maintain\n\t\texpiryInterval: '1 minute'\n\t})\n)\nfor await(const { items } of sub) {\n\tfor(const item of items) {\n\t\tconsole.log('Received event', item.payload)\n\t}\n\t\n\t// breaking the for loop will automatically\n\t// unregister the subscription\n\t// break\n\t// alternatively, you can call sub.return() to\n\t// unregister the subscription\n}\n```\n\n## Publishing Messages\n\nAs shown above, it's quite easy to publish a message or two:\n``` ts\nawait pgmb.publish(\n\t[\n\t\t{ topic: 'msg-created', payload: { id: 123, hello: 'world' } },\n\t\t{ topic: 'msg-updated', payload: { id: 123, status: 'active' } }\n\t],\n\t// optionally provide a PG client, if publishing\n\t// in a transactional context\n\ttxClient\n)\n```\n\nIf the client is typed, then you'll have type-safety while publishing messages too. If you need to publish a large number of messages at different points in your code and do not want to block the main request, PGMB can enqueue messages to be published in the background. \n``` ts \nconst pgmb = new PgmbClient({\n\t...otherOpts,\n\t// flush every second\n\tflushIntervalMs: 1000,\n\t// or if there are 100 messages queued\n\tmaxBatchSize: 100,\n})\n\n// enqueue messages to be published in the background\npgmb.enqueue({ topic: 'msg-created', payload: { id: 123, hello: 'world' } })\n```\n\nTo flush any queued messages immediately, you can call:\n``` ts\nawait pgmb.flush()\n```\n\nIn case of failures while publishing messages in the background, PGMB will log the full failed message so it can be picked up later.\n\n## Automatic Table Events\n\nIf you need to automatically produce events whenever a table is mutated (insert, update, delete), PGMB makes it super easy to do so:\n\n``` sql\n-- let's say we have a table \"users\"\nCREATE TABLE users (\n\tid SERIAL PRIMARY KEY,\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\tname TEXT NOT NULL,\n\temail TEXT NOT NULL UNIQUE\n);\n\n-- to enable automatic events from mutations on this table,\n-- we just need to call the following function:\nSELECT pgmb.push_table_mutations('public.users'::regclass);\n\n-- to disable certain mutation types, we can parameters for the same.\n-- for example, to only enable \"insert\" events:\nSELECT pgmb.push_table_mutations(\n\t'public.users'::regclass,\n\tdelete := FALSE,\n\tupdate := FALSE\n);\n```\n\nThis will automatically create statement level triggers on the `users` table,\nand push events whenever a mutation occurs. The following events are produced:\n- `\u003cschema\u003e.\u003ctable\u003e.insert` (eg. `public.users.insert`)\n- `\u003cschema\u003e.\u003ctable\u003e.update` (eg. `public.users.update`)\n- `\u003cschema\u003e.\u003ctable\u003e.delete` (eg. `public.users.delete`)\n\nNote: table event names are lowercased automatically.\n\nAs events are published via triggers in the same transaction as the mutation,\nyou can be sure that if the transaction rolls back, no events are published.\n\nFor any given table, you can obtain the event's schema using:\n``` ts\nimport { PgmbClient, ITableMutationEventData } from '@haathie/pgmb'\n\ntype UserEventData = ITableMutationEventData\u003c\n\t// ideally the table's row type comes from some\n\t// type generator like pgtyped, kanel, etc\n\t{\n\t\tid: number\n\t\tcreated_at: string\n\t\tname: string\n\t\temail: string\n\t},\n\t'public.users'\n\u003e\n\n// then if you create the client as shown before:\nconst pgmb = new PgmbClient\u003cUserEventData\u003e({ ... })\n\n// you can now consume events with proper typing:\nconst sub = await pgmb.registerFireAndForgetHandler({})\nfor await(const { items } of sub) {\n\tfor(const item of items) {\n\t\tif(item.topic === 'public.users.insert') {\n\t\t\t// TypeScript knows that item.payload is of type\n\t\t\tconsole.log('New user created w ID:', item.payload.id)\n\t\t}\n\t}\n}\n```\n\n### Customising Table Events\n\nSometimes, it's not desirable to publish events for all mutations on a table, but only a subset of them. Or perhaps some transformations need to be applied to the data before publishing. To achieve this, PGMB allows you to override the default serialise function that determines whether to publish an event or not + have the ability to serialise the data to JSON on your own.\n\n``` sql\nREPLACE FUNCTION serialise_record_for_event(\n\ttabl oid,\n\top TEXT, -- 'INSERT', 'UPDATE', 'DELETE'\n\trecord RECORD,\n\tserialised OUT JSONB,\n\temit OUT BOOLEAN\n) AS $$\nBEGIN\n\tif tabl = 'public.users'::regclass THEN\n\t\tserialised := jsonb_build_object(\n\t\t\t'id', record.id,\n\t\t\t'name', record.name\n\t\t\t-- note: we're omitting email \u0026 created_at from the event payload\n\t\t);\n\t\t-- don't emit events for spam users, just a demo condition\n\t\temit := record.email NOT LIKE '%@spam.com';\n\t\tRETURN;\n\tEND IF;\n\t\n\t-- add other table customisations here, if needed\n\t\n\tserialised := to_jsonb(record);\n\temit := TRUE;\nEND\n$$ LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE\n\tSECURITY INVOKER;\n```\n\n## HTTP SSE Subscriptions\n\nPGMB makes it super easy to create HTTP SSE (Server Sent Events) endpoints that stream events to clients in real-time. PGMB also supports efficient resumption of SSE streams via the standard `Last-Event-ID` header, as events \u0026 their mappings are stored durably in Postgres.\n\n``` ts\nimport express from 'express'\n\nconst app = express()\n\nconst handler = createSSERequestHandler.call(pgmb, {\n\t// obtain subscription parameters based on the request,\n\t// can throw errors if the request is invalid, unauthenticated\n\t// or any other checks fail.\n\tgetSubscriptionOpts: req =\u003e (\n\t\t// we'll listen to user-updated events for the user ID\n\t\t// specified in the query param\n\t\tcreateTopicalSubscriptionParams({\n\t\t\ttopics: ['user-updated'],\n\t\t\t// the old row is stored in metadata-\u003e'old'\n\t\t\tadditionalFilters: { id: \"e.metadata-\u003e'old'-\u003e\u003e'id'\" },\n\t\t\tadditionalParams: { id: req.query.id },\n\t\t})\n\t),\n\t// upon reconnection, we'll replay at most 1000 events\n\tmaxReplayEvents: 1000,\n\t// we'll only allow replaying events that are at most 5 minutes old\n\tmaxReplayIntervalMs: 5 * 60 * 1000,\n})\n\napp.get('/sse', handler)\napp.listen(8000)\n```\n\nNow, on the client side, you can connect to this SSE endpoint as follows:\n\n``` js\nconst evtSource = new EventSource('/sse?id=123')\n\nevtSource.addEventListener('user-updated', function(event) {\n\tconst data = JSON.parse(event.data)\n\tconsole.log('User updated:', data)\n})\n```\n\nNote: in case an assertion with the \"last-event-id\" fails, PGMB will automatically close the SSE connection with a 204, which tells the client to not retry the connection.\n\n## Webhook Subscriptions\n\nPGMB also supports HTTP webhooks for events, with retry logic built-in.\n\n``` ts\nconst pgmb = new PgmbClient({\n\twebhookHandlerOpts: {\n\t\tretryOpts: {\n\t\t\t// if a webhook failes, retry it after\n\t\t\t// 1 minute, then after 5 minutes, then after 15 minutes\n\t\t\tretriesS: [60, 5 * 60, 15 * 60]\n\t\t},\n\t\t// timeout each webhook after 5 seconds\n\t\ttimeoutMs: 5000,\n\t\t// headers added to each webhook request\n\t\theaders: {\n\t\t\t// add a custom user-agent header\n\t\t\t'user-agent': 'my-service-client/1.0.0'\n\t\t},\n\t\t// if payload is larger than 1KB, will be sent as a gzipped body\n\t\t// with content-encoding: gzip header\n\t\tminCompressSizeBytes: 1024,\n\t},\n\t// this function is called to obtain webhook URLs\n\t// for a given set of PGMB subscription IDs. These\n\t// subscription IDs may have 0 or more webhook URLs\n\t// associated with them.\n\tasync getWebhookInfo(subIds) {\n\t\t// need to have a table that maps pgmb_sub_id to\n\t\t// webhook URL \u0026 a unique identifier for the webhook\n\t\t// subscription.\n\t\t// The PGMB subscription ID can map to multiple webhook\n\t\t// subscriptions.\n\t\t// This is just an example, adapt as per your schema.\n\t\tconst { rows } = await pgmb.client.query(\n\t\t\t`SELECT id, url, pgmb_sub_id as \"subId\"\n\t\t\tFROM webhook_subscriptions\n\t\t\tWHERE pgmb_sub_id = ANY($1)`,\n\t\t\t[subIds]\n\t\t)\n\t\treturn rows.reduce((acc, row) =\u003e {\n\t\t\tacc[row.subId] ||= []\n\t\t\tacc[row.subId].push({ url: row.url })\n\t\t\treturn acc\n\t\t}, {} as Record\u003cstring, WebhookInfo[]\u003e)\n\t}\n})\nawait pgmb.init()\n\n// creating a webhook subscription,\n// just as a more advanced example, we're inserting \n// the subscription \u0026 webhook in a single transaction\nconst cl = await pool.connect()\nawait cl.query('BEGIN')\n\n// create a webhook subscription for all \"order-placed\" events\n// (of course any other conditions can be used here too)\nconst sub = await pgmb.assertSubscription(\n\tcreateTopicalSubscriptionParams({\n\t\ttopics: ['order-placed']\n\t}),\n\tcl\n)\n\n// insert a webhook for this subscription\nawait cl.query(\n\t`INSERT INTO webhook_subscriptions (pgmb_sub_id, url)\n\tVALUES ($1, $2)`,\n\t[sub.id, 'https://webhook.site/12345']\n)\n\nawait cl.query('END')\ncl.release()\n```\n\nThe above setup will ensure that any webhooks associated with a subscription are called whenever events are published to that subscription. In case of failures, the webhooks will be retried based on the provided retry options.\nEach webhook request is further paired with a `idempotency-key` header, which remains constant across retries for the same event. This allows webhook handlers to be idempotent, and safely handle retries without worrying about duplicate processing.\n\nIf you need to override the serialisation of webhook payloads, you can provide a custom function while creating the `PgmbClient`:\n\n``` ts\nconst pgmb = new PgmbClient({\n\twebhookHandlerOpts: {\n\t\tserialiseEvent(ev) {\n\t\t\treturn {\n\t\t\t\tbody: JSON.stringify(ev),\n\t\t\t\tcontentType: 'application/json'\n\t\t\t}\n\t\t},\n\t\t...\n\t},\n\t...\n})\n```\n\n## Configuring Knobs\n\nPGMB provides a number of configuration options to tune its behaviour (eg. how often to poll for events, read events, expire subscriptions, etc.). These can be configured via relevant env vars too, the names for which can be found [here](src/client.ts#L105)\n``` ts\n// poll every 500ms\nprocess.env.PGMB_READ_EVENTS_INTERVAL_MS = '500' // default: 1000\nconst pgmb = new PgmbClient(opts)\n```\n\n## Production Considerations\n\nPGMB relies on 2 functions that need to be run periodically \u0026 only once globally to ensure smooth operation, i.e.\n1. `poll_for_events()` -- finds unread events \u0026 assigns them to relevant subscriptions.\n\tIt's okay if this runs simultaneously in multiple processes, but that can create unnecessary contention on the `unread_events` table, which can bubble up to other tables.\n2. `maintain_events_table()` -- removes old partitions \u0026 creates new partitions for the events table. It's also okay if this runs simultaneously in multiple processes, as it has advisory locks to ensure only a single process is maintaining the events table at any time, but running this too frequently can cause unnecessary overhead.\n\nIf you have the `pg_cron` extension installed, `pgmb` will automatically setup these functions to run periodically via `pg_cron` on initialization. The default intervals are:\n- `poll_for_events()` -- every `1 second`\n- `maintain_events_table()` -- on every `30th minute`\n\nThese can be easily configured by changing the values in the `config` table in the `pgmb` schema. Changes automatically get applied to the underlying cron jobs via triggers.\n\nIf you do not have `pg_cron` installed, PGMB will run these functions in the same process as the client itself, at the above intervals. This is okay for development \u0026 small deployments.\n\n## General Notes\n\n- **Does the client automatically reconnect on errors \u0026 temporary network issues?**\n\t- If using a `pg.Pool` as the client, yes -- the pool will automatically handle reconnections.\n\t- If using a single `pg.Client`, then no -- you'll have to handle reconnections on your own.\n- **What happens if we accidentally start multiple instances of the same PGMB client with the same `groupId`?**\n\tPGMB uses advisory locks to ensure that only a single instance of a subscription is active. If multiple instances are started with the same `groupId`, only one will be active, and the others will wait for the lock to be released. This ensures that there are no duplicate deliveries of events.\n- **Upgrade from 0.1.x to 0.2.x**\n\t`0.2.x` is a completely new implementation of PGMB, with a different architecture \u0026 design. So when upgrading from `0.1.x` to `0.2.x`, code will need to be rewritten to use the new API. However, the new schema does not interfere with the old schema, so both versions can co-exist in the same database, allowing for a gradual migration. To add 0.2.x to an existing database with 0.1.x, simply run the setup script for 0.2.x -- this will add the new tables \u0026 functions required for 0.2.x without affecting the existing 0.1.x setup.\n\tPractically all features from `0.1.x` can be achieved in `0.2.x`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhaathie%2Fpgmb","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhaathie%2Fpgmb","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhaathie%2Fpgmb/lists"}