<?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>DEV Community: Ritom Puzari</title>
    <description>The latest articles on DEV Community by Ritom Puzari (@ritompuzari).</description>
    <link>https://dev.to/ritompuzari</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4133424%2Fae06e979-a76e-43fe-9b59-efc4c26013ff.jpg</url>
      <title>DEV Community: Ritom Puzari</title>
      <link>https://dev.to/ritompuzari</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ritompuzari"/>
    <language>en</language>
    <item>
      <title>See every API your server calls (and every call it receives) without changing a line of code</title>
      <dc:creator>Ritom Puzari</dc:creator>
      <pubDate>Sat, 19 Sep 2026 21:45:20 +0000</pubDate>
      <link>https://dev.to/ritompuzari/see-every-api-your-server-calls-and-every-call-it-receives-without-changing-a-line-of-code-39fh</link>
      <guid>https://dev.to/ritompuzari/see-every-api-your-server-calls-and-every-call-it-receives-without-changing-a-line-of-code-39fh</guid>
      <description>&lt;p&gt;Instrumenting an application for APM means picking a vendor SDK, adding it to every service, redeploying, and hoping the framework version is supported. There is an older trick that gets you most of the value with none of that: listen to the network interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the host can see
&lt;/h2&gt;

&lt;p&gt;A raw socket opened with &lt;code&gt;socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL))&lt;/code&gt; on Linux receives a copy of every frame that crosses any interface, the same tap &lt;code&gt;tcpdump&lt;/code&gt; uses. On its own that is expensive on a busy box, so the first thing to do is attach a classic BPF program with &lt;code&gt;setsockopt(SO_ATTACH_FILTER)&lt;/code&gt;. The nine-instruction program that &lt;code&gt;tcpdump -dd tcp&lt;/code&gt; prints accepts IPv4 and IPv6 frames whose next header is TCP and rejects everything else inside the kernel, before a single byte is copied to user space. The agent uses exactly that program, so UDP, ARP and ICMP never reach Python.&lt;/p&gt;

&lt;p&gt;What remains is a stream of TCP segments. You do not need full stream reassembly to get useful data, because the interesting bytes sit at the start of a segment: an HTTP/1.x request line begins a client segment, a status line begins a server segment, and a TLS ClientHello is the first record on a fresh connection. Keying by the 4-tuple (source address, source port, destination address, destination port) and pairing a request with the next response on the reversed tuple gives you a latency sample per request. From that you can reconstruct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain HTTP/1.x&lt;/strong&gt;: method, path (query string stripped), &lt;code&gt;Host&lt;/code&gt;, status code, bytes each way, and the time from the request segment to the first byte of the response. That is enough for per-endpoint counts, 4xx/5xx rates and p50/p95/max latency. Absolute-form request targets, which proxies use, are normalised to a path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS&lt;/strong&gt;: not the payload. The &lt;code&gt;ClientHello&lt;/code&gt; (handshake type 1 inside a record of content type 22) carries the Server Name Indication extension (type 0) in clear text on TLS 1.2 and 1.3, so you still learn which host the server is talking to, on which port, how many connections, how many bytes each way, and the round-trip time from ClientHello to ServerHello. That is your outgoing dependency map: Stripe, your database host, an S3 endpoint, a third-party API nobody knew about. Encrypted ClientHello (ECH) hides SNI, but adoption on API endpoints is still rare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local services&lt;/strong&gt;: traffic between your reverse proxy and your app on &lt;code&gt;127.0.0.1:8000&lt;/code&gt; is usually plain HTTP, so you get full request-level detail for your own application even when the public side is TLS.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it cannot see
&lt;/h2&gt;

&lt;p&gt;HTTP/2 multiplexes streams inside one connection and is almost always inside TLS; without keys it shows up as an opaque flow with byte counts. gRPC likewise. Cleartext HTTP/2 (&lt;code&gt;h2c&lt;/code&gt;) is binary-framed, so the request-line parser skips it too. For those you want either the reverse proxy's access log (nginx, Apache and Caddy formats, which the same agent tails from the standard paths) or an in-process SDK. Two more limits worth knowing: a request that spans several segments is only parsed if the request line and &lt;code&gt;Host&lt;/code&gt; header are in the first 2 KB of the first segment, and keep-alive connections that pipeline requests are attributed by order, which is correct for HTTP/1.1 because responses must arrive in request order.&lt;/p&gt;

