惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
Netflix TechBlog - Medium
大猫的无限游戏
大猫的无限游戏
爱范儿
爱范儿
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Register - Security
The Register - Security
IT之家
IT之家
博客园_首页
Microsoft Security Blog
Microsoft Security Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
I
InfoQ
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Jina AI
Jina AI
Apple Machine Learning Research
Apple Machine Learning Research
M
MIT News - Artificial intelligence
博客园 - Franky
C
Check Point Blog
T
The Blog of Author Tim Ferriss
V
Visual Studio Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
T
Tailwind CSS Blog
Recent Announcements
Recent Announcements
云风的 BLOG
云风的 BLOG
美团技术团队
The Cloudflare Blog
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
MyScale Blog
MyScale Blog
The GitHub Blog
The GitHub Blog
D
DataBreaches.Net
Google DeepMind News
Google DeepMind News
V
V2EX
aimingoo的专栏
aimingoo的专栏
GbyAI
GbyAI
G
Google Developers Blog
S
SegmentFault 最新的问题
Hugging Face - Blog
Hugging Face - Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
U
Unit 42
罗磊的独立博客
量子位
MongoDB | Blog
MongoDB | Blog
Last Week in AI
Last Week in AI
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件
D
Docker
人人都是产品经理
人人都是产品经理

Datadog | The Monitor blog

Introducing our open source AI-native SAST Instrument and monitor Boomi integration flows with OpenTelemetry and Datadog Not all index scans are equal: How we cut query latency by over 99% Platform engineering metrics: What to measure and what to ignore Integrate Recorded Future threat intelligence with Datadog Cloud SIEM CI/CD security: threat modeling using a MITRE-style threat matrix CI/CD security: How to secure your GitHub ecosystem Ingress NGINX is EOL: A practical guide for migrating to Kubernetes Gateway API Operating agentic AI with Amazon Bedrock AgentCore and Datadog LLM Observability: Lessons from NTT DATA Introducing the Datadog Code Security MCP Capture and analyze custom heatmaps in Session Replay Understand session replays faster with AI summaries and smart chapters Monitor ClickHouse query performance with Datadog Database Monitoring How we designed empathetic alert sounds for on-call engineers Search and act across Datadog to resolve issues faster with Bits Assistant Measure the business impact of every product change with Datadog Experiments Analyzing round trip query latency Configuring JavaScript caches for better performance Introducing Bits AI Dev Agent for Code Security Datadog achieves ISO 42001 certification for responsible AI Monitor Nutanix clusters, hosts, and VMs with Datadog Monitor Juniper Mist in Datadog A new Host Map for modern infrastructure Annotate traces to improve LLM quality with Datadog LLM Observability What’s new in Cloud SIEM: AI-powered investigations, enhanced threat intelligence, and scalable security operations Explore Kubernetes with native OpenTelemetry data Monitor Oracle Fusion Cloud Applications with Datadog Announcing the Datadog Terraform provider v4.0.0 Scaling Kubernetes workloads on custom metrics How to design cloud environments for AI-powered threat analysis Monitor Aruba Central in Datadog How we centralize and remediate risks with Datadog Case Management Accelerate incident response with Datadog and ServiceNow Monitor your application and network load balancer logs Understanding Karpenter architecture for Kubernetes autoscaling Tools for collecting metrics and logs from Karpenter Monitor Karpenter with Datadog What your product data is actually saying Key metrics for monitoring Karpenter Securing Datadog’s platform in the AI age: The role of observability data Four ways engineering teams use the Datadog MCP Server to power AI agents Approaching your observability migration with the right mindset Meet the new Bits AI SRE: Deeper reasoning, twice as fast Key learnings from the 2026 State of DevSecOps study Use plain English to query your multi-cloud infrastructure in Resource Catalog Simplifying troubleshooting across the user journey with Datadog Synthetic Monitoring Protect your OCI resources with Datadog Cloud Security This Month in Datadog - February 2026 Amazon EC2 security: How misconfigured and public AMIs expand your cloud attack surface Enable end-to-end visibility into your Java apps with a single command Measure and improve mobile app startup performance with Datadog RUM Evaluating our AI Guard application to improve quality and control cost Identify untested code across every level of your codebase Make use of guardrail metrics and stop babysitting your releases Monitor Versa Networks SD-WAN performance in Datadog Improve performance and reliability with APM Recommendations Remediate transitive vulnerabilities faster with Datadog Software Composition Analysis Generate audit-ready vulnerability and compliance reports with Datadog Sheets Monitor Fortinet FortiManager performance in Datadog Improve test coverage across codebases with Datadog Code Coverage Move fast, don’t break things: Consistent testing standards at scale Enrich logs with ServiceNow CMDB context before routing to any SIEM or logging tool Monitor Lustre with Datadog Make faster, better product decisions with Datadog Product Analytics Surface and remediate runtime posture issues with Workload Protection Findings Protect agentic AI applications with Datadog AI Guard How to optimize JavaScript code with CSS Trace Google Pub/Sub workloads in Cloud Run with Datadog Detect human names in logs with ML in Sensitive Data Scanner How we cut our NLQ agent debugging time from hours to minutes with LLM Observability Debug PostgreSQL query latency faster with EXPLAIN ANALYZE in Datadog Database Monitoring Datadog acquires Propolis Unify and correlate frontend and backend data with retention filters Scale compliance across global frameworks with Datadog Cloud Security Monitor Arista VeloCloud SD-WAN performance with Datadog Building reliable dashboard agents with Datadog LLM Observability Simplify log collection and aggregation for MSSPs with Datadog Observability Pipelines Mitigation for Node.js denial-of-service vulnerability affecting Datadog APM Automate flaky test fixes with the Bits AI Dev Agent and Test Optimization How we built an AI SRE agent that investigates like a team of engineers Datadog integrations 2025 recap: Observability for AI, security, and hybrid cloud Design effective executive dashboards with Datadog Implement dbt data quality checks with dbt-expectations Bring faster visibility into AWS Lambda functions with remote instrumentation Troubleshoot faster with the GitLab Source Code integration in Datadog How Cambia Health Solutions saved $30,000 monthly with Cloud Cost Management and the Datadog Resource Catalog Normalize any logs for Cloud SIEM with Datadog's OCSF processor Optimizing Datadog at scale: Cost-efficient observability at Zendesk Detect, diagnose, and resolve network issues easily with CNM Network Health Connect engineering errors to user impact in early-stage products Cilium configuration for Kubernetes operations at scale Designing feedback loops for progressive delivery Ship features faster and safer with Datadog Feature Flags Choosing the right OpenTelemetry Collector distribution Route your monitor alerts with Datadog monitor notification rules Automate Cloud SIEM investigations with Bits AI Security Analyst Cloud threat detection: How to identify risky activity across control and data planes Collecting Kafka performance metrics Monitoring Kafka with Datadog Monitoring Kafka performance metrics
When upserts don’t update but still write: Debugging Postgres performance at scale
Anthonin Bonnefoy · 2026-03-23 · via Datadog | The Monitor blog

