<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: javier ramírez</title>
    <description>The latest articles on Forem by javier ramírez (@javier).</description>
    <link>https://forem.com/javier</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F14238%2Fbccb9985-00f6-48fa-85ae-f5cee5d7ead3.jpeg</url>
      <title>Forem: javier ramírez</title>
      <link>https://forem.com/javier</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/javier"/>
    <language>en</language>
    <item>
      <title>🔐 Using TLS with PgBouncer for QuestDB</title>
      <dc:creator>javier ramírez</dc:creator>
      <pubDate>Mon, 09 Jun 2025 15:45:14 +0000</pubDate>
      <link>https://forem.com/javier/using-tls-with-pgbouncer-for-questdb-2m62</link>
      <guid>https://forem.com/javier/using-tls-with-pgbouncer-for-questdb-2m62</guid>
      <description>&lt;p&gt;If you're putting PgBouncer in front of QuestDB to enable TLS, you might run into a confusing error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;closing because: server refused SSL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here’s what’s going on — and how to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧩 The Setup
&lt;/h2&gt;

&lt;p&gt;You likely have a &lt;code&gt;pgbouncer.ini&lt;/code&gt; that looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[databases]&lt;/span&gt;
&lt;span class="py"&gt;questdb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;host=127.0.0.1 port=8812 dbname=questdb user=admin password=secret&lt;/span&gt;

&lt;span class="nn"&gt;[pgbouncer]&lt;/span&gt;
&lt;span class="py"&gt;listen_addr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1&lt;/span&gt;
&lt;span class="py"&gt;listen_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;5432&lt;/span&gt;
&lt;span class="py"&gt;auth_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;trust&lt;/span&gt;

&lt;span class="py"&gt;client_tls_sslmode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;require&lt;/span&gt;
&lt;span class="py"&gt;client_tls_key_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/path/pgbouncer.key&lt;/span&gt;
&lt;span class="py"&gt;client_tls_cert_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/path/pgbouncer.crt&lt;/span&gt;
&lt;span class="py"&gt;client_tls_ca_file&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/etc/ssl/cert.pem&lt;/span&gt;

&lt;span class="py"&gt;server_tls_sslmode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;require&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You then launch PgBouncer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pgbouncer ./pgbouncer.ini
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And try connecting like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;psql &lt;span class="s2"&gt;"host=127.0.0.1 port=5432 dbname=questdb user=admin sslmode=require"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the connection fails with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;closing because: server refused SSL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💡 What’s Going Wrong?
&lt;/h2&gt;

&lt;p&gt;QuestDB (Open Source) &lt;strong&gt;does not support TLS over the PostgreSQL wire protocol&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So when PgBouncer tries to connect to QuestDB using &lt;code&gt;server_tls_sslmode = require&lt;/code&gt;, the QuestDB server responds with “refused SSL.”&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ The Fix: Disable TLS Between PgBouncer and QuestDB
&lt;/h2&gt;

&lt;p&gt;Update your &lt;code&gt;pgbouncer.ini&lt;/code&gt; config like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;-server_tls_sslmode = require
&lt;/span&gt;&lt;span class="gi"&gt;+server_tls_sslmode = disable
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps TLS &lt;strong&gt;enabled from the client to PgBouncer&lt;/strong&gt;, but disables it &lt;strong&gt;between PgBouncer and QuestDB&lt;/strong&gt;, which is necessary for compatibility.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔐 TLS Path Overview
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Connection&lt;/th&gt;
&lt;th&gt;TLS Enabled?&lt;/th&gt;
&lt;th&gt;PgBouncer Config&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Client → PgBouncer&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;client_tls_sslmode = require&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PgBouncer → QuestDB&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;td&gt;&lt;code&gt;server_tls_sslmode = disable&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This lets you offer encrypted connections to clients, even if QuestDB itself doesn’t handle TLS.&lt;/p&gt;




&lt;h2&gt;
  
  
  📝 Final Thoughts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;QuestDB OSS doesn’t support TLS on the PostgreSQL wire protocol.&lt;/li&gt;
&lt;li&gt;You can terminate TLS at PgBouncer.&lt;/li&gt;
&lt;li&gt;If you need full end-to-end encryption, you’ll need QuestDB Enterprise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This setup is ideal for local or internal deployments where PgBouncer acts as a secure gateway to QuestDB.&lt;/p&gt;