&lt;h2&gt;
  
  
  From packets to something useful
&lt;/h2&gt;

&lt;p&gt;Raw requests are noise. The useful transformation is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Normalise paths so cardinality stays bounded: &lt;code&gt;/users/1234/orders/550e8400-e29b-41d4-a716-446655440000&lt;/code&gt; becomes &lt;code&gt;/users/{id}/orders/{uuid}&lt;/code&gt;. Purely numeric segments become &lt;code&gt;{id}&lt;/code&gt;, UUIDs &lt;code&gt;{uuid}&lt;/code&gt;, hex strings of 16 or more characters &lt;code&gt;{hash}&lt;/code&gt;, long base64-looking segments &lt;code&gt;{token}&lt;/code&gt;. Everything else is kept verbatim so &lt;code&gt;/v1/users&lt;/code&gt; and &lt;code&gt;/v2/users&lt;/code&gt; stay distinct.&lt;/li&gt;
&lt;li&gt;Aggregate per reporting interval: count, 4xx and 5xx counts, and p50/p95/max latency per &lt;code&gt;(direction, host, method, path)&lt;/code&gt;. Percentiles are computed from the raw samples in that interval, not from averages of averages.&lt;/li&gt;
&lt;li&gt;Keep a small sample of the slowest and failing requests with their timing, so an incident shows evidence, not just a number.&lt;/li&gt;
&lt;li&gt;Roll up hourly for history, and build the service map from the outgoing side: this server talks to these hosts, this often, this slowly. The reverse-DNS name of each destination is resolved once and cached, so the map shows &lt;code&gt;api.stripe.com&lt;/code&gt;, not an IP.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Doing it with the Vigil agent
&lt;/h2&gt;

&lt;p&gt;The agent is a single Python file that uses only the standard library, so it runs on any Linux with Python 3.8 or newer. Installed as root it enables capture automatically; without root it still reports metrics, processes and connections from &lt;code&gt;/proc/net/tcp&lt;/code&gt;, but not per-request detail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://puzaricloud.in/install-server-agent.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--token&lt;/span&gt; vgs_YOUR_TOKEN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server's &lt;strong&gt;APIs&lt;/strong&gt; tab then shows incoming endpoints with p95 and 5xx rates, the &lt;strong&gt;Network&lt;/strong&gt; tab shows outgoing hosts with connections and bytes, and the &lt;strong&gt;Services&lt;/strong&gt; tab draws the map. Rules on &lt;code&gt;http_error_pct&lt;/code&gt;, &lt;code&gt;http_p95_ms&lt;/code&gt; and &lt;code&gt;new_destination&lt;/code&gt; open incidents, so a new outbound host appearing at 3 am pages you, which is one of the cheaper ways to notice a compromised box.&lt;/p&gt;

&lt;p&gt;For request traces across services, drop in the Python or Node SDK; it reports to the same local agent on &lt;code&gt;127.0.0.1:9111&lt;/code&gt; and links spans to the captured endpoints.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy and safety notes
&lt;/h2&gt;

&lt;p&gt;The parser reads the request line and the &lt;code&gt;Host&lt;/code&gt; header, and the status line of the response. It never parses other headers or any body, so &lt;code&gt;Authorization&lt;/code&gt;, &lt;code&gt;Cookie&lt;/code&gt;, query strings and payloads are not in memory beyond the kernel buffer and never leave the machine. Only aggregates and a handful of slow-request samples (method, path, status, timing) are reported. If your compliance rules forbid even that, turn capture off per server and rely on access logs and SDK spans instead.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://puzaricloud.in/blog/capture-every-api-call-on-a-linux-server" rel="noopener noreferrer"&gt;PuzariCloud engineering blog&lt;/a&gt;. Drafted with AI assistance and reviewed, edited and tested by the author, who builds &lt;a href="https://puzaricloud.in" rel="noopener noreferrer"&gt;Vigil by PuzariCloud&lt;/a&gt;, the monitoring service the examples use.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>apm</category>
      <category>linux</category>
      <category>networking</category>
    </item>
    <item>
      <title>Self-hosting a full monitoring stack in one command: servers, logs, APM and status pages</title>
      <dc:creator>Ritom Puzari</dc:creator>
      <pubDate>Sat, 19 Sep 2026 21:45:16 +0000</pubDate>
      <link>https://dev.to/ritompuzari/self-hosting-a-full-monitoring-stack-in-one-command-servers-logs-apm-and-status-pages-10lc</link>
      <guid>https://dev.to/ritompuzari/self-hosting-a-full-monitoring-stack-in-one-command-servers-logs-apm-and-status-pages-10lc</guid>
      <description>&lt;p&gt;Teams self-host monitoring for two reasons: the data cannot leave the building, or the SaaS bill grew faster than the fleet. The catch is that "self-hosted monitoring" usually means five services. This post is about what those five are, what running them really costs, and how to collapse them into one install.&lt;/p&gt;

