{"id":1503,"date":"2026-11-13T18:00:00","date_gmt":"2026-11-13T18:00:00","guid":{"rendered":"https:\/\/wade.one\/blog\/?p=1503"},"modified":"2026-11-12T23:14:52","modified_gmt":"2026-11-12T23:14:52","slug":"optimistic-locking-stops-an-old-tab-overwriting-new-work","status":"publish","type":"post","link":"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/","title":{"rendered":"Optimistic Locking Stops an Old Tab Overwriting New Work"},"content":{"rendered":"<p>An old browser tab should not quietly overwrite somebody else&#8217;s work. If two people open the same record, the second person to save may be submitting a perfectly valid form based on information that is no longer current. Validation will happily accept it. The person whose changes disappear will be rather less happy.<\/p>\n<p>For a record edited as a whole, I like an explicit version number. The client reads version 7 and sends that version back with its changes. The database update checks the version and increments it in the same statement. Here is the basic shape in PostgreSQL, assuming an integer <code>version<\/code> column:<\/p>\n<pre><code>UPDATE projects\nSET name = $1,\n    version = version + 1\nWHERE id = $2\n  AND version = $3\nRETURNING id, name, version;<\/code><\/pre>\n<p>If another writer has already moved the record to version 8, this update changes no rows. PostgreSQL&#8217;s <a href=\"https:\/\/www.postgresql.org\/docs\/18\/transaction-iso.html#XACT-READ-COMMITTED\">Read Committed behaviour<\/a> also matters when the writes overlap: after waiting for a concurrent updater to commit, it checks the condition against the updated row. The check belongs inside the write. Reading the version, comparing it in application code and then running an unconditional update leaves a gap.<\/p>\n<p>That query is the concurrency part, not the entire endpoint. Keep the usual authorization checks and tenant boundary. A zero-row result needs handling according to the API&#8217;s rules, since a missing or inaccessible record can also produce no result. Every path that changes the protected record needs to maintain the version too, including background jobs and administrative tools.<\/p>\n<p>The interface has work to do when there is a conflict. Keep the person&#8217;s unsaved changes, explain that the record changed and let them compare or reload. Automatically fetching the latest version and resubmitting the old form defeats the protection. It turns a conflict into a silent overwrite with an extra request.<\/p>\n<p>This is useful where overwriting an edit would matter; a live counter may need a different operation entirely. I would test it with two clients reading the same version and then saving different values. One save should succeed, and the other should get a result the person can actually recover from.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Two people can save valid changes and still overwrite each other. A version check gives the application a chance to catch it.<\/p>\n","protected":false},"author":0,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_hide_featured_image":false,"_jetpack_featured_image_duplicate_dismissed":0,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2},"jetpack_post_was_ever_published":false},"categories":[13],"tags":[96,333,253],"class_list":["post-1503","post","type-post","status-publish","format-standard","hentry","category-programming","tag-concurrency","tag-optimistic-locking","tag-postgresql"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Optimistic Locking Stops an Old Tab Overwriting New Work - wade.one<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/\" \/>\n<meta property=\"og:locale\" content=\"en_GB\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Optimistic Locking Stops an Old Tab Overwriting New Work - wade.one\" \/>\n<meta property=\"og:description\" content=\"Two people can save valid changes and still overwrite each other. A version check gives the application a chance to catch it.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/\" \/>\n<meta property=\"og:site_name\" content=\"wade.one\" \/>\n<meta property=\"article:published_time\" content=\"2026-11-13T18:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/wade.one\/blog\/wp-content\/uploads\/2015\/02\/Wade-Logo-cropped.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1518\" \/>\n\t<meta property=\"og:image:height\" content=\"1506\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@wadewomersley\" \/>\n<meta name=\"twitter:site\" content=\"@wadewomersley\" \/>\n<meta name=\"twitter:label1\" content=\"Estimated reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"2 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/2026\\\/11\\\/13\\\/optimistic-locking-stops-an-old-tab-overwriting-new-work\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/2026\\\/11\\\/13\\\/optimistic-locking-stops-an-old-tab-overwriting-new-work\\\/\"},\"author\":{\"name\":\"\",\"@id\":\"\"},\"headline\":\"Optimistic Locking Stops an Old Tab Overwriting New Work\",\"datePublished\":\"2026-11-13T18:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/2026\\\/11\\\/13\\\/optimistic-locking-stops-an-old-tab-overwriting-new-work\\\/\"},\"wordCount\":340,\"publisher\":{\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/#\\\/schema\\\/person\\\/8b4739f8f8bb2cff5d792d4b8779fcc3\"},\"keywords\":[\"concurrency\",\"optimistic-locking\",\"postgresql\"],\"articleSection\":[\"Programming\"],\"inLanguage\":\"en-GB\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/2026\\\/11\\\/13\\\/optimistic-locking-stops-an-old-tab-overwriting-new-work\\\/\",\"url\":\"https:\\\/\\\/wade.one\\\/blog\\\/2026\\\/11\\\/13\\\/optimistic-locking-stops-an-old-tab-overwriting-new-work\\\/\",\"name\":\"Optimistic Locking Stops an Old Tab Overwriting New Work - wade.one\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/#website\"},\"datePublished\":\"2026-11-13T18:00:00+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/2026\\\/11\\\/13\\\/optimistic-locking-stops-an-old-tab-overwriting-new-work\\\/#breadcrumb\"},\"inLanguage\":\"en-GB\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/wade.one\\\/blog\\\/2026\\\/11\\\/13\\\/optimistic-locking-stops-an-old-tab-overwriting-new-work\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/2026\\\/11\\\/13\\\/optimistic-locking-stops-an-old-tab-overwriting-new-work\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/wade.one\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Optimistic Locking Stops an Old Tab Overwriting New Work\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/wade.one\\\/blog\\\/\",\"name\":\"wade.one\",\"description\":\"wade womersley - york based software engineer\",\"publisher\":{\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/#\\\/schema\\\/person\\\/8b4739f8f8bb2cff5d792d4b8779fcc3\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/wade.one\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-GB\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/#\\\/schema\\\/person\\\/8b4739f8f8bb2cff5d792d4b8779fcc3\",\"name\":\"Wade Womersley\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-GB\",\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/wp-content\\\/uploads\\\/2015\\\/02\\\/200px.png\",\"url\":\"https:\\\/\\\/wade.one\\\/blog\\\/wp-content\\\/uploads\\\/2015\\\/02\\\/200px.png\",\"contentUrl\":\"https:\\\/\\\/wade.one\\\/blog\\\/wp-content\\\/uploads\\\/2015\\\/02\\\/200px.png\",\"width\":202,\"height\":200,\"caption\":\"Wade Womersley\"},\"logo\":{\"@id\":\"https:\\\/\\\/wade.one\\\/blog\\\/wp-content\\\/uploads\\\/2015\\\/02\\\/200px.png\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Optimistic Locking Stops an Old Tab Overwriting New Work - wade.one","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/","og_locale":"en_GB","og_type":"article","og_title":"Optimistic Locking Stops an Old Tab Overwriting New Work - wade.one","og_description":"Two people can save valid changes and still overwrite each other. A version check gives the application a chance to catch it.","og_url":"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/","og_site_name":"wade.one","article_published_time":"2026-11-13T18:00:00+00:00","og_image":[{"width":1518,"height":1506,"url":"https:\/\/wade.one\/blog\/wp-content\/uploads\/2015\/02\/Wade-Logo-cropped.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_creator":"@wadewomersley","twitter_site":"@wadewomersley","twitter_misc":{"Estimated reading time":"2 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/#article","isPartOf":{"@id":"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/"},"author":{"name":"","@id":""},"headline":"Optimistic Locking Stops an Old Tab Overwriting New Work","datePublished":"2026-11-13T18:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/"},"wordCount":340,"publisher":{"@id":"https:\/\/wade.one\/blog\/#\/schema\/person\/8b4739f8f8bb2cff5d792d4b8779fcc3"},"keywords":["concurrency","optimistic-locking","postgresql"],"articleSection":["Programming"],"inLanguage":"en-GB"},{"@type":"WebPage","@id":"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/","url":"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/","name":"Optimistic Locking Stops an Old Tab Overwriting New Work - wade.one","isPartOf":{"@id":"https:\/\/wade.one\/blog\/#website"},"datePublished":"2026-11-13T18:00:00+00:00","breadcrumb":{"@id":"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/#breadcrumb"},"inLanguage":"en-GB","potentialAction":[{"@type":"ReadAction","target":["https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/wade.one\/blog\/2026\/11\/13\/optimistic-locking-stops-an-old-tab-overwriting-new-work\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/wade.one\/blog\/"},{"@type":"ListItem","position":2,"name":"Optimistic Locking Stops an Old Tab Overwriting New Work"}]},{"@type":"WebSite","@id":"https:\/\/wade.one\/blog\/#website","url":"https:\/\/wade.one\/blog\/","name":"wade.one","description":"wade womersley - york based software engineer","publisher":{"@id":"https:\/\/wade.one\/blog\/#\/schema\/person\/8b4739f8f8bb2cff5d792d4b8779fcc3"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/wade.one\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-GB"},{"@type":["Person","Organization"],"@id":"https:\/\/wade.one\/blog\/#\/schema\/person\/8b4739f8f8bb2cff5d792d4b8779fcc3","name":"Wade Womersley","image":{"@type":"ImageObject","inLanguage":"en-GB","@id":"https:\/\/wade.one\/blog\/wp-content\/uploads\/2015\/02\/200px.png","url":"https:\/\/wade.one\/blog\/wp-content\/uploads\/2015\/02\/200px.png","contentUrl":"https:\/\/wade.one\/blog\/wp-content\/uploads\/2015\/02\/200px.png","width":202,"height":200,"caption":"Wade Womersley"},"logo":{"@id":"https:\/\/wade.one\/blog\/wp-content\/uploads\/2015\/02\/200px.png"}}]}},"jetpack_publicize_connections":[],"jetpack_sharing_enabled":true,"jetpack-related-posts":[{"id":1431,"url":"https:\/\/wade.one\/blog\/2026\/10\/12\/postgresql-14-support-ends-in-november-plan-the-upgrade-now\/","url_meta":{"origin":1503,"position":0},"title":"PostgreSQL 14 Support Ends in November: Plan the Upgrade Now","author":"","date":"October 12, 2026","format":false,"excerpt":"PostgreSQL 14 reaches the end of community support on 12 November 2026.","rel":"","context":"In &quot;Programming&quot;","block_context":{"text":"Programming","link":"https:\/\/wade.one\/blog\/category\/programming\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1434,"url":"https:\/\/wade.one\/blog\/2026\/10\/15\/read-the-postgresql-query-plan-before-adding-another-index\/","url_meta":{"origin":1503,"position":1},"title":"Read the PostgreSQL Query Plan Before Adding Another Index","author":"","date":"October 15, 2026","format":false,"excerpt":"When a database query gets slow, adding an index is an appealingly concrete thing to do.","rel":"","context":"In &quot;Programming&quot;","block_context":{"text":"Programming","link":"https:\/\/wade.one\/blog\/category\/programming\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1184,"url":"https:\/\/wade.one\/blog\/2026\/04\/16\/how-i-decide-when-an-api-needs-versioning\/","url_meta":{"origin":1503,"position":2},"title":"How I Decide When an API Needs Versioning","author":"","date":"April 16, 2026","format":false,"excerpt":"API versioning is useful when the contract has really changed, not when a team wants a convenient place to hide messy changes. I usually want the compatibility story to be explicit before I reach for a new version.","rel":"","context":"In &quot;Programming&quot;","block_context":{"text":"Programming","link":"https:\/\/wade.one\/blog\/category\/programming\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":362,"url":"https:\/\/wade.one\/blog\/2009\/12\/10\/super-mario-bros-wii-a-must-play-game\/","url_meta":{"origin":1503,"position":3},"title":"Super Mario Bros. Wii &#8211; A must play game","author":"Wade","date":"December 10, 2009","format":false,"excerpt":"At the weekend I finally caved and bought Super Mario Bro's Wii, I loved the older Super Mario on the SNES and the Wii version seemed to go back to Mario roots with this new game - at least, the advertisements made it look like it did so I would\u2026","rel":"","context":"In &quot;Gaming&quot;","block_context":{"text":"Gaming","link":"https:\/\/wade.one\/blog\/category\/gaming\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1472,"url":"https:\/\/wade.one\/blog\/2026\/10\/29\/a-database-constraint-can-close-the-gap-between-checking-and-writing\/","url_meta":{"origin":1503,"position":4},"title":"A Database Constraint Can Close the Gap Between Checking and Writing","author":"Wade","date":"October 29, 2026","format":false,"excerpt":"Checking whether a record exists before inserting another one does not guarantee uniqueness.","rel":"","context":"In &quot;Programming&quot;","block_context":{"text":"Programming","link":"https:\/\/wade.one\/blog\/category\/programming\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":1440,"url":"https:\/\/wade.one\/blog\/2026\/10\/21\/hashed-asset-names-make-cdn-releases-easier-to-reason-about\/","url_meta":{"origin":1503,"position":5},"title":"Hashed Asset Names Make CDN Releases Easier to Reason About","author":"","date":"October 21, 2026","format":false,"excerpt":"Replacing app.js and hoping every cache forgets the old version is a fairly unpleasant deployment strategy.","rel":"","context":"In &quot;Programming&quot;","block_context":{"text":"Programming","link":"https:\/\/wade.one\/blog\/category\/programming\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]}],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/wade.one\/blog\/wp-json\/wp\/v2\/posts\/1503","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wade.one\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/wade.one\/blog\/wp-json\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/wade.one\/blog\/wp-json\/wp\/v2\/comments?post=1503"}],"version-history":[{"count":2,"href":"https:\/\/wade.one\/blog\/wp-json\/wp\/v2\/posts\/1503\/revisions"}],"predecessor-version":[{"id":1520,"href":"https:\/\/wade.one\/blog\/wp-json\/wp\/v2\/posts\/1503\/revisions\/1520"}],"wp:attachment":[{"href":"https:\/\/wade.one\/blog\/wp-json\/wp\/v2\/media?parent=1503"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/wade.one\/blog\/wp-json\/wp\/v2\/categories?post=1503"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/wade.one\/blog\/wp-json\/wp\/v2\/tags?post=1503"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}