At Datadog, we track the life cycle of millions of ephemeral hosts that report telemetry data to our platform. When a host stops emitting data, we eventually need to clean it up to avoid bloating our metadata store.

To detect inactive hosts, the Datadog team that manages the host metadata store introduced a new upsert to track the last time a host was seen. We expected this new query to have minimal impact. Each host would be updated at most once a day, so even at 25,000 upserts per second, most queries should have been no-ops.

But when we rolled out the new query, disk writes doubled and Write-Ahead Logging (WAL) syncs quadrupled. We discovered that even when an upsert doesn’t change any values, it still locks the conflicting row, which is recorded in the WAL. Given that a Postgres cluster can only have a single writer, there’s a hard limit to how many writes it can handle. The increase in disk writes introduced by the new query was consuming too much of this limited budget and had to be fixed.

In this post, we’ll walk through how we diagnosed the unexpected overhead by inspecting Postgres’s WAL and how we rewrote the query to eliminate the cost without sacrificing correctness.

Designing a low-overhead upsert in Postgres

When the Datadog Agent runs, it collects host metadata—such as host_id, availability zone, and tags—and sends it to Datadog, where we store it in a Postgres database. Because hosts have a limited lifetime, they eventually terminate and stop emitting metrics. At that point, we can safely delete them from the database to keep it from growing indefinitely.