&lt;h2&gt;
  
  
  The usual stack and what each piece costs you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Common choice&lt;/th&gt;
&lt;th&gt;What you maintain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Host metrics&lt;/td&gt;
&lt;td&gt;Prometheus + node_exporter&lt;/td&gt;
&lt;td&gt;retention sizing, cardinality, remote storage once it grows&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboards + alerts&lt;/td&gt;
&lt;td&gt;Grafana + Alertmanager&lt;/td&gt;
&lt;td&gt;provisioning, dashboards as code, alert routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logs&lt;/td&gt;
&lt;td&gt;Loki + Promtail&lt;/td&gt;
&lt;td&gt;chunk storage, label discipline, query limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uptime and status page&lt;/td&gt;
&lt;td&gt;Uptime Kuma&lt;/td&gt;
&lt;td&gt;one more SQLite database to back up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traces&lt;/td&gt;
&lt;td&gt;Tempo or Jaeger&lt;/td&gt;
&lt;td&gt;object storage and sampling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each is good software. Together they are roughly a day to set up properly, an afternoon a month to keep patched, and one incident a quarter when a disk fills or an upgrade changes a config format. For a platform team that is fine. For two developers doing ops on the side, it is the reason monitoring quietly stops getting attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a single install has to do to replace that
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Collect host metrics, processes, containers and services with one agent that needs no configuration.&lt;/li&gt;
&lt;li&gt;Ship logs from the same agent, with search and alerting on patterns.&lt;/li&gt;
&lt;li&gt;Give you request-level visibility without instrumenting every service. Passive capture of HTTP on the host gets you per-endpoint counts, error rates and p95 latency with zero code changes; SDKs add traces where you want them.&lt;/li&gt;
&lt;li&gt;Run outside-in checks (HTTP, TCP, DNS, TLS, cron heartbeats) and a status page.&lt;/li&gt;
&lt;li&gt;Update itself safely, verify what it installs, and keep working when the licence server or the internet is unreachable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The one-command version
&lt;/h2&gt;

&lt;p&gt;Vigil's self-hosted edition installs like this on any Ubuntu, Debian, RHEL-family or Alpine host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://puzaricloud.in/selfhosted/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;sh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--domain&lt;/span&gt; vigil.example.com &lt;span class="nt"&gt;--email&lt;/span&gt; you@example.com &lt;span class="nt"&gt;--license&lt;/span&gt; &lt;span class="s1"&gt;'VGL1.…'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It installs Docker if missing, downloads the release bundle and its detached Ed25519 signature, and verifies the signature with &lt;code&gt;openssl pkeyutl -verify -rawin&lt;/code&gt; against a public key embedded in the installer before extracting anything. Ed25519 was chosen over RSA because the signature is 64 bytes, verification is a single call in OpenSSL 1.1.1 or newer with no hash negotiation to get wrong, and the same key signs licence keys, so there is one root of trust to protect. The installer then writes a Caddyfile with automatic HTTPS (ACME through Let's Encrypt, or &lt;code&gt;tls internal&lt;/code&gt; for private networks) and starts Postgres, a worker and two web replicas behind Caddy's load balancer with active health checks.&lt;/p&gt;

&lt;p&gt;Two replicas are what make updates safe. &lt;code&gt;vigil update&lt;/code&gt; downloads and verifies the new bundle, builds the image, then replaces the first web container and waits for its health check to pass before touching the second, so there is never a moment without a healthy backend. If the new version fails its health check within two minutes the previous image is retagged and both replicas roll back. &lt;code&gt;vigil backup&lt;/code&gt; runs &lt;code&gt;pg_dump&lt;/code&gt; in custom format plus the config files every six hours by cron and keeps 14 daily and 8 weekly archives.&lt;/p&gt;

