From f79a29fec587e36c8a249cfae1364d90e69458ca Mon Sep 17 00:00:00 2001 From: Chris Rimmer Date: Tue, 18 Nov 2025 23:10:29 +0000 Subject: [PATCH 1/2] Re-queue failed pageview batch for retry on next interval --- server/src/services/tracker/pageviewQueue.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/services/tracker/pageviewQueue.ts b/server/src/services/tracker/pageviewQueue.ts index ac20e1f76..3fb34307e 100644 --- a/server/src/services/tracker/pageviewQueue.ts +++ b/server/src/services/tracker/pageviewQueue.ts @@ -124,7 +124,9 @@ class PageviewQueue { format: "JSONEachRow", }); } catch (error) { - this.logger.error(error, "Error processing pageview queue"); + this.logger.error(error, `Error processing pageview queue, re-adding ${batch.length} items to the start of the queue`); + // Re-queue the failed batch so it can be retried on the next interval + this.queue.unshift(...batch); } finally { this.processing = false; } From f4efd7f4364eeef9431cf966700aed9ede53ba78 Mon Sep 17 00:00:00 2001 From: Chris Rimmer Date: Fri, 21 Nov 2025 19:50:34 +0000 Subject: [PATCH 2/2] Add max retries for ClickHouse pageview inserts with exponential backoff --- .../self-hosting-advanced.mdx | 5 ++- server/src/services/tracker/pageviewQueue.ts | 31 +++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/docs/content/docs/self-hosting-guides/self-hosting-advanced.mdx b/docs/content/docs/self-hosting-guides/self-hosting-advanced.mdx index f79ce2cb1..d989b19e9 100644 --- a/docs/content/docs/self-hosting-guides/self-hosting-advanced.mdx +++ b/docs/content/docs/self-hosting-guides/self-hosting-advanced.mdx @@ -86,4 +86,7 @@ IMAGE_TAG=latest # Port mapping (only needed for custom ports or --no-webserver) HOST_BACKEND_PORT="3001:3001" HOST_CLIENT_PORT="3002:3002" -``` \ No newline at end of file + +# ClickHouse insert retries for pageview queue (default: 3 retries after the initial attempt) +PAGEVIEW_QUEUE_MAX_RETRIES=3 +``` diff --git a/server/src/services/tracker/pageviewQueue.ts b/server/src/services/tracker/pageviewQueue.ts index 3fb34307e..18bb836d4 100644 --- a/server/src/services/tracker/pageviewQueue.ts +++ b/server/src/services/tracker/pageviewQueue.ts @@ -10,6 +10,11 @@ type TotalPayload = TotalTrackingPayload & { sessionId: string; }; +const parsePositiveNumber = (value: string | undefined, fallback: number) => { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +}; + const getParsedProperties = (properties: string | undefined) => { try { return properties ? JSON.parse(properties) : undefined; @@ -24,6 +29,7 @@ class PageviewQueue { private interval = 10000; private processing = false; private logger = createServiceLogger("pageview-queue"); + private maxRetries = parsePositiveNumber(process.env.PAGEVIEW_QUEUE_MAX_RETRIES, 3); constructor() { // Start processing interval @@ -116,7 +122,16 @@ class PageviewQueue { }); this.logger.info({ count: processedPageviews.length }, "Bulk insert to ClickHouse"); - // Bulk insert into database + try { + await this.insertBatchWithRetry(processedPageviews); + } catch (error) { + this.logger.error(error, `Error processing pageview queue after ${this.maxRetries + 1} attempts, dropping ${batch.length} items`); + } finally { + this.processing = false; + } + } + + private async insertBatchWithRetry(processedPageviews: Record[], retryCount = 0): Promise { try { await clickhouse.insert({ table: "events", @@ -124,11 +139,15 @@ class PageviewQueue { format: "JSONEachRow", }); } catch (error) { - this.logger.error(error, `Error processing pageview queue, re-adding ${batch.length} items to the start of the queue`); - // Re-queue the failed batch so it can be retried on the next interval - this.queue.unshift(...batch); - } finally { - this.processing = false; + if (retryCount >= this.maxRetries) { + throw error; + } + + const retryAttempt = retryCount + 1; + const delay = this.interval * Math.pow(2, retryCount); + this.logger.warn({ retryAttempt, delay }, "Bulk insert to ClickHouse failed, retrying"); + await new Promise(resolve => setTimeout(resolve, delay)); + await this.insertBatchWithRetry(processedPageviews, retryCount + 1); } } }