However, we don’t have a specific signal when a host has been terminated, other than the fact that it has stopped sending data. We decided that if a host hasn’t emitted metrics for 7 days, we can safely delete it. To support that, we needed a way to track the last time a host was seen. Updating a row every time we see a host is prohibitively expensive: In our largest data centers, this means more than 25,000 updates per second.

We needed to design our table and queries to avoid overloading our database, with three constraints in mind:

  • Reduce update cost. Postgres uses MultiVersion Concurrency Control (MVCC), so every update creates a new row version. Adding a last_ingested column to the existing host table would copy all host metadata on each update. Moving this field to a dedicated table will reduce the amount of data written per update.

  • Minimize write amplification by relying on Heap-Only Tuples (HOT) updates. By default, updates add entries to all indexes on the table. If an update does not modify indexed columns and there is enough free space on the page, Postgres can apply a HOT update, avoiding index writes and making updates significantly cheaper.

  • Accept coarse-grained freshness. We only need to know whether a host has emitted data within the past 7 days, so 1-day granularity is sufficient.

With those constraints, we created the following table:

CREATE TABLE host_last_ingested (

host_id BIGINT PRIMARY KEY,

last_ingested TIMESTAMP WITH TIME ZONE NOT NULL default now()

) WITH (fillfactor=80);

We didn’t create an index on the last_ingested column so that updates only modify unindexed columns, allowing Postgres to apply HOT updates. We also set the table’s fillfactor to 80%. This means inserts will only fill 80% of an 8 kB page, leaving free space on each page so that subsequent updates can stay on the same page and qualify for HOT updates.

For the query, we used this upsert:

INSERT INTO host_last_ingested as t

VALUES($1, now())

ON CONFLICT (host_id)

DO UPDATE

SET last_ingested = EXCLUDED.last_ingested

WHERE t.last_ingested < EXCLUDED.last_ingested - '1 day'::interval;

This is a standard ON CONFLICT DO UPDATE upsert. If the host doesn’t exist, we insert it. Otherwise, we run the update. The WHERE condition limits updates to a host to once a day.

If we run the query twice on a nonexistent host:

<query>

INSERT 0 1

<query>

INSERT 0 0

The INSERT command outputs the number of processed_rows in the second integer (the first one is always 0). The first query inserts 1 row, and the second reports 0 processed rows because the WHERE condition prevents the update from being applied.

With this query, we expected that most upserts would behave like no-ops. But that assumption turned out to be false.

What we saw when we rolled out the upsert: Writes without updates

The new upsert was gradually rolled out on our smaller data centers until we reached about 500 upserts per second:

Query rate for upserts reaching about 500 queries/s after rollout.

We saw an initial spike in insertions and no updated rows, which matched our expectation that most upserts would be no-ops:

Postgres rows inserted vs. updated showing insert spike and no change in update rate.

However, the number of write IOPS more than doubled, even though the update rate remained flat:

IOPS chart showing increase in write operations per second after rollout.

Thankfully, it was a small data center and 500 IOPS was still within manageable limits. More importantly, WAL syncs increased by roughly the same amount:

WAL sync chart showing significant increase in syncs per second.

In Postgres, all changes—such as inserting rows or modifying indexes—are recorded in the WAL. When a transaction commits, the WAL is flushed to disk to ensure durability, while table and index pages are flushed later by the background writer or during checkpoints. If a crash occurs, Postgres can replay changes from the WAL to restore consistency. The WAL sync metric reports the number of times these logs are flushed to disk.

Since the only change we’d introduced was the new query, it was likely the source of the WAL syncs, even though the rows weren’t actually being updated. To understand why, we needed to look deeper into what the upsert was writing to the WAL.

Digging into Postgres WAL to explain the IOPS spike

Starting in Postgres 15, the pg_walinspect extension provides debugging functions to inspect the WAL. Since our metrics were pointing squarely at WAL activity, this was the most direct way to see what the upsert was actually doing. We installed the extension with:

CREATE EXTENSION pg_walinspect;

Once installed, the extension exposes the pg_get_wal_records_info function, which returns all WAL records between two log sequence numbers (LSNs). To make the extension easier to work with, we added the following shortcuts to our ~/.psqlrc:

\set wal_lsn_start 'SELECT pg_current_wal_lsn() \\gset'