&lt;p&gt;For private networks, &lt;code&gt;--internal-tls&lt;/code&gt; makes Caddy issue certificates from its own local CA and &lt;code&gt;--no-phone-home&lt;/code&gt; keeps the instance fully offline. A licence key is a signed JSON payload (&lt;code&gt;VGL1.&amp;lt;payload&amp;gt;.&amp;lt;signature&amp;gt;&lt;/code&gt;) that the instance verifies locally against the embedded public key, so the instance runs with no outbound connection at all. On first start each installation generates its own Ed25519 identity, and the licence that finally unlocks it is bound to that identity's fingerprint, which is what stops one key being copied across many hosts. When a key lapses there are 14 days of grace; after that the instance becomes read-only rather than stopping monitoring, because an expired licence should never be the reason you miss an outage.&lt;/p&gt;

&lt;p&gt;You can inspect exactly what the installer does before running it: &lt;a href="https://puzaricloud.in/selfhosted/install.sh" rel="noopener noreferrer"&gt;install.sh&lt;/a&gt; and its signature are published next to the &lt;a href="https://puzaricloud.in/selfhosted/pubkey" rel="noopener noreferrer"&gt;public key&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resource use
&lt;/h2&gt;

&lt;p&gt;Postgres is the only stateful part. A fleet of 20 servers reporting every 30 seconds with logs enabled lands around 2 GB of database growth a month at the default retention.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should still run the big stack
&lt;/h2&gt;

&lt;p&gt;If you need distributed tracing across dozens of services with high-cardinality labels, or you already have Prometheus expertise and dashboards you love, keep them. The one-install approach is for teams who want monitoring to be a thing they have, not a thing they run.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://puzaricloud.in/blog/self-host-monitoring-in-one-command" rel="noopener noreferrer"&gt;PuzariCloud engineering blog&lt;/a&gt;. Drafted with AI assistance and reviewed, edited and tested by the author, who builds &lt;a href="https://puzaricloud.in" rel="noopener noreferrer"&gt;Vigil by PuzariCloud&lt;/a&gt;, the monitoring service the examples use.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>docker</category>
      <category>observability</category>
    </item>
    <item>
      <title>Get alerted before an SSL certificate expires (the three checks that actually matter)</title>
      <dc:creator>Ritom Puzari</dc:creator>
      <pubDate>Sat, 19 Sep 2026 21:26:32 +0000</pubDate>
      <link>https://dev.to/ritompuzari/get-alerted-before-an-ssl-certificate-expires-the-three-checks-that-actually-matter-14k0</link>
      <guid>https://dev.to/ritompuzari/get-alerted-before-an-ssl-certificate-expires-the-three-checks-that-actually-matter-14k0</guid>
      <description>&lt;p&gt;Let's Encrypt made certificates free and short-lived. It did not make them renew themselves. Renewal fails silently when a DNS record changes, a firewall rule blocks port 80, a cron job stops, or someone moves the site to a new server and forgets the hook. The certificate keeps working right up to the second it does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check expiry from the shell
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-dates&lt;/span&gt; &lt;span class="nt"&gt;-issuer&lt;/span&gt; &lt;span class="nt"&gt;-subject&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output includes &lt;code&gt;notAfter=&lt;/code&gt;. Anything under 14 days on a Let's Encrypt certificate (90-day lifetime, renewed by certbot at 30 days remaining, twice a day) means dozens of renewal attempts have already failed. The &lt;code&gt;-servername&lt;/code&gt; flag matters: it sets SNI, and without it a server hosting several sites returns its default certificate, which is a common source of false alarms in home-grown checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the chain, not just the leaf
&lt;/h2&gt;

&lt;p&gt;Browsers accept a certificate only when they can build a chain to a trusted root. A server that sends the leaf without the intermediate works in Chrome (which caches intermediates) and fails in &lt;code&gt;curl&lt;/code&gt;, in mobile apps and in every monitoring script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSI&lt;/span&gt; https://example.com &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo &lt;/span&gt;chain ok
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this fails with &lt;code&gt;unable to get local issuer certificate&lt;/code&gt; while the browser is happy, the intermediate is missing. Chrome and Firefox fetch missing intermediates through the Authority Information Access extension or use a cache; OpenSSL-based clients do not. Fix it in the web server's &lt;code&gt;ssl_certificate&lt;/code&gt; (nginx wants &lt;code&gt;fullchain.pem&lt;/code&gt;, not &lt;code&gt;cert.pem&lt;/code&gt;); in Caddy nothing is needed, because Caddy manages it. To see what the server actually sends, &lt;code&gt;openssl s_client -showcerts&lt;/code&gt; prints every certificate in the handshake, and a chain of one is the tell.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the hostname
&lt;/h2&gt;