</description>
      <category>questdb</category>
      <category>timeseries</category>
      <category>database</category>
    </item>
    <item>
      <title>⏱️ Taming QuestDB Materialized View Refresh Frequency</title>
      <dc:creator>javier ramírez</dc:creator>
      <pubDate>Mon, 02 Jun 2025 15:54:00 +0000</pubDate>
      <link>https://forem.com/javier/taming-questdb-materialized-view-refresh-frequency-49e6</link>
      <guid>https://forem.com/javier/taming-questdb-materialized-view-refresh-frequency-49e6</guid>
      <description>&lt;p&gt;Ever implemented a materialized view in QuestDB only to find it refreshing &lt;strong&gt;constantly&lt;/strong&gt;, even when you just want daily snapshots? You're not alone. By default, QuestDB materialized views use &lt;code&gt;INCREMENTAL&lt;/code&gt; refresh mode — which means they update &lt;strong&gt;as soon as new data arrives&lt;/strong&gt; in the base table.&lt;/p&gt;

&lt;p&gt;This is great for real-time dashboards… but wasteful if you only need a daily roll-up for analysis.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔧 The Problem: Too Many Refreshes
&lt;/h2&gt;

&lt;p&gt;Let’s say you have a &lt;code&gt;trades&lt;/code&gt; table with market data arriving every second. If you create a materialized view like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;trades_latest_1d&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;trades&lt;/span&gt;
  &lt;span class="n"&gt;SAMPLE&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will &lt;strong&gt;refresh every time new trades are inserted&lt;/strong&gt; — hundreds or thousands of times per day. That's the default behavior of &lt;code&gt;INCREMENTAL&lt;/code&gt; refresh mode.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ The Solution: Use &lt;code&gt;incremental_timer&lt;/code&gt; Instead
&lt;/h2&gt;

&lt;p&gt;You can control how often your materialized view refreshes by specifying a scheduled interval. Here's how:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;MATERIALIZED&lt;/span&gt; &lt;span class="k"&gt;VIEW&lt;/span&gt; &lt;span class="n"&gt;trades_latest_1d&lt;/span&gt;
  &lt;span class="n"&gt;REFRESH&lt;/span&gt; &lt;span class="k"&gt;START&lt;/span&gt; &lt;span class="s1"&gt;'2025-05-30T00:00:00.000000Z'&lt;/span&gt;
  &lt;span class="k"&gt;EVERY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;last&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;trades&lt;/span&gt;
  &lt;span class="n"&gt;SAMPLE&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;PARTITION&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This switches the view to &lt;code&gt;incremental_timer&lt;/code&gt; mode, which means it will &lt;strong&gt;only refresh once per day&lt;/strong&gt;, at midnight UTC. You can adjust the &lt;code&gt;EVERY&lt;/code&gt; clause to suit your needs (e.g. &lt;code&gt;1h&lt;/code&gt;, &lt;code&gt;5m&lt;/code&gt;, &lt;code&gt;7d&lt;/code&gt;, etc).&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 How to Check If It’s Working
&lt;/h2&gt;

&lt;p&gt;After creating the view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;materialized_views&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;refresh_type = incremental_timer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;last_refresh_start_timestamp = null&lt;/code&gt; (initially)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then, after the first scheduled refresh (in this case, at midnight), the view will populate and the timestamp fields will be updated.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 If You Want More Frequent Updates
&lt;/h2&gt;

&lt;p&gt;If you still want intraday updates but less noise, you could do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;REFRESH&lt;/span&gt; &lt;span class="k"&gt;START&lt;/span&gt; &lt;span class="s1"&gt;'2025-05-30T00:00:00.000000Z'&lt;/span&gt;
&lt;span class="k"&gt;EVERY&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the view only refreshes every 5 minutes — much saner than every insert.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 TL;DR
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Refresh Mode&lt;/th&gt;
&lt;th&gt;Recommended Interval&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real-time dashboard&lt;/td&gt;
&lt;td&gt;&lt;code&gt;INCREMENTAL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;N/A (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Daily OHLC summary&lt;/td&gt;
&lt;td&gt;&lt;code&gt;incremental_timer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EVERY 1d&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Light intraday reporting&lt;/td&gt;
&lt;td&gt;&lt;code&gt;incremental_timer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;EVERY 5m&lt;/code&gt; or &lt;code&gt;1h&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weekly snapshot for archiving&lt;/td&gt;
&lt;td&gt;&lt;code&gt;incremental_timer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;EVERY 7d&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  📝 Final Thoughts
&lt;/h2&gt;

&lt;p&gt;QuestDB’s default behavior favors low-latency use cases. But if you're building a daily view of activity, using &lt;code&gt;REFRESH EVERY&lt;/code&gt; gives you total control over how often data is materialized — saving compute, reducing WAL activity, and improving predictability.&lt;/p&gt;

&lt;p&gt;Perfect for finance, monitoring, and anyone with large data volumes but low update frequency requirements.&lt;/p&gt;

</description>
      <category>questdb</category>
      <category>database</category>
      <category>timeseries</category>
    </item>
  </channel>
</rss>