\set wal_records 'SELECT * FROM pg_get_wal_records_info(:''pg_current_wal_lsn'', ''FFFFFFFF/FFFFFFFF'');'

  • :wal_lsn_start saves the current LSN to the pg_current_wal_lsn variable.

  • :wal_records displays all WAL records between the saved LSN and the WAL end, using FFFFFFFF/FFFFFFFF as the upper bound (the maximum valid LSN value).

With those shortcuts available, we could reproduce the upsert and check whether it was generating WAL records:

:wal_lsn_start

INSERT INTO host_last_ingested as t

VALUES(1, now())

ON CONFLICT (host_id)

DO UPDATE

SET last_ingested = EXCLUDED.last_ingested

WHERE t.last_ingested < EXCLUDED.last_ingested - '1 day'::interval;

INSERT 0 0

:wal_records

The query didn’t update any rows, but pg_walinspect still showed WAL activity:

-[ RECORD 1 ]----+-----------------------------------------------------------------

start_lsn | 0/02469D30

end_lsn | 0/02469D68

prev_lsn | 0/02469CF8

xid | 818

resource_manager | Heap

record_type | LOCK

record_length | 54

main_data_length | 8

fpi_length | 0

description | xmax: 818, off: 2, infobits: [LOCK_ONLY, EXCL_LOCK], flags: 0x00

block_ref | blkref #0: rel 1663/5/16426 fork main blk 0

-[ RECORD 2 ]----+-----------------------------------------------------------------

start_lsn | 0/02469D68

end_lsn | 0/02469D98

prev_lsn | 0/02469D30

xid | 818

resource_manager | Transaction

record_type | COMMIT

record_length | 46

main_data_length | 20

fpi_length | 0

description | 2025-10-16 15:33:10.406748+00

block_ref | <null>

Two WAL records were written:

  • A Heap LOCK record indicates a row lock on the relation 16426, which happens to be our new host_last_ingested table.

  • A COMMIT record is written because a transaction ID (xid) was assigned to the transaction, and it must either be committed or rolled back.

To confirm what was generating the LOCK record, we attached a debugger to the Postgres backend process (lldb -p $backend_pid) and added a breakpoint on WALInsertLockAcquire. The backtrace (abridged) showed:

#0: postgres`WALInsertLockAcquire at xlog.c:1391:6

#1: postgres`XLogInsertRecord(...) at xlog.c:823:3

#2: postgres`XLogInsert(...) at xloginsert.c:524:12

#3: postgres`heap_lock_tuple(...) at heapam.c:5244:12

#4: postgres`heapam_tuple_lock(...) at heapam_handler.c:380:11

#5: postgres`table_tuple_lock(...) at tableam.h:1554:9

#6: postgres`ExecOnConflictUpdate(...) at nodeModifyTable.c:2749:9

#7: postgres`ExecInsert(...) at nodeModifyTable.c:1152:10

Looking at the code around the backtrace, we could trace what happens:

  • The upsert checks for the existence of the tuple.

  • The tuple exists, so ExecOnConflictUpdate runs and locks the row.

  • Locking the row modifies its metadata to include the locking transaction ID, assigning a transaction ID (xid).

  • A LOCK record is written to the WAL.

  • The WHERE condition evaluates to false, so the row is not updated.

  • Because a transaction ID was assigned, the transaction must be committed

  • A COMMIT record is written, and the WAL is flushed to disk.

Postgres’s INSERT documentation explicitly confirms this lock behavior for ON CONFLICT DO UPDATE:

Only rows for which this expression returns true will be updated, although all rows will be locked when the ON CONFLICT DO UPDATE action is taken. Note that condition is evaluated last, after a conflict has been identified as a candidate to update.

This explains the surge in IOPS and WAL syncs we saw. Each upsert triggers a WAL sync, and each sync writes an 8 kB page to disk. With 500 upserts per second, we had 500 WAL syncs per second using 500 IOPS.

There’s only a limited amount of sync a disk can handle. We used pg_test_fsync to estimate how many WAL syncs per second the hardware could sustain. On a gp3 EBS disk, we reach around 1,000 syncs per second using 8 kB writes. Once that limit is reached, Postgres starts batching commits, which introduces latency.