&lt;p&gt;A certificate for &lt;code&gt;www.example.com&lt;/code&gt; does not cover &lt;code&gt;example.com&lt;/code&gt; unless it is in the Subject Alternative Names; since 2017 browsers ignore the Common Name entirely, so a certificate with only a CN fails everywhere modern. A wildcard &lt;code&gt;*.example.com&lt;/code&gt; matches one label only: it covers &lt;code&gt;api.example.com&lt;/code&gt; but not &lt;code&gt;example.com&lt;/code&gt; and not &lt;code&gt;v2.api.example.com&lt;/code&gt;. Redirects hide these gaps: the bare domain redirects to &lt;code&gt;www&lt;/code&gt; before the browser complains, but a monitoring probe, an API client or a webhook hitting the bare host gets a hostname mismatch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; | openssl s_client &lt;span class="nt"&gt;-servername&lt;/span&gt; example.com &lt;span class="nt"&gt;-connect&lt;/span&gt; example.com:443 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  | openssl x509 &lt;span class="nt"&gt;-noout&lt;/span&gt; &lt;span class="nt"&gt;-ext&lt;/span&gt; subjectAltName
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why "we have auto-renewal" is not a monitoring strategy
&lt;/h2&gt;

&lt;p&gt;Auto-renewal is a process. Monitoring watches the outcome. The outcome is "the certificate the public sees expires on date X", and only an external check sees exactly what a visitor sees, including the wrong certificate served by a load balancer that still holds last year's file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a warning 14 days out
&lt;/h2&gt;

&lt;p&gt;Three options, from most to least effort:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A cron job around the &lt;code&gt;openssl&lt;/code&gt; command above that compares &lt;code&gt;notAfter&lt;/code&gt; with &lt;code&gt;date&lt;/code&gt; and posts to Slack. Works, breaks when the box that runs it is decommissioned.&lt;/li&gt;
&lt;li&gt;A Prometheus &lt;code&gt;blackbox_exporter&lt;/code&gt; probe with the &lt;code&gt;probe_ssl_earliest_cert_expiry&lt;/code&gt; metric and an alert rule. Solid if you already run Prometheus.&lt;/li&gt;
&lt;li&gt;An uptime monitor that checks the certificate as part of its HTTPS check. In Vigil every https monitor runs a dedicated TLS handshake every 12 hours, records the expiry, and alerts once per certificate when the days left cross the threshold you set per monitor (14 by default). Because the alert is keyed to the certificate's expiry date, a renewal resets it automatically and a certificate that keeps not renewing does not spam you. The chain and hostname problems above show up as a failed check with the exact &lt;code&gt;openssl&lt;/code&gt; error attached, so you are not guessing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can run all three checks on any hostname right now without an account at &lt;a href="https://puzaricloud.in/tools/ssl-check" rel="noopener noreferrer"&gt;/tools/ssl-check&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist for the next incident review
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Is the expiry alert sent to a channel someone reads, not only to the ops mailbox nobody opens?&lt;/li&gt;
&lt;li&gt;Does the alert fire at 14 days, when there is still time to debug renewal, rather than at 24 hours?&lt;/li&gt;
&lt;li&gt;Are the bare domain, &lt;code&gt;www&lt;/code&gt;, the API host and the status page all monitored separately? They can have four different certificates.&lt;/li&gt;
&lt;li&gt;Does the monitoring probe run from outside your network? A probe on the same box sees a different picture from your customers.&lt;/li&gt;
&lt;li&gt;Is OCSP stapling on, and is the stapled response fresh? A stale staple with &lt;code&gt;Must-Staple&lt;/code&gt; set fails the handshake in Firefox even when the certificate itself is fine. &lt;code&gt;openssl s_client -status&lt;/code&gt; shows the staple.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://puzaricloud.in/blog/alert-before-ssl-certificate-expires" rel="noopener noreferrer"&gt;PuzariCloud engineering blog&lt;/a&gt;. Drafted with AI assistance and reviewed, edited and tested by the author, who builds &lt;a href="https://puzaricloud.in" rel="noopener noreferrer"&gt;Vigil by PuzariCloud&lt;/a&gt;, the monitoring service the examples use.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tls</category>
      <category>ssl</category>
      <category>uptime</category>
    </item>
    <item>
      <title>How to monitor PostgreSQL connections, cache hit ratio and replication lag (and get paged before it hurts)</title>
      <dc:creator>Ritom Puzari</dc:creator>
      <pubDate>Sat, 19 Sep 2026 21:26:29 +0000</pubDate>
      <link>https://dev.to/ritompuzari/how-to-monitor-postgresql-connections-cache-hit-ratio-and-replication-lag-and-get-paged-before-it-31gb</link>
      <guid>https://dev.to/ritompuzari/how-to-monitor-postgresql-connections-cache-hit-ratio-and-replication-lag-and-get-paged-before-it-31gb</guid>
      <description>&lt;p&gt;Most PostgreSQL outages announce themselves an hour early. Connections creep towards &lt;code&gt;max_connections&lt;/code&gt;, the buffer cache starts missing, a replica falls behind. Nobody looks, because the dashboard that shows it lives in a tool nobody opens. This guide covers the four numbers worth alerting on, the queries behind them, and thresholds that page rarely but early.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Connections against max_connections
&lt;/h2&gt;

&lt;p&gt;The single most common self-inflicted PostgreSQL outage is &lt;code&gt;FATAL: sorry, too many clients already&lt;/code&gt;. It happens when an app pool grows, a migration script leaks connections, or someone runs &lt;code&gt;pgbouncer&lt;/code&gt; in the wrong mode.&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="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&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;used&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'max_connections'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'max_connections'&lt;/span&gt;&lt;span class="p"&gt;)::&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&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;pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Alert at &lt;strong&gt;90 %&lt;/strong&gt; of the maximum. Warn at 75 % if you want a heads-up. Note that &lt;code&gt;max_connections&lt;/code&gt; includes &lt;code&gt;superuser_reserved_connections&lt;/code&gt; (3 by default), so ordinary roles hit the wall a few connections earlier than the number suggests. Look at the &lt;code&gt;state&lt;/code&gt; column too: dozens of &lt;code&gt;idle in transaction&lt;/code&gt; sessions mean an application is holding transactions open, which pins the xmin horizon so vacuum cannot reclaim dead tuples and, with a long enough hold, blocks DDL behind an &lt;code&gt;ACCESS EXCLUSIVE&lt;/code&gt; lock queue.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Cache hit ratio
&lt;/h2&gt;

&lt;p&gt;Postgres serves reads from &lt;code&gt;shared_buffers&lt;/code&gt; when it can and from the OS page cache or disk when it cannot. A hit ratio that drops below 90 % on an OLTP database usually means the working set outgrew memory, a new query is scanning a big table, or somebody shrank the instance.&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="n"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blks_hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;nullif&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blks_hit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blks_read&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&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;cache_hit_pct&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_database&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;datname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_database&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These counters are cumulative since the last &lt;code&gt;pg_stat_reset()&lt;/code&gt;, so a monitor has to diff consecutive readings and compute the ratio on the deltas. Alert when the &lt;strong&gt;rolling ratio stays under 90 %&lt;/strong&gt; for several minutes rather than on a single reading. One caveat: &lt;code&gt;blks_read&lt;/code&gt; counts reads from the OS page cache as misses, because Postgres cannot see the kernel cache. On a host with lots of free memory a "miss" may still be served from RAM, so pair this number with the host's disk read throughput before concluding the working set outgrew memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Replication lag
&lt;/h2&gt;

&lt;p&gt;On a streaming replica:&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="k"&gt;CASE&lt;/span&gt; &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;pg_last_wal_receive_lsn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pg_last_wal_replay_lsn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
       &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="k"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epoch&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pg_last_xact_replay_timestamp&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;lag_seconds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CASE&lt;/code&gt; matters: when the replica has replayed everything it received, &lt;code&gt;pg_last_xact_replay_timestamp()&lt;/code&gt; stops advancing on an idle primary, and the naive &lt;code&gt;now() - replay_timestamp&lt;/code&gt; reports growing lag on a system that is perfectly in sync. On the primary, &lt;code&gt;pg_stat_replication&lt;/code&gt; shows every replica's &lt;code&gt;write_lag&lt;/code&gt;, &lt;code&gt;flush_lag&lt;/code&gt; and &lt;code&gt;replay_lag&lt;/code&gt; as intervals measured from WAL send time, which is the more precise view. Alert at &lt;strong&gt;30 seconds&lt;/strong&gt; for read replicas that serve traffic, longer for backup replicas. Lag that grows steadily is a replica that cannot keep up (usually single-threaded replay on a primary with many parallel writers); lag that spikes and recovers is usually a long transaction, a bulk load or a vacuum on the primary.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Active and waiting queries