This particular cluster handled 500 upserts per second just fine. But our biggest clusters will add 25,000 upserts per second. That would overwhelm the sync capacity for a query that was expected to behave like a no-op.

We needed to change our approach to avoid this overhead.

Rewriting the query to avoid upsert overhead

We couldn’t rely on ON CONFLICT DO UPDATE, but we still needed to track each host’s last ingested time. The application also has strict latency requirements, so everything needs to happen in a single query. To do that, we emulated an upsert by using a data-modifying Common Table Expression (CTE).

Here’s how it works:

  • The CTE attempts to insert the row using ON CONFLICT DO NOTHING, which avoids locking existing rows.

  • If the insertion succeeds, the CTE returns the inserted row.

  • If no insertion occurs, the CTE returns nothing.

  • The outer UPDATE uses the presence (or absence) of a returned row to decide whether to run.

WITH insert_attempt AS (

-- Try to insert

INSERT INTO host_last_ingested (host_id)

-- Values to insert

VALUES (:host_id)

-- Only try to insert if row doesn't already exist

ON CONFLICT DO NOTHING

RETURNING *

)

-- Do the update

UPDATE host_last_ingested

SET last_ingested=now() WHERE host_id=:host_id

-- Only modify if last_ingested is older than 1 day

AND last_ingested < now() - '1 day'::interval

-- And only if no insertion was done

AND NOT EXISTS (SELECT * FROM insert_attempt);

Compared to ON CONFLICT DO UPDATE, this version introduces potential race conditions. For example:

  • Tx1: The insert attempt fails because the row already exists.

  • Tx2: Deletes the row.

  • Tx1: Attempts to update the row that was deleted.

This happens because we’re not locking the row. This trade-off is acceptable for our use case: Host tracking can be imprecise, and the likelihood of seeing a host again after 7 days of inactivity is low.

We then checked the WAL records generated by this query to confirm that it behaved as expected.

WAL record behavior

If the row doesn’t exist, we see the following records:

xidresource_managerrecord_type
798HeapINSERT+INIT
798BtreeNEWROOT
798BtreeINSERT_LEAF
798HeapHEAP_CONFIRM
798TransactionCOMMIT

Those WAL records add a new row to the table (Heap INSERT+INIT) and a new entry in the index (Btree NEWROOT + INSERT_LEAF). Because the insert uses ON CONFLICT DO NOTHING, it is performed speculatively and needs to be confirmed via the HEAP_CONFIRM WAL record.

If the row already exists and last_ingested is older than 1 day, we see the following records:

xidresource_managerrecord_type
800HeapHOT_UPDATE
800TransactionCOMMIT

In this case, the update qualifies as a HOT update and does not require any index changes.

And if the row exists and last_ingested age is less than 1 day, no WAL records are generated.

After confirming that the new query avoided unnecessary WAL records, we rolled it out again to our small data center:

CTE-based upsert query rollout with 428 queries/s.

We saw the expected spike in update queries during the rollout:

Postgres rows updated chart showing 37.68 rows/s during rollout.

This time, the increase in WAL syncs closely matched the actual row updates. When no rows were modified, the WAL sync rate remained steady:

WAL sync rate stays flat during periods with no updates.

Lessons learned: WAL and query optimization

By using pg_walinspect, we were able to pinpoint the source of our unexpected overhead: During conflict resolution, an upsert locks the row, even if it doesn’t update anything. That lock modifies the row’s metadata with a transaction ID. Once a transaction ID is allocated, the transaction must be committed—even if no row was updated—triggering a WAL sync.

Thanks to this information, we:

  • Rewrote the query to avoid the implicit row lock

  • Validated the new behavior by inspecting the resulting WAL records

  • Successfully removed the unnecessary overhead while preserving correctness

Postgres’s WAL tooling has broader applications, too. For example, pg_get_wal_stats provides insight into the types and sizes of WAL records, which can help quantify the cost of index maintenance or the impact of full page images.

When you’re operating databases at scale, even “no-ops” queries can turn into performance surprises. Tools like pg_walinspect help make those surprises visible and fixable.

If you enjoy digging into systems like this to uncover hidden performance costs and building scalable solutions to fix them, we’re hiring! Explore open roles on our engineering team.