&lt;/h2&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="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&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;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;wait_event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Lock'&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;waiting_on_locks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;query_start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="s1"&gt;'5 seconds'&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;slow&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_activity&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;backend_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'client backend'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rising &lt;code&gt;waiting_on_locks&lt;/code&gt; count is the earliest signal of a lock pile-up behind a migration. Alert when it exceeds a handful for more than a minute. &lt;code&gt;wait_event_type&lt;/code&gt; and &lt;code&gt;backend_type&lt;/code&gt; exist from PostgreSQL 10; on older servers drop the &lt;code&gt;backend_type&lt;/code&gt; filter. &lt;code&gt;pg_blocking_pids(pid)&lt;/code&gt; tells you which session is at the head of the queue, which is usually an &lt;code&gt;ALTER TABLE&lt;/code&gt; waiting for a long-running &lt;code&gt;SELECT&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running these checks without an exporter
&lt;/h2&gt;

&lt;p&gt;You can wrap the queries in a cron job that posts to a webhook, or run &lt;code&gt;postgres_exporter&lt;/code&gt; and Prometheus. Both work; both are one more thing to maintain.&lt;/p&gt;

&lt;p&gt;If a server already runs the Vigil agent, add a database entry to &lt;code&gt;/etc/vigil-agent.json&lt;/code&gt; and restart it. The agent calls &lt;code&gt;psql&lt;/code&gt; locally with the password passed through the environment, never on the command line, and reports the numbers above every interval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"databases"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgresql"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"dsn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgresql://vigil_ro:PASSWORD@127.0.0.1:5432/app"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a read-only role for it first. &lt;code&gt;pg_monitor&lt;/code&gt; (PostgreSQL 10+) grants exactly the statistics views the queries above need, including the unredacted &lt;code&gt;query&lt;/code&gt; column in &lt;code&gt;pg_stat_activity&lt;/code&gt;, without any table access:&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="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;vigil_ro&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="s1"&gt;'PASSWORD'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="n"&gt;pg_monitor&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;vigil_ro&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;vigil-server-agent --db-test&lt;/code&gt; prints what the collector sees so you can check the DSN before restarting. In the server's Databases tab you get connections, cache hit, transactions per second, replication lag, active and waiting queries, and the top statements when &lt;code&gt;pg_stat_statements&lt;/code&gt; is installed. Rules for connections percentage, cache hit ratio and replication lag open incidents automatically, with the thresholds above as defaults.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thresholds in one table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Warn&lt;/th&gt;
&lt;th&gt;Page&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Connections used&lt;/td&gt;
&lt;td&gt;75 %&lt;/td&gt;
&lt;td&gt;90 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache hit ratio (rolling)&lt;/td&gt;
&lt;td&gt;under 95 %&lt;/td&gt;
&lt;td&gt;under 90 %&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replication lag&lt;/td&gt;
&lt;td&gt;10 s&lt;/td&gt;
&lt;td&gt;30 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sessions waiting on locks&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;10 for 1 minute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Idle in transaction&lt;/td&gt;
&lt;td&gt;any older than 5 min&lt;/td&gt;
&lt;td&gt;any older than 15 min&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tune them to your workload after a week of data, and make sure the alert goes to a channel someone reads at night.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on the &lt;a href="https://puzaricloud.in/blog/monitor-postgresql-connections-and-replication-lag" rel="noopener noreferrer"&gt;PuzariCloud engineering blog&lt;/a&gt;. Drafted with AI assistance and reviewed, edited and tested by the author, who builds &lt;a href="https://puzaricloud.in" rel="noopener noreferrer"&gt;Vigil by PuzariCloud&lt;/a&gt;, the monitoring service the examples use.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>database</category>
      <category>alerting</category>
    </item>
  </channel>
</rss>
