<?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: Uptime Architect</title>
    <description>The latest articles on DEV Community by Uptime Architect (@uptimearchitect).</description>
    <link>https://dev.to/uptimearchitect</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%2F3973084%2Fdc838b73-4aab-46b5-8272-3b9ced38ce77.png</url>
      <title>DEV Community: Uptime Architect</title>
      <link>https://dev.to/uptimearchitect</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/uptimearchitect"/>
    <language>en</language>
    <item>
      <title>Oracle Unified Auditing Without the Noise</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Sun, 09 Aug 2026 21:15:15 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/oracle-unified-auditing-without-the-noise-2k3l</link>
      <guid>https://dev.to/uptimearchitect/oracle-unified-auditing-without-the-noise-2k3l</guid>
      <description>&lt;p&gt;Most Oracle audit trails are useless, and they fail in one of two ways. Either nobody ever enabled anything, so when a breach investigation asks "who altered that account," the answer is a shrug. Or someone once turned on &lt;em&gt;everything&lt;/em&gt; — every statement, every user, every object — and the trail became a firehose that filled &lt;code&gt;SYSAUX&lt;/code&gt;, slowed the database, and got quietly ignored until it was disabled again. Neither of those is auditing. One is blindness; the other is noise you can't read.&lt;/p&gt;

&lt;p&gt;The skill isn't auditing &lt;em&gt;more&lt;/em&gt;. It's auditing the &lt;strong&gt;few things that actually matter&lt;/strong&gt; — privilege abuse, credential attacks, changes to your security configuration — in a trail you can trust and actually read. Oracle's &lt;strong&gt;Unified Auditing&lt;/strong&gt; makes that easy, and there's a deadline attached: as of &lt;strong&gt;Oracle Database 23ai, traditional auditing is desupported&lt;/strong&gt;. If your process still leans on &lt;code&gt;AUDIT ...&lt;/code&gt; writing to &lt;code&gt;AUD$&lt;/code&gt;, it has an expiry date. Here's how to do auditing right, and what changes underneath you as you move to 23ai.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version.&lt;/strong&gt; &lt;strong&gt;Unified Auditing&lt;/strong&gt; (the model since 12c) writes every audit record to a single &lt;strong&gt;protected, read-only internal table&lt;/strong&gt; owned by &lt;code&gt;AUDSYS&lt;/code&gt;, and you turn it on through &lt;strong&gt;policies&lt;/strong&gt;, not statement by statement. Keep the baseline Oracle pre-enables — &lt;strong&gt;&lt;code&gt;ORA_SECURECONFIG&lt;/code&gt;&lt;/strong&gt; (privileged and structural changes) and &lt;strong&gt;&lt;code&gt;ORA_LOGON_FAILURES&lt;/code&gt;&lt;/strong&gt; — then add a &lt;em&gt;few&lt;/em&gt; narrow policies of your own, not a firehose. 19c ships in &lt;strong&gt;mixed mode&lt;/strong&gt; (traditional &lt;code&gt;AUD$&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; unified both active); move to &lt;strong&gt;pure&lt;/strong&gt; unified auditing by relinking the binary (&lt;code&gt;uniaud_on&lt;/code&gt;) and restarting. In &lt;strong&gt;23ai, traditional auditing is gone&lt;/strong&gt; — unified is all there is. Read the trail in &lt;strong&gt;&lt;code&gt;UNIFIED_AUDIT_TRAIL&lt;/code&gt;&lt;/strong&gt;; keep it from growing forever with &lt;strong&gt;&lt;code&gt;DBMS_AUDIT_MGMT&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Unified Auditing actually changed
&lt;/h2&gt;

&lt;p&gt;Before 12c, auditing was a scatter of destinations — &lt;code&gt;AUD$&lt;/code&gt; in the database, &lt;code&gt;FGA_LOG$&lt;/code&gt; for fine-grained policies, OS files, XML files, the &lt;code&gt;SYS&lt;/code&gt; operations audit — each configured differently and each with its own gaps. Unified Auditing collapses all of it into &lt;strong&gt;one trail&lt;/strong&gt; and one way to configure it. Three properties make it worth adopting on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A single, protected trail.&lt;/strong&gt; Every record lands in a read-only internal table in the &lt;strong&gt;&lt;code&gt;AUDSYS&lt;/code&gt;&lt;/strong&gt; schema. You cannot &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, or &lt;code&gt;DELETE&lt;/code&gt; it with SQL — not even as &lt;code&gt;SYS&lt;/code&gt;. An attacker who compromises a privileged account can't quietly scrub their tracks the way they could when audit rows lived in a normal table. You read it through the &lt;strong&gt;&lt;code&gt;UNIFIED_AUDIT_TRAIL&lt;/code&gt;&lt;/strong&gt; view.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Policy-based, not statement-by-statement.&lt;/strong&gt; You define &lt;strong&gt;audit policies&lt;/strong&gt; — named bundles of actions, privileges, or roles to watch — and enable them, optionally scoped to specific users. It's declarative and far easier to review than a pile of individual &lt;code&gt;AUDIT&lt;/code&gt; statements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Better performance, by default.&lt;/strong&gt; Unified records are written in &lt;strong&gt;queued (asynchronous) mode&lt;/strong&gt;: they buffer in the SGA and flush to disk in batches, rather than forcing a write on every audited action. (That buffering has one practical consequence for the impatient — see "reading the trail" below.)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The baseline is already on — don't turn it off
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprises people: a modern Oracle database is &lt;strong&gt;already auditing the right security-critical actions&lt;/strong&gt;, because Oracle pre-enables a sensible baseline. Two policies do most of the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ORA_SECURECONFIG&lt;/code&gt;&lt;/strong&gt; — audits the actions that change your security posture: &lt;code&gt;CREATE&lt;/code&gt;/&lt;code&gt;ALTER&lt;/code&gt;/&lt;code&gt;DROP USER&lt;/code&gt;, &lt;code&gt;GRANT&lt;/code&gt;, &lt;code&gt;REVOKE&lt;/code&gt;, &lt;code&gt;ALTER SYSTEM&lt;/code&gt;, &lt;code&gt;CREATE&lt;/code&gt;/&lt;code&gt;DROP&lt;/code&gt; of directories and database links, and more. This is enabled by default from 12.2 onward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ORA_LOGON_FAILURES&lt;/code&gt;&lt;/strong&gt; — audits failed logons, which is exactly the fingerprint of a credential attack or a misconfigured application hammering the listener.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check what's actually enabled right now — the first query every audit review should start with:&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;policy_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enabled_option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_name&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;audit_unified_enabled_policies&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;policy_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;ORA_SECURECONFIG&lt;/code&gt; isn't in that list, someone disabled it, and &lt;em&gt;that&lt;/em&gt; is your finding — a database that isn't recording who changes its own security is a database you can't investigate. The baseline is the floor, not the ceiling, but it is a floor most estates already have for free. Don't rip it out in the name of "reducing overhead"; the overhead is negligible and the coverage is exactly the high-value set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit the few things that are specific to you
&lt;/h2&gt;

&lt;p&gt;On top of the baseline, add narrow policies for the things that matter in &lt;em&gt;your&lt;/em&gt; database — a sensitive table, a powerful privilege, a specific service account. This is where discipline pays: every policy you add is a policy someone has to read, so add few and make them sharp.&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="c1"&gt;-- Watch reads of one sensitive table (and who did them)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;AUDIT&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;aud_salary_access&lt;/span&gt;
  &lt;span class="n"&gt;ACTIONS&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;hr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;employee_salary&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;AUDIT&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;aud_salary_access&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Watch any use of a skeleton-key system privilege&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;AUDIT&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;aud_any_table&lt;/span&gt;
  &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;ANY&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;AUDIT&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;aud_any_table&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Policies can be scoped and conditioned so they capture signal, not volume: &lt;code&gt;AUDIT POLICY aud_x BY app_user&lt;/code&gt; limits it to one account, &lt;code&gt;... WHENEVER NOT SUCCESSFUL&lt;/code&gt; records only the &lt;em&gt;failed&lt;/em&gt; attempts (often the interesting ones), and an &lt;code&gt;audit_condition&lt;/code&gt; on a policy lets you fire only when a column or context matches. The instinct to "audit all DML on everything" is the firehose again — resist it. A handful of targeted policies plus the baseline is a trail a human will actually read during an incident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mixed mode, pure mode, and the 23ai cliff
&lt;/h2&gt;

&lt;p&gt;Whether unified auditing is the &lt;em&gt;only&lt;/em&gt; audit system running depends on your release, and this trips people up during upgrades.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;12c–19c ship in &lt;em&gt;mixed mode&lt;/em&gt;.&lt;/strong&gt; Both the old traditional auditing (&lt;code&gt;AUDIT&lt;/code&gt; → &lt;code&gt;AUD$&lt;/code&gt;) &lt;em&gt;and&lt;/em&gt; unified auditing are active. Mixed mode exists so you can adopt unified auditing gradually without breaking legacy scripts. It also means your audit data is split across two systems — the thing unified auditing was supposed to end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure unified auditing&lt;/strong&gt; turns traditional auditing off entirely. Moving to it isn't a &lt;code&gt;SET&lt;/code&gt; — you &lt;strong&gt;relink the Oracle binary&lt;/strong&gt; with unified auditing on and restart the instance:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-- database down, then from $ORACLE_HOME/rdbms/lib:
make -f ins_rdbms.mk uniaud_on ioracle ORACLE_HOME=$ORACLE_HOME
-- restart; it's reversible with uniaud_off if you must go back
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;23ai removes the choice.&lt;/strong&gt; As of &lt;strong&gt;Oracle Database 23ai, traditional auditing is desupported.&lt;/strong&gt; A freshly created 23ai database has no traditional audit at all — unified auditing is the audit system. On an &lt;em&gt;upgrade&lt;/em&gt;, any traditional audit settings you were still using are carried forward for compatibility, but the direction is one-way: new work goes to unified, and the old model is on its way out. The practical takeaway — &lt;strong&gt;if your compliance runbook still says "&lt;code&gt;AUDIT&lt;/code&gt; this, check &lt;code&gt;AUD$&lt;/code&gt;," it needs rewriting before you reach 23ai&lt;/strong&gt;, not after.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv7qfydnsn3y9jttqkgjn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv7qfydnsn3y9jttqkgjn.png" alt="Getting Unified Auditing right is three decisions, not a hundred: keep the baseline, add a" width="800" height="1691"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Getting Unified Auditing right is three decisions, not a hundred: keep the baseline, add a few sharp policies, and know which audit mode your release actually runs.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prove your policies actually capture what you think.&lt;/strong&gt; The &lt;a href="https://github.com/pyaroslav/oracle-labs/tree/main/unified-auditing" rel="noopener noreferrer"&gt;unified-auditing lab&lt;/a&gt; stands up an Oracle Database Free container, enables the baseline plus a custom policy on a sensitive table, then &lt;em&gt;triggers&lt;/em&gt; the real events — a failed login, a &lt;code&gt;GRANT&lt;/code&gt;, a read of the protected table — flushes the audit buffer, and queries &lt;code&gt;UNIFIED_AUDIT_TRAIL&lt;/code&gt; to &lt;strong&gt;assert every one was recorded&lt;/strong&gt;. If a policy silently isn't capturing, the run fails. It's the difference between "we have auditing" and "we checked."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Reading and keeping the trail
&lt;/h2&gt;

&lt;p&gt;A trail you never read is theater. Reading unified auditing is one view — &lt;code&gt;UNIFIED_AUDIT_TRAIL&lt;/code&gt; — filtered by what you care about:&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="c1"&gt;-- failed logons in the last day (credential-attack fingerprint)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;event_timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbusername&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userhost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_code&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;unified_audit_trail&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;action_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'LOGON'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;return_code&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt;    &lt;span class="n"&gt;event_timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SYSTIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'1'&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;event_timestamp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two operational details keep this working. First, because records are written in &lt;strong&gt;queued mode&lt;/strong&gt;, the newest ones may still be in the SGA buffer, not yet in the view — force them out with &lt;code&gt;DBMS_AUDIT_MGMT.FLUSH_UNIFIED_AUDIT_TRAIL&lt;/code&gt; before you trust "nothing's there" (the lab does exactly this before it checks). Second, the trail &lt;strong&gt;grows forever unless you manage it&lt;/strong&gt;: set a retention window and a scheduled purge with &lt;strong&gt;&lt;code&gt;DBMS_AUDIT_MGMT&lt;/code&gt;&lt;/strong&gt; so the audit table doesn't quietly consume your tablespace.&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="c1"&gt;-- keep 180 days, then schedule automatic cleanup&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="n"&gt;DBMS_AUDIT_MGMT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SET_LAST_ARCHIVE_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;audit_trail_type&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DBMS_AUDIT_MGMT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AUDIT_TRAIL_UNIFIED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;last_archive_time&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SYSTIMESTAMP&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'180'&lt;/span&gt; &lt;span class="k"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;DBMS_AUDIT_MGMT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CREATE_PURGE_JOB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;audit_trail_type&lt;/span&gt;       &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DBMS_AUDIT_MGMT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AUDIT_TRAIL_UNIFIED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;audit_trail_purge_interval&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;audit_trail_purge_name&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'unified_purge'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;use_last_arch_timestamp&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;TRUE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What teams get wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auditing everything.&lt;/strong&gt; "Audit all actions on all objects" is the firehose that gets muted and then disabled. Enable the baseline, add a few sharp policies, and review them. Coverage you never read is worse than none — it's cost with no benefit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turning off the baseline "for performance."&lt;/strong&gt; &lt;code&gt;ORA_SECURECONFIG&lt;/code&gt; is cheap and its coverage is exactly the security-critical actions. Disabling it is how a database loses the ability to explain who changed its own configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming traditional auditing still works forever.&lt;/strong&gt; It's desupported in 23ai. A runbook built on &lt;code&gt;AUDIT&lt;/code&gt; statements and &lt;code&gt;AUD$&lt;/code&gt; queries breaks on the upgrade you haven't scheduled yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Living in mixed mode by accident.&lt;/strong&gt; On 19c, "we use unified auditing" is often only half true — the old system is still running alongside it, splitting your trail. Decide to go pure, and relink.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting to purge.&lt;/strong&gt; The unified trail grows without bound. No retention policy means an audit table that eventually pressures &lt;code&gt;SYSAUX&lt;/code&gt;/its tablespace — set a &lt;code&gt;DBMS_AUDIT_MGMT&lt;/code&gt; purge job on day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not flushing before you conclude "nothing happened."&lt;/strong&gt; Queued-mode records lag. Flush the buffer before you trust an empty result during an investigation.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Auditing isn't about volume, it's about signal. Use &lt;strong&gt;Unified Auditing&lt;/strong&gt; — one protected, read-only trail in &lt;code&gt;AUDSYS&lt;/code&gt;, configured by policies. Keep the baseline Oracle already enabled (&lt;strong&gt;&lt;code&gt;ORA_SECURECONFIG&lt;/code&gt;&lt;/strong&gt; + &lt;strong&gt;&lt;code&gt;ORA_LOGON_FAILURES&lt;/code&gt;&lt;/strong&gt;) and add a &lt;em&gt;few&lt;/em&gt; narrow policies for your sensitive tables and powerful privileges — never a firehose. Know your mode: &lt;strong&gt;19c runs mixed&lt;/strong&gt; (traditional and unified together), you move to &lt;strong&gt;pure&lt;/strong&gt; by relinking &lt;code&gt;uniaud_on&lt;/code&gt; and restarting, and &lt;strong&gt;23ai desupports traditional auditing outright&lt;/strong&gt;, so rewrite any &lt;code&gt;AUD$&lt;/code&gt;-based runbook before you get there. Read it through &lt;strong&gt;&lt;code&gt;UNIFIED_AUDIT_TRAIL&lt;/code&gt;&lt;/strong&gt; (flush the buffer first), and keep it bounded with a &lt;strong&gt;&lt;code&gt;DBMS_AUDIT_MGMT&lt;/code&gt;&lt;/strong&gt; purge job. A handful of policies you actually read beats a trail of everything you don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the difference between traditional and Unified Auditing in Oracle?
&lt;/h3&gt;

&lt;p&gt;Traditional auditing (the AUDIT command writing to SYS.AUD$, plus separate destinations for fine-grained auditing, OS files, and SYS operations) is configured statement by statement and scattered across multiple trails. Unified Auditing, the model since Oracle 12c, consolidates everything into a single protected, read-only internal table in the AUDSYS schema and is configured through named policies you enable or disable as a unit. Unified Auditing is easier to manage, harder to tamper with, and higher performance because records are written asynchronously. As of Oracle Database 23ai, traditional auditing is desupported.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Unified Auditing enabled by default in Oracle?
&lt;/h3&gt;

&lt;p&gt;Partly. Unified Auditing as a system is available in every 12c and later database, and Oracle pre-enables a baseline of predefined policies — most importantly ORA_SECURECONFIG (security-relevant actions such as CREATE/ALTER USER, GRANT, and ALTER SYSTEM) from 12.2 onward, and ORA_LOGON_FAILURES for failed logons. However, in 12c through 19c the database runs in mixed mode, meaning traditional auditing is still active alongside unified auditing. Running pure unified auditing (unified only) requires relinking the Oracle binary and restarting.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I enable pure Unified Auditing?
&lt;/h3&gt;

&lt;p&gt;Pure Unified Auditing is enabled by relinking the Oracle executable with unified auditing turned on, not by a parameter. Shut the database down, then from $ORACLE_HOME/rdbms/lib run: make -f ins_rdbms.mk uniaud_on ioracle ORACLE_HOME=$ORACLE_HOME, and restart the instance. After that, traditional auditing is disabled and only unified auditing records are produced. The change is reversible by relinking with uniaud_off. In Oracle Database 23ai this is moot because traditional auditing is desupported.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where are Unified Audit records stored and can they be modified?
&lt;/h3&gt;

&lt;p&gt;Unified audit records are stored in a read-only internal table owned by the AUDSYS schema, and you read them through the UNIFIED_AUDIT_TRAIL view. They cannot be changed with ordinary SQL — INSERT, UPDATE, and DELETE against the trail are not permitted, even for SYS. Records are managed only through the DBMS_AUDIT_MGMT package, which is used to archive and purge old records. This protection is a core reason to prefer unified auditing: a compromised privileged account cannot silently erase its own audit trail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do my new audit records not appear in UNIFIED_AUDIT_TRAIL immediately?
&lt;/h3&gt;

&lt;p&gt;Because unified audit records are written in queued (asynchronous) mode by default: they buffer in the SGA and are flushed to the audit table in batches for performance, so the most recent actions can lag before they appear in the view. To force them out immediately — for example during an investigation before concluding that nothing was recorded — run DBMS_AUDIT_MGMT.FLUSH_UNIFIED_AUDIT_TRAIL, then re-query. This buffering is normal and is the tradeoff that keeps auditing cheap.&lt;/p&gt;

&lt;h3&gt;
  
  
  What audit policies should I actually enable?
&lt;/h3&gt;

&lt;p&gt;Keep the baseline Oracle already enables — ORA_SECURECONFIG for security-configuration changes and ORA_LOGON_FAILURES for failed logons — and add a small number of narrow policies for what matters in your database: access to a specific sensitive table, use of powerful ANY privileges, or activity by a particular service account. Scope and condition them (BY user, WHENEVER NOT SUCCESSFUL, or an audit_condition) so they capture signal rather than volume. Auditing everything produces a trail nobody reads; a handful of sharp policies plus the baseline is what actually helps in an incident.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I stop the unified audit trail from growing forever?
&lt;/h3&gt;

&lt;p&gt;Use the DBMS_AUDIT_MGMT package to set a retention window and a scheduled purge for the unified audit trail. Set a last-archive timestamp with DBMS_AUDIT_MGMT.SET_LAST_ARCHIVE_TIMESTAMP and create a recurring cleanup with DBMS_AUDIT_MGMT.CREATE_PURGE_JOB for audit_trail_type AUDIT_TRAIL_UNIFIED. Without a purge policy the audit table grows without bound and eventually pressures its tablespace. Decide a retention period that satisfies your compliance requirement and automate the cleanup from the start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is traditional auditing really gone in Oracle 23ai?
&lt;/h3&gt;

&lt;p&gt;Traditional auditing is desupported in Oracle Database 23ai. A newly created 23ai database has no traditional auditing; unified auditing is the audit system. If you upgrade a database that still had traditional audit settings, those settings are carried forward for compatibility, but new auditing work uses unified auditing and the traditional model should not be relied on going forward. Any process, script, or compliance runbook built on the AUDIT command and the AUD$ table should be migrated to unified auditing before adopting 23ai.&lt;/p&gt;

&lt;p&gt;Auditing is the third leg of the security-and-ops discipline, alongside &lt;a href="https://uptimearchitect.com/blog/oracle-patching-cpu-ru-rur/" rel="noopener noreferrer"&gt;patching&lt;/a&gt; and &lt;a href="https://uptimearchitect.com/blog/oracle-database-hardening-checklist/" rel="noopener noreferrer"&gt;hardening&lt;/a&gt; — patching closes known vulnerabilities, hardening closes the configuration gaps, and auditing is how you &lt;em&gt;know&lt;/em&gt; when either one is being tested. Do it the same way you do the other two: not exhaustively, but deliberately — the few high-value policies, enabled, purged, and actually read. Prove yours capture what you think with the &lt;a href="https://github.com/pyaroslav/oracle-labs/tree/main/unified-auditing" rel="noopener noreferrer"&gt;unified-auditing lab&lt;/a&gt;, and put the review on the same cadence as your quarterly patch.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://uptimearchitect.com/blog/oracle-unified-auditing/" rel="noopener noreferrer"&gt;uptimearchitect.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>auditing</category>
      <category>oraclesecurity</category>
      <category>unifiedauditing</category>
    </item>
    <item>
      <title>The Oracle Hardening Checklist That Actually Matters</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Sat, 01 Aug 2026 22:15:45 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/the-oracle-hardening-checklist-that-actually-matters-eac</link>
      <guid>https://dev.to/uptimearchitect/the-oracle-hardening-checklist-that-actually-matters-eac</guid>
      <description>&lt;p&gt;Open any Oracle security benchmark and you'll find two hundred–plus line items, each with a rationale, a check, and a remediation. It is thorough, it is auditable, and it is where most hardening projects go to die — three weeks of setting obscure parameters that shave a rounding error off your risk while the two gaps that actually get databases breached sit untouched because they weren't near the top of the alphabet.&lt;/p&gt;

&lt;p&gt;Hardening is not a completeness exercise. A short list of controls stops the overwhelming majority of real incidents: accounts that still have their default password, privileges handed out for convenience, a listener open to the world, unencrypted data at rest, and no audit trail to notice any of it. Here's that list — in priority order, each with &lt;em&gt;why it matters&lt;/em&gt;, &lt;em&gt;how it actually gets exploited&lt;/em&gt;, and &lt;em&gt;the one query or command that tells you where you stand&lt;/em&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version.&lt;/strong&gt; Do these six, roughly in order. &lt;strong&gt;(1)&lt;/strong&gt; Find and close accounts with default passwords — &lt;code&gt;DBA_USERS_WITH_DEFPWD&lt;/code&gt; names them for you. &lt;strong&gt;(2)&lt;/strong&gt; Take back what you granted for convenience: &lt;code&gt;EXECUTE&lt;/code&gt; on the network packages (&lt;code&gt;UTL_HTTP&lt;/code&gt;, &lt;code&gt;UTL_TCP&lt;/code&gt;, &lt;code&gt;UTL_SMTP&lt;/code&gt;) from &lt;strong&gt;PUBLIC&lt;/strong&gt;, any &lt;code&gt;%ANY%&lt;/code&gt; system privilege, and &lt;code&gt;DBA&lt;/code&gt; on application accounts. &lt;strong&gt;(3)&lt;/strong&gt; Lock the listener — valid-node checking, admin restrictions, and never on the public internet. &lt;strong&gt;(4)&lt;/strong&gt; Turn on &lt;strong&gt;TDE&lt;/strong&gt; so a stolen datafile or backup is useless. &lt;strong&gt;(5)&lt;/strong&gt; Give the &lt;code&gt;DEFAULT&lt;/code&gt; profile a &lt;strong&gt;failed-login lockout&lt;/strong&gt; and a password verify function. &lt;strong&gt;(6)&lt;/strong&gt; Enable &lt;strong&gt;Unified Auditing&lt;/strong&gt; for the &lt;em&gt;few&lt;/em&gt; things worth watching (&lt;code&gt;ORA_SECURECONFIG&lt;/code&gt; + &lt;code&gt;ORA_LOGON_FAILURES&lt;/code&gt;), not everything. Everything past this is refinement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Kill the default passwords
&lt;/h2&gt;

&lt;p&gt;This is number one because it is the single most reliably exploited weakness in an Oracle estate, and it is free to fix. Every attacker's first move against an Oracle listener is to try known account/password pairs — &lt;code&gt;SYSTEM/manager&lt;/code&gt;, &lt;code&gt;DBSNMP/dbsnmp&lt;/code&gt;, &lt;code&gt;SCOTT/tiger&lt;/code&gt;, an app account whose password equals its username. It requires no exploit, no CVE, nothing but a dictionary.&lt;/p&gt;

&lt;p&gt;You do not have to guess which of your accounts are exposed, because Oracle ships a view that checks the password hashes against its own list of known defaults:&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;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;account_status&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dba_users_with_defpwd&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row is an account an attacker can walk into. For each one: if you don't need it, &lt;strong&gt;lock and expire&lt;/strong&gt; it; if you do, give it a real password. Sample schemas (&lt;code&gt;HR&lt;/code&gt;, &lt;code&gt;OE&lt;/code&gt;, &lt;code&gt;SCOTT&lt;/code&gt;) shouldn't be in a production database at all — they're not installed by default anymore, so if they're present, someone added them.&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;scott&lt;/span&gt;  &lt;span class="n"&gt;ACCOUNT&lt;/span&gt; &lt;span class="k"&gt;LOCK&lt;/span&gt; &lt;span class="n"&gt;PASSWORD&lt;/span&gt; &lt;span class="n"&gt;EXPIRE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;-- don't need it → close it&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;dbsnmp&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;"&amp;lt;a real secret&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- need it → stop using the default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While you're here, look at what's simply &lt;em&gt;open and unused&lt;/em&gt;: &lt;code&gt;SELECT username FROM dba_users WHERE account_status = 'OPEN'&lt;/code&gt; and lock every account no human or application actually logs in as. An account that can't authenticate can't be the way in.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Least privilege: PUBLIC, the ANY privileges, and DBA-for-convenience
&lt;/h2&gt;

&lt;p&gt;Almost every over-privilege problem in Oracle traces back to a grant someone made to &lt;em&gt;stop being asked about it&lt;/em&gt;. Three of them do most of the damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;PUBLIC&lt;/code&gt; means every user, including the one an attacker just compromised.&lt;/strong&gt; Historically Oracle granted &lt;code&gt;EXECUTE&lt;/code&gt; on the network and file packages — &lt;code&gt;UTL_HTTP&lt;/code&gt;, &lt;code&gt;UTL_TCP&lt;/code&gt;, &lt;code&gt;UTL_SMTP&lt;/code&gt;, &lt;code&gt;UTL_INADDR&lt;/code&gt;, &lt;code&gt;UTL_FILE&lt;/code&gt; — to &lt;code&gt;PUBLIC&lt;/code&gt;. Those packages let PL/SQL open outbound network connections and read files; in the hands of a low-privilege account that's been popped, they're an exfiltration and lateral-movement kit. Find what &lt;code&gt;PUBLIC&lt;/code&gt; can execute that it shouldn't:&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;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;privilege&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;dba_tab_privs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;grantee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PUBLIC'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt;    &lt;span class="k"&gt;table_name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'UTL_HTTP'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTL_TCP'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTL_SMTP'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTL_INADDR'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'UTL_FILE'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'DBMS_LOB'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Revoke from &lt;code&gt;PUBLIC&lt;/code&gt; and grant back to the &lt;em&gt;specific&lt;/em&gt; schemas that genuinely need it — usually far fewer than you expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;ANY&lt;/code&gt; privileges are a skeleton key.&lt;/strong&gt; &lt;code&gt;SELECT ANY TABLE&lt;/code&gt;, &lt;code&gt;EXECUTE ANY PROCEDURE&lt;/code&gt;, &lt;code&gt;ALTER ANY …&lt;/code&gt; — each one crosses every schema boundary in the database. They get granted to application accounts and reporting users because it was easier than enumerating objects. List the ones held by accounts &lt;em&gt;you&lt;/em&gt; created (Oracle's own maintained accounts legitimately hold some):&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grantee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;privilege&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;dba_sys_privs&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;dba_users&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grantee&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;oracle_maintained&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'N'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt;    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;privilege&lt;/span&gt; &lt;span class="k"&gt;LIKE&lt;/span&gt; &lt;span class="s1"&gt;'%ANY%'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;DBA&lt;/code&gt; (or &lt;code&gt;PDB_DBA&lt;/code&gt;) on an application account&lt;/strong&gt; is the same mistake wearing a role. An app should own its objects and hold exactly the privileges its code needs — never the role that can read, alter, and drop everything. Check with &lt;code&gt;DBA_ROLE_PRIVS&lt;/code&gt; filtered the same way, and replace the role with a tailored one.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Lock the front door: the listener and the network
&lt;/h2&gt;

&lt;p&gt;The database can be perfectly hardened and still be trivially reachable if the listener in front of it isn't. Three controls matter more than the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Valid-node checking.&lt;/strong&gt; Restrict which hosts may even &lt;em&gt;connect&lt;/em&gt; to the listener with &lt;code&gt;TCP.VALIDNODE_CHECKING&lt;/code&gt;, &lt;code&gt;TCP.INVITED_NODES&lt;/code&gt;, and &lt;code&gt;TCP.EXCLUDED_NODES&lt;/code&gt; in &lt;code&gt;sqlnet.ora&lt;/code&gt;. It's an allow-list at the network layer, before authentication.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Admin restrictions.&lt;/strong&gt; Set &lt;code&gt;ADMIN_RESTRICTIONS_&amp;lt;listener&amp;gt; = ON&lt;/code&gt; so nobody can reconfigure the listener remotely at runtime, and make sure it requires OS authentication for admin — a listener you can &lt;code&gt;set&lt;/code&gt; remotely is a listener an attacker can &lt;code&gt;set&lt;/code&gt; remotely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never on the public internet.&lt;/strong&gt; A database listener on &lt;code&gt;0.0.0.0:1521&lt;/code&gt; reachable from outside your network is the finding behind a large share of Oracle compromises. It belongs on a private subnet, behind a security group that allows only the application tier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can encrypt the client-to-database traffic, do — either native network encryption in &lt;code&gt;sqlnet.ora&lt;/code&gt; or TCPS/TLS. An unencrypted connection is credentials and data in cleartext on the wire.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Encrypt at rest: TDE
&lt;/h2&gt;

&lt;p&gt;Everything above protects the &lt;em&gt;running&lt;/em&gt; database. &lt;strong&gt;Transparent Data Encryption&lt;/strong&gt; protects it when it isn't running — when a datafile, a backup piece, or a decommissioned disk leaves the building. Without TDE, a stolen datafile or RMAN backup is just a file an attacker &lt;code&gt;strings&lt;/code&gt;; with it, it's noise without the keystore.&lt;/p&gt;

&lt;p&gt;TDE encrypts at the tablespace (or column) level and is transparent to the application — no SQL changes. You set up a keystore once, then encrypt:&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="c1"&gt;-- one-time: configure and open a keystore (WALLET_ROOT), then:&lt;/span&gt;
&lt;span class="n"&gt;ADMINISTER&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="n"&gt;MANAGEMENT&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="k"&gt;FORCE&lt;/span&gt; &lt;span class="n"&gt;KEYSTORE&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nv"&gt;"&amp;lt;pwd&amp;gt;"&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;BACKUP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;TABLESPACE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="n"&gt;ENCRYPTION&lt;/span&gt; &lt;span class="n"&gt;ONLINE&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="s1"&gt;'AES256'&lt;/span&gt; &lt;span class="n"&gt;ENCRYPT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New tablespaces should be encrypted from creation. And TDE is what makes an RMAN backup safe to store off-site — pair it with the recovery discipline in the &lt;a href="https://uptimearchitect.com/blog/oracle-rman-recovery-runbook/" rel="noopener noreferrer"&gt;RMAN Recovery Runbook&lt;/a&gt; so the backup you can restore is also the backup you don't have to worry about losing.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Password profiles: make brute force expensive
&lt;/h2&gt;

&lt;p&gt;Default-password hygiene (item 1) closes the &lt;em&gt;known&lt;/em&gt; passwords. A &lt;strong&gt;failed-login lockout&lt;/strong&gt; closes the guessable ones by making an online brute-force attack die after a handful of tries. This is one &lt;code&gt;ALTER PROFILE&lt;/code&gt; on the &lt;code&gt;DEFAULT&lt;/code&gt; profile — which nearly every account inherits — and it's often left wide open:&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;resource_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;limit&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;dba_profiles&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;  &lt;span class="n"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DEFAULT'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt;    &lt;span class="n"&gt;resource_name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'FAILED_LOGIN_ATTEMPTS'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'PASSWORD_LIFE_TIME'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'PASSWORD_VERIFY_FUNCTION'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;FAILED_LOGIN_ATTEMPTS&lt;/code&gt; reads &lt;code&gt;UNLIMITED&lt;/code&gt;, an attacker can guess forever. Set a finite lockout, a password lifetime, and attach a verify function so weak passwords can't be set in the first place:&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;ALTER&lt;/span&gt; &lt;span class="n"&gt;PROFILE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt;
  &lt;span class="n"&gt;FAILED_LOGIN_ATTEMPTS&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
  &lt;span class="n"&gt;PASSWORD_LIFE_TIME&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;
  &lt;span class="n"&gt;PASSWORD_VERIFY_FUNCTION&lt;/span&gt; &lt;span class="n"&gt;ora12c_strong_verify_function&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Balance the lockout against your reality: too aggressive a threshold plus a shared service account is a self-inflicted denial of service. Ten is a sane default; service accounts should use long, rotated secrets rather than relying on lockout at all.)&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Audit the few things worth auditing
&lt;/h2&gt;

&lt;p&gt;The reason most Oracle databases have no useful audit trail is that "audit everything" produces a firehose nobody reads, so eventually someone turns it off. &lt;strong&gt;Unified Auditing&lt;/strong&gt; — the model since 12c, which writes to a protected internal table instead of OS files — makes selective, low-noise auditing easy through &lt;em&gt;policies&lt;/em&gt;. You don't need dozens; you need the two that catch privilege abuse and credential attacks, which Oracle pre-defines:&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="c1"&gt;-- what's actually enabled right now:&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;policy_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enabled_option&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;audit_unified_enabled_policies&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;policy_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- the sensible baseline:&lt;/span&gt;
&lt;span class="n"&gt;AUDIT&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;ora_secureconfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;-- privileged actions, structural changes (on by default — keep it)&lt;/span&gt;
&lt;span class="n"&gt;AUDIT&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;ora_logon_failures&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- failed logins: the brute force from item 5, recorded&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ORA_SECURECONFIG&lt;/code&gt; is enabled by default in a modern database — the failure mode is someone having turned it &lt;em&gt;off&lt;/em&gt;. From that baseline, add narrow policies for the things specific to you (access to a sensitive table, use of a powerful role) rather than broad ones. If you're still on the legacy &lt;code&gt;AUDIT&lt;/code&gt;/&lt;code&gt;AUD$&lt;/code&gt; model, plan the move to pure Unified Auditing — it's faster and its trail can't be tampered with from outside the database.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ck318neapo6znmu2u06.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6ck318neapo6znmu2u06.png" alt="Oracle database hardening in priority order — the six controls, most impactful first" width="800" height="2872"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Hardening in priority order: each step closes a class of real attack. Do them top to bottom — a later control on an unhardened account is polish on a door that's already open.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Run the checklist against a live database.&lt;/strong&gt; The &lt;a href="https://github.com/pyaroslav/oracle-labs/tree/main/hardening" rel="noopener noreferrer"&gt;hardening-audit lab&lt;/a&gt; stands up an Oracle Database Free container in a deliberately weak state, runs &lt;code&gt;./run.sh audit&lt;/code&gt; to score it against these controls — default passwords, &lt;code&gt;PUBLIC&lt;/code&gt;/&lt;code&gt;ANY&lt;/code&gt; grants, &lt;code&gt;DBA&lt;/code&gt;-on-app-accounts, the failed-login profile, and the auditing baseline — then &lt;code&gt;./run.sh harden&lt;/code&gt; fixes them and re-audits to &lt;strong&gt;prove&lt;/strong&gt; every check flips from FAIL to PASS. The CI matrix runs the whole cycle on every push, so the numbers aren't a claim, they're a test.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What teams get wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chasing the whole benchmark.&lt;/strong&gt; Two hundred controls sorted by document order means the critical five compete for attention with the trivial. Prioritize by exploitability, not by checklist completeness.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditing everything, reading nothing.&lt;/strong&gt; A firehose gets muted. Enable the two baseline policies plus a few targeted ones, and actually review them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaving default and unused accounts open.&lt;/strong&gt; &lt;code&gt;DBA_USERS_WITH_DEFPWD&lt;/code&gt; and &lt;code&gt;account_status = 'OPEN'&lt;/code&gt; take one query each. There is no excuse for an open account with a known password.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Granting &lt;code&gt;DBA&lt;/code&gt; (or &lt;code&gt;%ANY%&lt;/code&gt;) for convenience.&lt;/strong&gt; The app that "just needs it to work" becomes the account that can read and drop every schema once it's compromised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating hardening as one-time.&lt;/strong&gt; Grants accumulate, accounts get created, someone disables a policy to debug and forgets. Configuration drifts; re-audit on a cadence, the same way you patch on a cadence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encrypting nothing because "it's internal."&lt;/strong&gt; Internal networks get breached and backups get lost. TDE is the control that makes those events non-events.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Skip the two-hundred-item benchmark and do the six that matter, in order. Close accounts with default passwords (&lt;code&gt;DBA_USERS_WITH_DEFPWD&lt;/code&gt; finds them). Take back convenience grants — &lt;code&gt;EXECUTE&lt;/code&gt; on the network packages from &lt;code&gt;PUBLIC&lt;/code&gt;, every &lt;code&gt;%ANY%&lt;/code&gt; privilege, and &lt;code&gt;DBA&lt;/code&gt; on application accounts. Lock the listener with valid-node checking and admin restrictions, and keep it off the public internet. Turn on &lt;strong&gt;TDE&lt;/strong&gt; so stolen datafiles and backups are useless. Give the &lt;code&gt;DEFAULT&lt;/code&gt; profile a &lt;strong&gt;failed-login lockout&lt;/strong&gt; and a verify function. Enable &lt;strong&gt;Unified Auditing&lt;/strong&gt; for &lt;code&gt;ORA_SECURECONFIG&lt;/code&gt; and &lt;code&gt;ORA_LOGON_FAILURES&lt;/code&gt;, then add narrow policies — not a firehose. Then re-audit on a schedule, because hardening drifts. That list stops the incidents that actually happen; everything else is refinement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is the most important first step in Oracle database hardening?
&lt;/h3&gt;

&lt;p&gt;Closing accounts that still have a default or known password. It is the most reliably exploited Oracle weakness and requires no exploit — an attacker simply tries known username/password pairs against the listener. Oracle provides the DBA_USERS_WITH_DEFPWD view, which lists every account whose password matches a known default; lock and expire the ones you do not need and set real passwords on the ones you do. It costs one query and a few ALTER USER statements and removes the easiest way in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I revoke EXECUTE on packages from PUBLIC?
&lt;/h3&gt;

&lt;p&gt;For the network and file packages — UTL_HTTP, UTL_TCP, UTL_SMTP, UTL_INADDR, UTL_FILE — yes, in most environments. Granted to PUBLIC, they let any authenticated account (including a compromised low-privilege one) open outbound network connections or read files, which is an exfiltration and lateral-movement capability. Revoke EXECUTE from PUBLIC and grant it back only to the specific schemas that genuinely require it. Test first, because application code occasionally relies on these grants.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between traditional and Unified Auditing in Oracle?
&lt;/h3&gt;

&lt;p&gt;Traditional auditing (the legacy AUDIT command writing to SYS.AUD$ or to OS files) is configured statement by statement and can be verbose and hard to protect. Unified Auditing, the model since Oracle 12c, consolidates all audit data into a single protected, read-only internal table and is configured through policies you enable or disable as a unit. Unified Auditing is faster, harder to tamper with, and easier to keep low-noise. Oracle recommends moving to pure Unified Auditing; enable the ORA_SECURECONFIG and ORA_LOGON_FAILURES policies as a baseline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does hardening an Oracle database require downtime?
&lt;/h3&gt;

&lt;p&gt;Most of it does not. Locking accounts, revoking privileges, altering the DEFAULT profile, and enabling Unified Auditing policies are all online operations. Encrypting existing tablespaces with TDE can be done online in current releases, though the initial keystore configuration may involve a restart depending on how WALLET_ROOT is set. Listener and sqlnet.ora changes take effect on a listener reload, not a database restart. Plan TDE and network-encryption rollouts, but the high-value account and privilege work is all no-outage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Transparent Data Encryption (TDE) necessary if my database is on an internal network?
&lt;/h3&gt;

&lt;p&gt;It protects a different threat than network controls do. Listener and firewall rules protect the running database from unauthorized connections; TDE protects the data when it is at rest and leaves the running system — a stolen or misplaced datafile, an RMAN backup piece copied off-site, a decommissioned disk. Internal networks are breached and backups do get lost, so for any database holding sensitive or regulated data, TDE is the control that turns those events into non-events. It is transparent to the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  How often should I re-audit an Oracle database against a hardening checklist?
&lt;/h3&gt;

&lt;p&gt;On a defined cadence, the same way you patch — quarterly is a reasonable baseline, plus after any significant change. Hardening drifts: privileges get granted for a one-off task and never revoked, accounts get created, someone disables an audit policy to debug an issue and forgets to re-enable it. A one-time hardening project degrades steadily unless you re-check it. Automating the checks (a script or a lab-style audit that reports PASS/FAIL) makes the re-audit cheap enough to actually do.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the ANY privileges and why are they a hardening risk?
&lt;/h3&gt;

&lt;p&gt;The system privileges containing ANY — SELECT ANY TABLE, EXECUTE ANY PROCEDURE, ALTER ANY TABLE, and others — grant an operation across every schema in the database, ignoring object ownership boundaries. They are commonly granted to application or reporting accounts as a shortcut instead of enumerating the specific objects needed. If such an account is compromised, the ANY privilege lets the attacker read, alter, or drop objects anywhere. Find them with DBA_SYS_PRIVS filtered to accounts where ORACLE_MAINTAINED = N, and replace them with grants on the specific objects the account actually uses.&lt;/p&gt;

&lt;p&gt;Hardening and &lt;a href="https://uptimearchitect.com/blog/oracle-patching-cpu-ru-rur/" rel="noopener noreferrer"&gt;patching&lt;/a&gt; are the two halves of the security-and-ops discipline: patching closes the vulnerabilities Oracle tells you about, hardening closes the ones your own configuration opened. Neither is a one-time project — both are a cadence. Start with the six controls above, prove them with the &lt;a href="https://github.com/pyaroslav/oracle-labs/tree/main/hardening" rel="noopener noreferrer"&gt;hardening-audit lab&lt;/a&gt;, and put the re-audit on the same calendar as your quarterly Release Update.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://uptimearchitect.com/blog/oracle-database-hardening-checklist/" rel="noopener noreferrer"&gt;uptimearchitect.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>hardening</category>
      <category>oraclesecurity</category>
      <category>leastprivilege</category>
    </item>
    <item>
      <title>Okroshka, Ice-Cold: I painted a Khokhloma bowl of cold soup in pure CSS (0 lines of JS — even the interaction)</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Sat, 01 Aug 2026 16:40:27 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/okroshka-ice-cold-i-painted-a-khokhloma-bowl-of-cold-soup-in-pure-css-0-lines-of-js-even-the-j2m</link>
      <guid>https://dev.to/uptimearchitect/okroshka-ice-cold-i-painted-a-khokhloma-bowl-of-cold-soup-in-pure-css-0-lines-of-js-even-the-j2m</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, CSS Art&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;Okroshka is the soup you eat when it's too hot to eat soup — a cold kvass (or&lt;br&gt;
kefir) base over a confetti of diced cucumber, radish, potato and egg, buried&lt;br&gt;
under more fresh dill than seems reasonable, with one proud dollop of smetana&lt;br&gt;
in the middle.&lt;/p&gt;

&lt;p&gt;Mine comes with a memory. When I was six, my parents took me haymaking in&lt;br&gt;
the water meadows. At noon we would hide in the dense shade of a fresh&lt;br&gt;
haystack, and my father would pull a jar wrapped in a thick towel out of a&lt;br&gt;
canvas bag: ice-cold okroshka my mother had made back at dawn. The cucumbers&lt;br&gt;
crunched, the sharp homemade kvass stung the tongue, and every sip drove the&lt;br&gt;
exhaustion away. (The full memory lives in my sister entry, linked below.)&lt;/p&gt;

&lt;p&gt;The scene I painted is the okroshka of my imagination's summer table:&lt;br&gt;
a &lt;strong&gt;Khokhloma bowl&lt;/strong&gt; — that black-lacquered Russian wooden ware painted with&lt;br&gt;
gold leaves and red berries — on a red lacquer saucer, over a green gingham&lt;br&gt;
tablecloth, with rye bread, a painted wooden spoon, and a little bowl of&lt;br&gt;
smetana standing by.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/pyaro/embed/yygGVMg?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Click (or Tab + Space) the bowl to stir the smetana into the soup.&lt;/strong&gt; The&lt;br&gt;
cream sweeps a full turn and a half through the dill, the herb blanket gets&lt;br&gt;
dragged along with it, and it all settles back slowly when you let go.&lt;/p&gt;

&lt;p&gt;There is &lt;strong&gt;no JavaScript anywhere on this page&lt;/strong&gt; — not for the art, and not&lt;br&gt;
for the interaction. The stir is a visually-hidden (but fully keyboard-real)&lt;br&gt;
checkbox, a &lt;code&gt;:has()&lt;/code&gt; selector, and one registered &lt;code&gt;@property&lt;/code&gt; transition.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffq2g57nfo43x90mpahs3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffq2g57nfo43x90mpahs3.png" alt="The scene at rest: a Khokhloma bowl of okroshka under a blanket of dill on a green gingham cloth" width="800" height="557"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flsxghg4qu7rroshxetpe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flsxghg4qu7rroshxetpe.png" alt="Mid-stir: the white cream comma sweeping through the dill blanket" width="800" height="557"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;Some of the tricks I'm proudest of, in the order they saved me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The curved rim bands ("ellipse shingles").&lt;/strong&gt; A Khokhloma bowl has a red lip&lt;br&gt;
and a gold band that &lt;em&gt;curve&lt;/em&gt; with the rim. Horizontal gradient stripes can't&lt;br&gt;
bend — so each band is the same ellipse as the bowl opening, dropped a couple&lt;br&gt;
of &lt;code&gt;cqi&lt;/code&gt; lower. Only its curved front crescent peeks out from behind the&lt;br&gt;
ellipse above it. Three stacked ellipses, a perfectly curved rim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dill blanket.&lt;/strong&gt; The photo-real signature of okroshka is a surface&lt;br&gt;
&lt;em&gt;buried&lt;/em&gt; in chopped dill. Mine is five overlapping speckle patterns with&lt;br&gt;
deliberately co-prime-ish &lt;code&gt;background-size&lt;/code&gt; tiles (2.3×1.7, 1.7×1.3,&lt;br&gt;
2.9×2.1…) so the grids never visually align, a tiny &lt;code&gt;blur()&lt;/code&gt; to melt them&lt;br&gt;
into herb texture, and a radial ring mask that opens the center for the&lt;br&gt;
smetana dollop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stirring a flat ellipse.&lt;/strong&gt; Rotating the dill layer in screen space swings&lt;br&gt;
it out of the soup plane like a slab (I know because I shipped it for five&lt;br&gt;
minutes). The fix is the old animator's trick — squash → rotate → unsquash:&lt;br&gt;
&lt;code&gt;transform: scaleY(0.25) rotate(38deg) scaleY(4)&lt;/code&gt; stretches the ellipse into&lt;br&gt;
a circle, rotates it &lt;em&gt;in that space&lt;/em&gt;, and squashes it back. The silhouette&lt;br&gt;
never moves; the texture swirls in the plane of the soup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The smetana swirl.&lt;/strong&gt; A conic-gradient comma masked into a ring, whose start&lt;br&gt;
angle is a registered custom property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@property&lt;/span&gt; &lt;span class="n"&gt;--swirl&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;syntax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'&amp;lt;angle&amp;gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;inherits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;initial-value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0deg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.stage&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;.stir&lt;/span&gt;&lt;span class="nd"&gt;:checked&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;.soup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--swirl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;540deg&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.soup&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--swirl&lt;/span&gt; &lt;span class="m"&gt;1.6s&lt;/span&gt; &lt;span class="n"&gt;cubic-bezier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registration is the whole trick — an unregistered custom property would snap&lt;br&gt;
instead of sweeping. Unchecking transitions back over 2.4s, because cream&lt;br&gt;
settles slower than it stirs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dill fronds with &lt;code&gt;mask-composite&lt;/code&gt;.&lt;/strong&gt; A frond is a repeating-conic fan of&lt;br&gt;
needle rays &lt;em&gt;intersected&lt;/em&gt; with a 96° wedge — without the wedge the fan is a&lt;br&gt;
360° starburst (also shipped that for five minutes).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility, because art is still a page.&lt;/strong&gt; The scene is one&lt;br&gt;
&lt;code&gt;role="img"&lt;/code&gt; figure with alt text written like a photo caption. The checkbox&lt;br&gt;
and its label live &lt;em&gt;outside&lt;/em&gt; the figure (&lt;code&gt;role="img"&lt;/code&gt; flattens its subtree&lt;br&gt;
for assistive tech), the label announces "Stir the sour cream into the soup",&lt;br&gt;
and focus draws a gold ring around the bowl's lip. Every animation and&lt;br&gt;
transition lives inside &lt;code&gt;@media (prefers-reduced-motion: no-preference)&lt;/code&gt; —&lt;br&gt;
reduced-motion visitors get a finished still life, and the stir still works&lt;br&gt;
as an instant state change.&lt;/p&gt;

&lt;p&gt;Everything is sized in &lt;code&gt;cqi&lt;/code&gt; container units, so the same fragment drops&lt;br&gt;
into any page at any width — which is exactly what my sister entry does:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://dev.to/uptimearchitect/dear-okroshka-a-love-letter-to-a-cold-soup-and-a-landing-page-that-switches-sides-with-zero-ope"&gt;Dear Okroshka — a love letter to a cold soup&lt;/a&gt;&lt;/strong&gt;, a full landing page for the same dish.&lt;/p&gt;

&lt;p&gt;Code is MIT-licensed.&lt;/p&gt;

</description>
      <category>frontendchallenge</category>
      <category>devchallenge</category>
      <category>css</category>
    </item>
    <item>
      <title>Dear Okroshka: a love letter to a cold soup and a landing page that switches sides with zero JavaScript</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Sat, 01 Aug 2026 16:36:13 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/dear-okroshka-a-love-letter-to-a-cold-soup-and-a-landing-page-that-switches-sides-with-zero-ope</link>
      <guid>https://dev.to/uptimearchitect/dear-okroshka-a-love-letter-to-a-cold-soup-and-a-landing-page-that-switches-sides-with-zero-ope</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, Perfect Landing&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;The prompt allowed "a love letter to a regional dish," so I took it&lt;br&gt;
literally: &lt;strong&gt;Dear Okroshka&lt;/strong&gt; is a single-page letter written &lt;em&gt;to&lt;/em&gt; the cold&lt;br&gt;
Eastern European summer soup — with a salutation, enclosed evidence (it's on&lt;br&gt;
an actual Russian postage stamp), an anatomy of the bowl, a recipe you can&lt;br&gt;
check off while cooking, and a sign-off.&lt;/p&gt;

&lt;p&gt;Okroshka is my dish because of one memory: the shade of a fresh haystack at&lt;br&gt;
noon during haymaking, and a jar wrapped in a thick towel — ice-cold&lt;br&gt;
okroshka my mother had made at dawn, sharp kvass stinging the tongue, the&lt;br&gt;
air full of dill. That memory, nearly in full, is the letter's centerpiece&lt;br&gt;
on the page.&lt;/p&gt;

&lt;p&gt;The centerpiece is the argument every okroshka household has: &lt;strong&gt;kvass or&lt;br&gt;
kefir?&lt;/strong&gt; The page doesn't just describe the feud — it takes sides. A native&lt;br&gt;
radio group re-themes the entire page (colors, content strings, even the&lt;br&gt;
recipe's first ingredient) between an amber rye world and a cool milky one.&lt;/p&gt;

&lt;p&gt;The whole switch is CSS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;#base-kefir&lt;/span&gt;&lt;span class="nd"&gt;:checked&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#F5F8F2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--ink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#25322B&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-strong&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#2C5A3A&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c"&gt;/* …every token */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;body&lt;/span&gt;&lt;span class="nd"&gt;:has&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;#base-kefir&lt;/span&gt;&lt;span class="nd"&gt;:checked&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;.only-kvass&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JavaScript is optional on this page.&lt;/strong&gt; The ~60 lines it ships add two&lt;br&gt;
enhancements: scroll-reveal (gated on &lt;em&gt;both&lt;/em&gt; an &lt;code&gt;html.js&lt;/code&gt; class and&lt;br&gt;
&lt;code&gt;prefers-reduced-motion: no-preference&lt;/code&gt;, so no-JS and reduced-motion visitors&lt;br&gt;
see everything with zero flash) and a polite &lt;code&gt;role="status"&lt;/code&gt; announcement&lt;br&gt;
when the base changes. Turn JS off and the page still re-themes, the&lt;br&gt;
ingredient cards still open, the recipe still checks off.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/pyaro/embed/EaZGNXQ?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkk3b29jtr9vmeijvgyba.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkk3b29jtr9vmeijvgyba.png" alt="The debate section in the kefir theme — pick a side and the whole page takes it" width="800" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why the switch is a radio group, not a toggle.&lt;/strong&gt; My first sketch was an&lt;br&gt;
&lt;code&gt;aria-pressed&lt;/code&gt; button. But neither kvass nor kefir is "off" — a switch is&lt;br&gt;
the wrong mental model, and a screen reader would announce it misleadingly.&lt;br&gt;
A native radio group in a fieldset gives exclusivity, arrow-key behavior and&lt;br&gt;
"selected, 1 of 2" announcements for free — and it's what makes the zero-JS&lt;br&gt;
theming possible at all. The accessible choice and the clever choice turned&lt;br&gt;
out to be the same choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessibility was a judging criterion, so I treated it as a feature:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Landmarks + labeled sections, one &lt;code&gt;h1&lt;/code&gt;, no heading skips; skip link.&lt;/li&gt;
&lt;li&gt;Both themes' text tokens are contrast-verified: every body-text pair
≥ 4.5:1 (kvass ink 11.66:1, muted 5.97:1; kefir ink 12.48:1, muted 5.87:1).&lt;/li&gt;
&lt;li&gt;The ingredient explorer is native &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt; — keyboard and screen-reader
operable with zero ARIA bolted on. The recipe steps are real labeled
checkboxes; the done-state is strikethrough + the checkbox itself, never
color alone.&lt;/li&gt;
&lt;li&gt;Reduced motion: no reveal offsets, no smooth scroll, no theme transition.&lt;/li&gt;
&lt;li&gt;Forced colors (Windows High Contrast): the selected base keeps a
double-border so it survives color removal.&lt;/li&gt;
&lt;li&gt;Lighthouse accessibility: &lt;strong&gt;100&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Real photography, licensed properly.&lt;/strong&gt; The food photos are from Wikimedia&lt;br&gt;
Commons (CC BY / CC BY-SA / public domain), credited inline under each&lt;br&gt;
figure and in the footer — including a matched pair of kvass and kefir cups&lt;br&gt;
by the same photographer for the debate, a 2016 Russian okroshka postage&lt;br&gt;
stamp, and a July 1977 photo of a street kvass barrel with its queue.&lt;br&gt;
Choosing photos that share warmth turned out&lt;br&gt;
to be half the design system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No webfonts, no frameworks, no build.&lt;/strong&gt; The type system is two stacks:&lt;br&gt;
a Palatino-class serif for the letter's voice, system sans for the UI layer.&lt;br&gt;
The contrast between them &lt;em&gt;is&lt;/em&gt; the typography. One fluid clamp() scale, one&lt;br&gt;
token sheet, five labeled CSS layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The sister entry.&lt;/strong&gt; The gingham ribbon under the hero is a hand-off: the&lt;br&gt;
same table setting exists as a pure-CSS painting — &lt;strong&gt;&lt;a href="https://dev.to/uptimearchitect/okroshka-ice-cold-i-painted-a-khokhloma-bowl-of-cold-soup-in-pure-css-0-lines-of-js-even-the-j2m"&gt;Okroshka, Ice-Cold&lt;/a&gt;&lt;/strong&gt; — my entry for the CSS Art prompt.&lt;br&gt;
Same dish, two languages.&lt;/p&gt;

&lt;p&gt;Code is MIT-licensed.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>The transaction that committed in the past: how I diagnosed one bug six times in an evening</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Sat, 01 Aug 2026 13:26:17 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/the-transaction-that-committed-in-the-past-how-i-diagnosed-one-bug-six-times-in-an-evening-gli</link>
      <guid>https://dev.to/uptimearchitect/the-transaction-that-committed-in-the-past-how-i-diagnosed-one-bug-six-times-in-an-evening-gli</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There is a special kind of bug that doesn't crash anything. No exception, no error log, no red dashboard. The system hums along, green across the board — and quietly throws away your data.&lt;/p&gt;

&lt;p&gt;This is the story of one of those, in a change-data-capture engine I build in Rust — a system that reads Oracle's redo logs byte-by-byte, off the database, and replicates committed transactions to warehouses and streams. Its one sacred promise: &lt;strong&gt;if Oracle committed it, we deliver it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One June evening, a test proved the promise was a lie. Over the next three hours and thirteen minutes I diagnosed the bug six times. Only the first was right — and I dismissed it, then chased four wrong theories, caught myself in an embarrassing hex-arithmetic error halfway through, and circled all the way back to where I'd started. The trail of wrong turns is preserved, in real time, in my commit history, and I'm going to walk you through all of it, timestamps and all, because the wrong turns &lt;em&gt;are&lt;/em&gt; the story.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Up front, so you know what you're reading: the engine is closed-source, so there's no repo to hand you. But every commit message I quote below is real — lifted verbatim from that evening's git history, wrong arithmetic and all. Internal tool and ticket names are the only things I've changed.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Two weeks earlier I had shipped the engine's real-time mode: instead of waiting for Oracle to archive a redo log and reading it cold, the tailer polls the &lt;strong&gt;live online redo log&lt;/strong&gt; every few hundred milliseconds — decoding a file &lt;em&gt;while Oracle is still writing it&lt;/em&gt;. Median commit-to-decoded latency: 238 ms. I was very proud of it.&lt;/p&gt;

&lt;p&gt;Polling a live file means re-decoding an ever-growing window, which means seeing the same transactions again on every poll. So the tailer deduplicated across polls the obvious way: remember the highest commit SCN you've emitted (Oracle's System Change Number — a monotonically increasing clock stamped on every change), and on the next poll, skip everything at or below that floor.&lt;/p&gt;

&lt;p&gt;Simple. Obvious. Wrong in a way that took me six theories — and a full circle back to the first — to prove.&lt;/p&gt;

&lt;h2&gt;
  
  
  20:03 — Theory #1: it's the dedup floor
&lt;/h2&gt;

&lt;p&gt;A long-transaction test went red: open a transaction, leave it open while a dozen short transactions commit, then commit it. The engine captured the twelve short ones and &lt;strong&gt;silently dropped the long one&lt;/strong&gt;. The archived-log path captured it fine — this was real-time-mode only.&lt;/p&gt;

&lt;p&gt;My first commit that evening documented a suspicion that will sound very smart in about three hours:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"the windowed re-decode dedups by a scalar commit-SCN floor … the txn's insert is released under that prior (low) commit SCN, and once the floor passes it the row is pruned forever. … **Proper fix is a design change: dedup by transaction identity … not a scalar SCN floor.&lt;/em&gt;&lt;em&gt;"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wrote down the correct fix, in the first twenty minutes, in the commit message. Then I spent the rest of the evening talking myself out of it — because my repro was dirty in a way I didn't yet understand, and clean-looking evidence was about to point somewhere much scarier.&lt;/p&gt;

&lt;h2&gt;
  
  
  20:17 — Theory #2: no, it's the core decoder
&lt;/h2&gt;

&lt;p&gt;I tightened the repro with unique per-run markers, re-ran, and the new evidence said the dropped row's &lt;code&gt;commit_scn&lt;/code&gt; equaled its &lt;em&gt;insert&lt;/em&gt; SCN — not its real commit SCN. Worse: the batch path decoding the very same log showed the same wrong value. This wasn't a real-time dedup quirk. This was the &lt;strong&gt;core decoder&lt;/strong&gt; stamping the wrong commit SCN on any transaction held open while others commit.&lt;/p&gt;

&lt;p&gt;I escalated it in the log: core decode bug, affects every path, do not rush the fix. The dedup-floor theory from 14 minutes ago? Officially a red herring, "misled by cross-run contamination." (Hold that phrase.)&lt;/p&gt;

&lt;h2&gt;
  
  
  20:27 — Theory #3: a single wrong byte
&lt;/h2&gt;

&lt;p&gt;If the decoder stamps the wrong SCN, find where. I dumped the raw redo with &lt;code&gt;ALTER SYSTEM DUMP LOGFILE&lt;/code&gt; and went byte-level. (A held-open transaction is an &lt;em&gt;IMU&lt;/em&gt; — in-memory undo — transaction: Oracle buffers its redo privately and flushes begin + insert + commit as one unit at commit time.) The dump was unambiguous: the row's true SCN was &lt;code&gt;0x150b4a1&lt;/code&gt; everywhere it mattered, but the decoder emitted &lt;code&gt;0x150b2a1&lt;/code&gt; — a value that appears &lt;strong&gt;nowhere&lt;/strong&gt; in the log. One byte off. My commit concluded the parser was mis-framing the record, and helpfully wrote out the decimal conversions of both hex values.&lt;/p&gt;

&lt;p&gt;One of those conversions was wrong. Nobody noticed. Especially not me.&lt;/p&gt;

&lt;h2&gt;
  
  
  20:31 — Theory #4: the byte is fabricated downstream
&lt;/h2&gt;

&lt;p&gt;Four minutes later, an offline probe over the preserved log killed Theory #3: the parser framed the record perfectly. Every change-vector SCN in that record was correct. The mystery value &lt;code&gt;0x150b2a1&lt;/code&gt; matched &lt;em&gt;no&lt;/em&gt; header anywhere in the file — so it wasn't mis-read, it was &lt;strong&gt;fabricated&lt;/strong&gt; somewhere downstream in event assembly. I listed three candidate functions and narrowed the hunt.&lt;/p&gt;

&lt;p&gt;I was now hunting the origin of a value that did not exist. For a very good reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  20:42 — Theory #5: the pivot
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"That was a HEX-CONVERSION ERROR on my part: &lt;code&gt;0x150b4a1&lt;/code&gt; = 22066337 (the CORRECT commit SCN), not &lt;code&gt;0x150b2a1&lt;/code&gt;. The held-open IMU transaction actually decodes CORRECTLY."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There it is, verbatim, the most embarrassing commit message I have ever written — and the most valuable. Fifteen minutes earlier, doing arithmetic in my head at 20:27, I had converted &lt;code&gt;0x150b4a1&lt;/code&gt; to the wrong decimal and then pinned the &lt;em&gt;correct&lt;/em&gt; decimal onto the &lt;em&gt;wrong&lt;/em&gt; hex string. The decoder had been right all along. The "single wrong byte" was in my head. The batch path was fine; the caveats I'd splashed across three docs got withdrawn.&lt;/p&gt;

&lt;p&gt;Which meant the bug was real-time-only after all… and I still hadn't found it. Two reasoned fixes had already failed — implemented, 140 tests green, repro still red, reverted. My commit ends with a change of method that decided the whole hunt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"the next step is an OFFLINE poll-simulation over the preserved corpus to pin the exact drop and iterate the fix fast (not rebuild-and-pray)."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  21:52 — Theory #6: the ghost with four timestamps
&lt;/h2&gt;

&lt;p&gt;I spent the next seventy minutes building instead of guessing: I split the tailer's dedup core into a pure function an offline test could drive, and built a serializer that dumps a &lt;em&gt;live mid-write window&lt;/em&gt; — the exact bytes the tailer saw mid-poll — to a file that replays deterministically offline.&lt;/p&gt;

&lt;p&gt;The offline simulation immediately proved the sealed decode and the dedup path were both correct on any static snapshot. And the live trace showed something spectacular: the same &lt;code&gt;id=1000&lt;/code&gt; row appearing poll after poll with a &lt;strong&gt;different, increasing commit SCN each time&lt;/strong&gt; — &lt;code&gt;22087937 → 22089678 → 22090175 → 22090911&lt;/code&gt;. One row, four commit timestamps. A transaction whose commit kept moving.&lt;/p&gt;

&lt;p&gt;New theory, stated with great confidence: when a held-open transaction's own commit record is still beyond the write head, the assembler mis-attributes its commit SCN to the latest visible commit. The row is right, the metadata is wrong, the floor prunes it. Q.E.D.&lt;/p&gt;

&lt;p&gt;It's a beautiful theory. It survived exactly eighty-four minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  23:16 — What the NOTE column said
&lt;/h2&gt;

&lt;p&gt;The test table had a &lt;code&gt;NOTE&lt;/code&gt; column I'd been ignoring — each test run writes a unique random marker into it. I finally dumped it for those four "re-attributed" rows:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;r1854822674_long&lt;/code&gt;, &lt;code&gt;r878629041_long&lt;/code&gt;, &lt;code&gt;r348217387_long&lt;/code&gt;, &lt;code&gt;X3647_long&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Four &lt;em&gt;different&lt;/em&gt; markers. Four different &lt;strong&gt;runs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Oracle's online redo logs are circular — a "new" log is a recycled one, still holding valid-looking blocks from previous incarnations. Every run of my test did &lt;code&gt;DROP TABLE … PURGE&lt;/code&gt;, &lt;code&gt;CREATE&lt;/code&gt;, insert &lt;code&gt;id=1000&lt;/code&gt;. So four runs' worth of stale &lt;code&gt;id=1000&lt;/code&gt; rows were sitting in the log, landed in one decode window, and read as "one row whose commit SCN keeps changing." There was no re-attribution. There was no moving commit. &lt;strong&gt;The centerpiece evidence of Theory #6 was four different transactions wearing the same primary key.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And with the ghosts exorcised, the live trace of the &lt;em&gt;current&lt;/em&gt; run finally showed the actual smoking gun, small and undramatic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;held-open txn:  commit_scn = 22103071   ← correct, its own, real
dedup floor:                 22103185   ← already past it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A transaction held open while others commit gets its commit record written &lt;em&gt;after&lt;/em&gt; theirs — so it becomes &lt;strong&gt;readable last with an SCN from the past&lt;/strong&gt;. It commits earlier in SCN time, later in file time. By the time its commit surfaced in the window, the scalar floor — advanced by the younger transactions that beat it into the file — had already rolled past it, and the dedup pruned it as "already seen."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rqkiefowh8beezt2yax.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5rqkiefowh8beezt2yax.png" alt="The same four commits in two orders: by SCN, the held-open transaction has the lowest number (committed first); by read order it arrives last, after the dedup floor has already climbed past it to 22103185, so its commit at 22103071 is pruned as already-seen" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Theory #1. From 20:03. The one whose correct fix I wrote into my first commit message and then abandoned because a dirty repro made better theories look true. The bug was never in the decode, never in the framing, never a wrong byte, never a mis-attribution. It was the floor, all along, exactly as first suspected — only now &lt;em&gt;proven&lt;/em&gt;, with a trace that distinguished this run's transaction from its four dead ancestors.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Dedup by &lt;strong&gt;transaction identity&lt;/strong&gt;, not by a scalar SCN watermark. The tailer now tracks &lt;code&gt;emitted: HashMap&amp;lt;TxId, commit_scn&amp;gt;&lt;/code&gt;, self-bounded each poll to transactions still decodable in the window (anything that aged out of the circular log can never reappear, so it needn't be remembered). A transaction is emitted iff its commit &lt;code&gt;TxId&lt;/code&gt; hasn't been — so an out-of-SCN-order held-open commit can never be pruned by younger neighbors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nn"&gt;CaptureMessage&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Commit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;tx_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;already&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;emitted_txns&lt;/span&gt;&lt;span class="nf"&gt;.contains_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// identity, not an SCN floor&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;already&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;emitted_txns&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx_id&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;scn&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="nf"&gt;.append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="nf"&gt;.clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The regression test encodes the whole evening in fourteen lines: poll 1 emits a transaction committed at SCN 200; poll 2 surfaces a held-open commit at SCN &lt;strong&gt;150&lt;/strong&gt; — &lt;em&gt;below&lt;/em&gt; the highest already emitted — and asserts it &lt;strong&gt;must&lt;/strong&gt; still come out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;out2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;select_new_messages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;poll2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;txns&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;ddl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;commit_scns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;out2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s"&gt;"held-open commit (150) emitted though it is below the already-emitted 200"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The long-transaction gate flipped from KNOWN-FAIL to a mandatory passing gate: 2/2, three consecutive runs, on a live containerized Oracle 23ai. 145 unit tests green, p95 poll latency 248 ms — the fix costs nothing measurable.&lt;/p&gt;

&lt;p&gt;And the offline window-replay tool, built at 21:52 to chase a bug that turned out not to exist? Its &lt;em&gt;inability&lt;/em&gt; to reproduce the drop on any static snapshot was the strongest clue that the decode was innocent. Sometimes the most useful thing a tool can tell you is &lt;em&gt;"not here."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3jktallua1xbe6qi3z6g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3jktallua1xbe6qi3z6g.png" alt="Timeline of the evening: theory #1 at 20:03 (right, but dismissed), four wrong theories and one hex-error pivot in between, and at 23:16 the fix — which was theory #1 all along, with a return arrow looping back to the start" width="800" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I keep from that evening
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The log remembers your old experiments.&lt;/strong&gt; Circular buffers, recycled logs, reused primary keys — any repro that can't distinguish &lt;em&gt;this run&lt;/em&gt; from &lt;em&gt;previous runs&lt;/em&gt; will eventually hand you fiction. One unique marker per run turned three hours of ghost-hunting into one &lt;code&gt;SELECT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give your first theory the same rigor as your last.&lt;/strong&gt; I wrote the correct diagnosis &lt;em&gt;and the correct fix&lt;/em&gt; in the first commit — then abandoned them because contaminated evidence made a scarier theory look stronger. Theories aren't refuted by newer theories; they're refuted by clean evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When rebuild-and-pray fails twice, stop and build a replay harness.&lt;/strong&gt; The serializer + offline simulator took one focused hour and ended a hunt that guessing would have extended indefinitely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the wrong turns down.&lt;/strong&gt; Every one of these quotes is a real commit message from that evening, wrong arithmetic and all. That paper trail is why I can tell you this story — and why the post-mortem took ten minutes instead of a week.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The transaction that committed in the past ships on time now. Its commit SCN is lower than its neighbors'; its identity is its own; and there's a HashMap that will never forget it — for exactly one window, and not a poll longer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's the longest you've chased a bug that turned out to be your test harness lying to you? I want to hear the war story in the comments.&lt;/strong&gt; 👇&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is one of my two Summer Bug Smash entries. The other is a Clear the Lineup fix — &lt;a href="https://dev.to/uptimearchitect/zulip-logged-successfully-processed-your-reply-was-already-gone-4727"&gt;a year-old Zulip bug that six contributors tried before me&lt;/a&gt; — where the hard part wasn't the code, it was finishing what everyone else started.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written up from that evening's git history (2026-06-18, 20:03–23:16). The engine is a from-scratch Rust CDC system that decodes Oracle redo logs off-database; this real-time path was already live in production, which is exactly what made a silent, deniable transaction drop worth three hours of hunting.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>debugging</category>
      <category>rust</category>
    </item>
    <item>
      <title>Zulip logged 'Successfully processed.' Your reply was already gone.</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Sat, 01 Aug 2026 13:22:01 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/zulip-logged-successfully-processed-your-reply-was-already-gone-4727</link>
      <guid>https://dev.to/uptimearchitect/zulip-logged-successfully-processed-your-reply-was-already-gone-4727</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Clear the Lineup&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You reply to an email notification from your team chat — a quick "sounds good" to a thread you missed. It never arrives. No bounce, no error, no red anything. And the server's own logs report, cheerfully, &lt;em&gt;"Successfully processed email."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That reply had already been thrown away. And it kept happening for a full year — in a tool thousands of organizations trust — while six people tried and failed to fix it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — Replying by email to a group-DM notification silently dropped your message whenever a group member had been deactivated (while logging success). The fix is ~15 lines; the PR is &lt;a href="https://github.com/zulip/zulip/pull/39766" rel="noopener noreferrer"&gt;#39766&lt;/a&gt;, green across Zulip's CI matrix. Sentry's Seer independently reconstructed my diagnosis from one captured event, and Gemini stress-tested my test coverage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;The feature at the center of this is &lt;a href="https://github.com/zulip/zulip" rel="noopener noreferrer"&gt;Zulip&lt;/a&gt;'s &lt;strong&gt;email gateway&lt;/strong&gt; — Zulip being the open-source team chat thousands of organizations run themselves. Miss a message, get a notification email, and just &lt;em&gt;reply to it&lt;/em&gt;: your reply is parsed and dropped straight back into the conversation. Brilliant — until the conversation is a group DM and one of its members has since been deactivated. Then your reply vanishes, and the log lies about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug Fix or Performance Improvement
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/zulip/zulip/issues/35273" rel="noopener noreferrer"&gt;Issue #35273&lt;/a&gt;&lt;/strong&gt;, opened by a Zulip core maintainer in July 2025 — one year old this week. When someone replies by email to a group-DM notification and any member of that group has been deactivated, message validation throws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;django&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;&lt;span class="s"&gt;deactivated-recipient@example.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; is no longer using Zulip.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;zerver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JsonableError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deactivated-recipient@example.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;no&lt;/span&gt; &lt;span class="n"&gt;longer&lt;/span&gt; &lt;span class="n"&gt;using&lt;/span&gt; &lt;span class="n"&gt;Zulip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The exception is caught deep in the internal send path and logged — so nothing crashes, nothing retries, and the sender is never told. The reply is simply &lt;strong&gt;gone&lt;/strong&gt;. And here's the lie from the intro — my favorite detail of the whole bug. Right after eating your message, the mirror logs&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO Successfully processed email from user 8 to ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A success log for a message that was never delivered. That's the worst class of bug there is: &lt;strong&gt;silent data loss that every party — sender, server, and logs — believes succeeded.&lt;/strong&gt; Nobody files a report for a message they don't know they lost, which is exactly why it hid in a shipping product for a year.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7x0appkegfj19lfz2iz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn7x0appkegfj19lfz2iz.png" alt="Before/after: the unfixed path builds a recipient list including the deactivated user, validation rejects it, the JsonableError is swallowed, " width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The web app can't hit this — it refuses to compose to a group containing a deactivated user at all. Only the email path, replying to a notification that &lt;em&gt;predates&lt;/em&gt; the deactivation, can walk into it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bug, by the numbers:&lt;/strong&gt; open &lt;strong&gt;1 year&lt;/strong&gt; · &lt;strong&gt;6&lt;/strong&gt; contributors claimed it and drifted off · &lt;strong&gt;4&lt;/strong&gt; PRs opened, &lt;strong&gt;0&lt;/strong&gt; merged · the fix is &lt;strong&gt;~15 lines of logic&lt;/strong&gt; + &lt;strong&gt;3 new tests&lt;/strong&gt; · &lt;strong&gt;75/75&lt;/strong&gt; email-mirror tests green · CI green on &lt;strong&gt;5&lt;/strong&gt; Python versions (3.10–3.14).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Behind those numbers is a pattern.&lt;/strong&gt; Every one of the six claimants was auto-unassigned for inactivity; none of the four PRs cleared review. The most promising got a "Looks great!" from a maintainer with a short punch-list — use &lt;code&gt;.values_list("email", flat=True)&lt;/code&gt;, add a test where the conversation &lt;em&gt;remains&lt;/em&gt; a group DM, extract the duplicated test setup into a helper, and also handle the 1:1 DM case — and then stalled in commit-style churn. So before writing a line, I read all four attempts and that review, and treated the punch-list as my spec.&lt;/p&gt;

&lt;p&gt;The reading paid off with the key insight: &lt;strong&gt;the "1:1 case" no longer exists.&lt;/strong&gt; Since the earlier attempts, Zulip finished migrating &lt;em&gt;all&lt;/em&gt; direct messages — including 1:1s — to direct-message groups and dropped personal recipient rows entirely. The separate 1:1 fix the reviewer asked for in January is now structurally impossible &lt;em&gt;and&lt;/em&gt; structurally unnecessary: one fix in the group branch covers everything.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag_github-liquid-tag"&gt;
  &lt;h1&gt;
    &lt;a href="https://github.com/zulip/zulip/pull/39766" rel="noopener noreferrer"&gt;
      &lt;img class="github-logo" alt="GitHub logo" src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg"&gt;
      &lt;span class="issue-title"&gt;
        [ai] email_mirror: Ignore deactivated users in group DM email replies.
      &lt;/span&gt;
      &lt;span class="issue-number"&gt;#39766&lt;/span&gt;
    &lt;/a&gt;
  &lt;/h1&gt;
  &lt;div class="github-thread"&gt;
    &lt;div class="timeline-comment-header"&gt;
      &lt;a href="https://github.com/pyaroslav" rel="noopener noreferrer"&gt;
        &lt;img class="github-liquid-tag-img" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Favatars.githubusercontent.com%2Fu%2F20116942%3Fv%3D4" alt="pyaroslav avatar"&gt;
      &lt;/a&gt;
      &lt;div class="timeline-comment-header-text"&gt;
        &lt;strong&gt;
          &lt;a href="https://github.com/pyaroslav" rel="noopener noreferrer"&gt;pyaroslav&lt;/a&gt;
        &lt;/strong&gt; posted on &lt;a href="https://github.com/zulip/zulip/pull/39766" rel="noopener noreferrer"&gt;&lt;time&gt;Jul 19, 2026&lt;/time&gt;&lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag-github-body"&gt;
      &lt;p&gt;Fixes: #35273&lt;/p&gt;
&lt;p&gt;Replying by email to a message notification email for a group direct message raised an unhandled &lt;code&gt;JsonableError&lt;/code&gt; when any member of the group had since been deactivated, and the reply was silently lost (while the mirror still logged "Successfully processed email").&lt;/p&gt;
&lt;p&gt;This filters the group's members to active users when constructing the recipient list, so the reply is delivered to the remaining active participants — consistent with the web app, which does not offer composing to such a group at all. If nobody but the sender remains active, the reply is dropped with an INFO log rather than an exception.&lt;/p&gt;
&lt;p&gt;Two notes for review:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Since personal &lt;code&gt;Recipient&lt;/code&gt; rows no longer exist (1:1 direct messages are direct message groups), the same branch covers the 1:1 case raised in the review of #37449; a separate commit for it is no longer meaningful.&lt;/li&gt;
&lt;li&gt;The filtering query adds one database query to the reply path, so the query-count assertions in the two existing tests are updated from 22 to 23.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A possible follow-up (not included, to keep this scoped like the previously reviewed approach): notifying the sender via the notification bot when the reply is dropped, as &lt;code&gt;send_mm_reply_to_stream&lt;/code&gt; does for channel replies.&lt;/p&gt;
&lt;p&gt;Prior work on this issue: #36882, #37449 (whose review feedback is addressed here: &lt;code&gt;values_list&lt;/code&gt;-based filtering, a test where the conversation remains a group DM, and the repeated test setup extracted into helpers), #38117, #38933.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How changes were tested:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[x] &lt;code&gt;./tools/test-backend zerver.tests.test_email_mirror.TestMissedMessageEmailMessages&lt;/code&gt; — 12/12 pass (re-run after rebasing on current main); the three new tests fail without the lib change (silent message loss, no drop log).&lt;/li&gt;
&lt;li&gt;[x] &lt;code&gt;./tools/lint zerver/lib/email_mirror.py zerver/tests/test_email_mirror.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;[x] Coverage: &lt;code&gt;zerver/lib/email_mirror.py&lt;/code&gt; shows no uncovered lines under this test class.&lt;/li&gt;
&lt;li&gt;[x] Reproduced end-to-end in the dev environment (group DM → deactivate a member → email reply): before the fix the reply is lost; after, it is delivered to the remaining active members.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Screenshots and screen captures:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;N/A (backend-only change).&lt;/p&gt;

Self-review checklist
&lt;ul&gt;
&lt;li&gt;[x] Self-reviewed the changes for clarity and maintainability
(variable names, code reuse, readability, etc.).&lt;/li&gt;
&lt;li&gt;[x] Followed the &lt;a href="https://zulip.readthedocs.io/en/latest/contributing/contributing.html#ai-use-policy-and-guidelines" rel="nofollow noopener noreferrer"&gt;AI use policy&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Communicate decisions, questions, and potential concerns.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[x] Explains differences from previous plans (e.g., issue description).&lt;/li&gt;
&lt;li&gt;[x] Highlights technical choices and bugs encountered.&lt;/li&gt;
&lt;li&gt;[x] Calls out remaining decisions and concerns.&lt;/li&gt;
&lt;li&gt;[x] Automated tests verify logic where appropriate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Individual commits are ready for review (see &lt;a href="https://zulip.readthedocs.io/en/latest/contributing/commit-discipline.html" rel="nofollow noopener noreferrer"&gt;commit discipline&lt;/a&gt;).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[x] Each commit is a coherent idea.&lt;/li&gt;
&lt;li&gt;[x] Commit message(s) explain reasoning and motivation for changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Completed manual review and testing of the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[ ] Visual appearance of the changes.&lt;/li&gt;
&lt;li&gt;[ ] Responsiveness and internationalization.&lt;/li&gt;
&lt;li&gt;[ ] Strings and tooltips.&lt;/li&gt;
&lt;li&gt;[x] End-to-end functionality of the changes.&lt;/li&gt;
&lt;li&gt;[ ] Accessibility.&lt;/li&gt;
&lt;li&gt;[ ] Theming.&lt;/li&gt;
&lt;/ul&gt;


    &lt;/div&gt;
    &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/zulip/zulip/pull/39766" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;The whole change is about two dozen lines in &lt;code&gt;zerver/lib/email_mirror.py&lt;/code&gt; (roughly half of them comments): drop the group's &lt;strong&gt;deactivated&lt;/strong&gt; members before constructing the recipient list, deliver to whoever remains, and if nobody but the sender is left, log and drop instead of throwing. Here's the logic, comments elided:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Recipient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DIRECT_MESSAGE_GROUP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;display_recipient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_display_recipient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recipient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;user_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;user_dict&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;display_recipient&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;emails&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;UserProfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id__in&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_active&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_mirror_dummy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dropping message notification email reply from user %s to a group direct message with no active recipients&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;recipient_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;internal_send_group_direct_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;realm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;emails&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two subtleties in that query. The &lt;code&gt;.exclude(is_active=False, is_mirror_dummy=False)&lt;/code&gt; drops deactivated members but &lt;em&gt;keeps&lt;/em&gt; "mirror-dummy" users — inactive placeholder accounts that Zulip's own recipient validation still treats as valid recipients. A naive &lt;code&gt;is_active=True&lt;/code&gt; filter gets that wrong and silently drops them; a reviewer caught exactly that, which is why the query mirrors the validation's rule instead. And &lt;code&gt;.order_by("id")&lt;/code&gt; preserves the deterministic ordering &lt;code&gt;get_display_recipient&lt;/code&gt; provides, since we no longer iterate it directly.&lt;/p&gt;

&lt;p&gt;Plus three new backend tests (delivered-and-stays-a-group-DM, keeps-a-mirror-dummy-member, only-sender-remains-so-drop) and the test-helper refactor the reviewer asked for — extracted into its own preparatory commit, per Zulip's commit discipline. The filtering adds exactly one indexed query, so the two existing tests' query-count assertions moved from 22 to 23 — stated in the PR rather than hidden.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Improvements
&lt;/h2&gt;

&lt;p&gt;The discipline was: make the deactivation tests fail on &lt;code&gt;main&lt;/code&gt; for the &lt;em&gt;right reasons&lt;/em&gt; before making them pass (the third test guards the opposite direction — that a valid inactive "mirror-dummy" member is &lt;em&gt;kept&lt;/em&gt;, not over-filtered). On unfixed code they fail exactly as production does — recipients' latest message is still the &lt;em&gt;original&lt;/em&gt; group DM (the reply was eaten), and the drop-path test finds the lying "Successfully processed" log where the graceful drop should be:&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="p"&gt;AssertionError: Actual and expected outputs do not match; showing diff.
&lt;/span&gt;&lt;span class="gd"&gt;- original group direct message
&lt;/span&gt;&lt;span class="gi"&gt;+ Reply with deactivated group member body
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the fix: the whole &lt;code&gt;test_email_mirror&lt;/code&gt; module is green (75/75, my three new tests included), &lt;code&gt;./tools/lint&lt;/code&gt; (including mypy) clean, and coverage shows &lt;strong&gt;zero uncovered lines&lt;/strong&gt; in &lt;code&gt;email_mirror.py&lt;/code&gt; under this suite — including the drop path.&lt;/p&gt;

&lt;p&gt;Design choice worth surfacing: when members are filtered out, the reply is delivered to the &lt;em&gt;reduced&lt;/em&gt; group (the active members' conversation), matching what the previously-reviewed attempt did and what the maintainers endorsed in-thread. And when nobody but the sender remains, we log-and-drop rather than crash; a follow-up could notify the sender via Zulip's notification bot, the way stream-reply failures already do — I flagged that in the PR rather than scope-creeping into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Sentry
&lt;/h2&gt;

&lt;p&gt;Zulip ships Sentry integration natively (&lt;code&gt;zproject/sentry.py&lt;/code&gt;) — enabling it in the dev environment took exactly one environment variable, which made a live before/after demo of this bug almost embarrassingly easy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1q92ajlwwqz7ye7n2qs6.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1q92ajlwwqz7ye7n2qs6.gif" alt="Live: firing the crash in a terminal, then the JsonableError surfacing in Sentry, the traceback pinning the exact except→raise line, and Seer reconstructing the same root cause" width="480" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole loop, start to finish; the detail on each step is below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt; I ran the real scenario end-to-end — group DM, deactivate a member, reply by email through &lt;code&gt;process_message()&lt;/code&gt; — with the unfixed code and a Sentry DSN. The swallowed &lt;code&gt;JsonableError&lt;/code&gt; arrives in Sentry as a first-class error event with the full traceback from the issue:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgixwb0nmbw40g7tdjjv6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgixwb0nmbw40g7tdjjv6.png" alt="The captured event's stack trace, showing the chained ValidationError → JsonableError raised at message_send.py where the ValidationError is re-raised" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The trace pins it exactly: &lt;code&gt;except ValidationError as e: … raise JsonableError(e.messages[0])&lt;/code&gt; in &lt;code&gt;check_message&lt;/code&gt; — the reply is turned into an exception, caught upstream, logged, and dropped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seer:&lt;/strong&gt; I connected my Zulip fork and pointed Sentry's &lt;strong&gt;Seer&lt;/strong&gt; at the captured event. Its root-cause analysis independently landed on the same mechanism I'd fixed:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faotr2uw1lcpxvwzh1y8g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Faotr2uw1lcpxvwzh1y8g.png" alt="Seer's root-cause analysis: the email reply handler calls _internal_prep_message → check_message with allow_deactivated=False, the recipient list still includes the deactivated user, raising JsonableError and silently dropping the reply — plus reproduction steps" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The recipient list for the conversation still includes the deactivated user11, causing a &lt;code&gt;JsonableError&lt;/code&gt; and silently dropping the email reply."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's exactly the bug — and Seer reconstructed the reproduction steps (create group DM → deactivate a member → reply by email) matching the ones I'd used, from a single captured event. A nice independent check on the diagnosis before shipping the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; identical scenario, fixed code — the reply is delivered to the remaining active members, the success log now lists only the active recipients, no new event is captured, and I marked the issue resolved (you can watch the whole before→after in the recording above).&lt;/p&gt;

&lt;p&gt;What sold me on the workflow: this bug's signature — &lt;em&gt;error event + success log + missing message&lt;/em&gt; — is invisible in any one log file, but obvious the moment the exception becomes a first-class object in Sentry with grouping and history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Use of Google AI
&lt;/h2&gt;

&lt;p&gt;Green tests are a trap: they prove the cases you &lt;em&gt;wrote&lt;/em&gt; pass, not the cases you forgot. So I handed &lt;strong&gt;Gemini 2.5 Flash&lt;/strong&gt; (via the Google AI Studio API) the exact patch and my three test descriptions and asked the one question I can't ask myself objectively — &lt;em&gt;what am I not testing?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It went straight for the boundary that worried me most, the &lt;code&gt;len(emails) &amp;lt;= 1&lt;/code&gt; drop path, and reasoned it out instead of hand-waving:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sender is the only active member in a group DM&lt;/strong&gt; — &lt;em&gt;Correctly dropped and logged. &lt;code&gt;len(emails) &amp;lt;= 1&lt;/code&gt; correctly handles this, as the sender's email will be in &lt;code&gt;emails&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the load-bearing assumption of the whole drop branch — the sender is always in the list, so "nobody left" means length 1, not 0. Then it flagged the &lt;code&gt;email&lt;/code&gt; vs &lt;code&gt;delivery_email&lt;/code&gt; question under restricted email-visibility — the &lt;em&gt;exact&lt;/em&gt; trap I'd already hit building the repro, where the send path resolves recipients by &lt;code&gt;.email&lt;/code&gt;, not &lt;code&gt;.delivery_email&lt;/code&gt; — and correctly scoped it to the downstream function rather than crying wolf on my diff. It also named two recipient shapes my tests don't exercise (bot members, cross-realm members) and reasoned both already-correct, which is why I kept the PR scoped to the deactivation case instead of expanding it.&lt;/p&gt;

&lt;p&gt;No bug fell out — and on a year-old issue that had already beaten six people, that was the win. Gemini bought me the thing you skip when the suite is green: a fast, adversarial second read that turns &lt;em&gt;"my tests pass"&lt;/em&gt; into &lt;em&gt;"I know exactly which cases I'm not covering, and why they're safe."&lt;/em&gt; The full prompt and response are saved alongside the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Six people had the right instinct on this issue; what was missing wasn't cleverness, it was finishing the job — reading the one review that already said exactly what to do, and acting on all of it. If you're hunting for a first open-source contribution, the highest-leverage move is often to pick up the issue everyone &lt;em&gt;started&lt;/em&gt; and nobody &lt;em&gt;closed&lt;/em&gt;. The trail of dead PRs isn't a warning sign — it's a spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you ever shipped (or inherited) a "Successfully processed" log line sitting right on top of a dropped message? Tell me the worst one in the comments.&lt;/strong&gt; 👇&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is one of my two Summer Bug Smash entries. The other is a Smash Stories deep-dive — &lt;a href="https://dev.to/uptimearchitect/the-transaction-that-committed-in-the-past-how-i-diagnosed-one-bug-six-times-in-an-evening-gli"&gt;the data-loss bug I diagnosed six times in one evening&lt;/a&gt; — different chaos, same lesson about trusting your evidence over your theories.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>OCI vs Oracle Database@Azure: Where Should Your Oracle Database Live?</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Sun, 26 Jul 2026 20:36:40 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/oci-vs-oracle-databaseazure-where-should-your-oracle-database-live-3190</link>
      <guid>https://dev.to/uptimearchitect/oci-vs-oracle-databaseazure-where-should-your-oracle-database-live-3190</guid>
      <description>&lt;p&gt;You've decided the database is going to the cloud, and you've &lt;a href="https://uptimearchitect.com/blog/oracle-cloud-migration-methods/" rel="noopener noreferrer"&gt;picked a migration method&lt;/a&gt;. Now comes the question that determines everything downstream — networking, contracts, operations, and what your 3am looks like: &lt;strong&gt;where does the database actually live?&lt;/strong&gt; For most Oracle estates in 2026 that's a choice between &lt;strong&gt;OCI native&lt;/strong&gt; and &lt;strong&gt;Oracle Database@Azure&lt;/strong&gt; — and a third option people keep forgetting, the &lt;strong&gt;OCI–Azure Interconnect&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The wrong way to decide is by database features, because that's the trick of it: &lt;strong&gt;the database is the same in all three&lt;/strong&gt;. Same Exadata, same 19c/26ai, same Data Guard, same RMAN. What changes is everything &lt;em&gt;around&lt;/em&gt; the database — and that's where the decision really lives.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version.&lt;/strong&gt; &lt;strong&gt;Database@Azure&lt;/strong&gt; puts Oracle-operated Exadata hardware &lt;em&gt;physically inside Azure datacenters&lt;/em&gt;: you provision from the Azure portal, latency to your Azure apps is same-building, and the spend burns down your &lt;strong&gt;Azure commitment (MACC)&lt;/strong&gt;. &lt;strong&gt;OCI native&lt;/strong&gt; gives you the full Oracle cloud — every service, typically the lowest total cost — and is the obvious home when your apps aren't married to Azure. The &lt;strong&gt;Interconnect&lt;/strong&gt; is the middle path: database in OCI, apps in Azure, &lt;strong&gt;under 2ms&lt;/strong&gt; between them with no egress charges over the link. Decide by three things: &lt;strong&gt;where your apps run, who holds your commercial commitments, and how much latency the app-to-database path can afford.&lt;/strong&gt; And the same template now works for AWS and Google Cloud — Database@AWS and Database@Google Cloud are the identical play on different hosts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Three options, not two
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. OCI native.&lt;/strong&gt; The database runs in Oracle's cloud — Exadata Database Service, Autonomous Database, Base Database, or plain compute — alongside the rest of OCI's catalog. One console, one bill, Oracle's infrastructure pricing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Oracle Database@Azure.&lt;/strong&gt; Oracle ships Exadata racks into Microsoft's datacenters and operates them there. You buy it in the &lt;strong&gt;Azure Marketplace&lt;/strong&gt;, provision it from the &lt;strong&gt;Azure portal&lt;/strong&gt;, and your apps in the same Azure region talk to it at in-datacenter latency. It is genuinely OCI — a slice of Oracle's cloud embedded in Azure — operated by Oracle's own OCI team, with support handled jointly by Oracle and Microsoft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The OCI–Azure Interconnect.&lt;/strong&gt; The database stays in OCI; your Azure workloads reach it over a private, pre-built link between paired OCI and Azure regions — &lt;strong&gt;a dozen region pairs, sub-2ms round-trip, and no ingress/egress charges over the link&lt;/strong&gt;. (You may also find references to &lt;em&gt;Oracle Database Service for Azure&lt;/em&gt; (ODSA), the 2022 portal-sync service built on this link — for new projects it's been superseded in practice by &lt;a href="mailto:Database@Azure"&gt;Database@Azure&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2z4df6y9fpjg6ron089.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz2z4df6y9fpjg6ron089.png" alt="Choosing where an Oracle database lives. Follow app gravity first, then latency, then commercials — the database itself is the same everywhere." width="800" height="818"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Choosing where an Oracle database lives. Follow app gravity first, then latency, then commercials — the database itself is the same everywhere.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Database@Azure actually is
&lt;/h2&gt;

&lt;p&gt;The name undersells it. This isn't a connector or a managed VM — it's &lt;strong&gt;OCI hardware in Azure buildings&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The services&lt;/strong&gt;: Exadata Database Service on &lt;strong&gt;Dedicated Infrastructure&lt;/strong&gt; and on &lt;strong&gt;Exascale Infrastructure&lt;/strong&gt;, &lt;strong&gt;Autonomous Database&lt;/strong&gt; (Serverless and Dedicated), &lt;strong&gt;Base Database Service&lt;/strong&gt;, plus &lt;strong&gt;GoldenGate&lt;/strong&gt; and the &lt;strong&gt;Zero Data Loss Autonomous Recovery Service&lt;/strong&gt;. Oracle Database 19c and the current &lt;strong&gt;26ai&lt;/strong&gt; are supported — 26ai being 23ai renamed (a single Release Update makes the jump, no upgrade or recertification), which is also why you'll increasingly see the branding &lt;em&gt;Oracle AI Database@Azure&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The footprint&lt;/strong&gt;: available in &lt;strong&gt;33 Azure regions&lt;/strong&gt; and climbing — it more than doubled in the last year.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The commercial model&lt;/strong&gt;: purchased through the &lt;strong&gt;Azure Marketplace&lt;/strong&gt;; consumption counts &lt;strong&gt;1:1 toward your Microsoft Azure Consumption Commitment (MACC)&lt;/strong&gt;. Your existing Oracle licenses apply — &lt;strong&gt;BYOL or license-included&lt;/strong&gt;, same as OCI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The operating model&lt;/strong&gt;: Oracle's OCI operations team runs the hardware, patching, and infrastructure; Microsoft runs the datacenter; you get &lt;strong&gt;Azure-portal provisioning&lt;/strong&gt; with &lt;strong&gt;Entra ID&lt;/strong&gt; identity and Azure-native monitoring hooks, while database internals still surface through an OCI console linked to your Azure tenancy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is the honest fine print: &lt;strong&gt;day-2 operations span two control planes.&lt;/strong&gt; Provisioning, networking, and billing feel like Azure; the deep database machinery is still OCI underneath. Teams that expect a purely Azure experience find the seam; teams that know OCI find it familiar.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision factors
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;OCI native&lt;/th&gt;
&lt;th&gt;Database@Azure&lt;/th&gt;
&lt;th&gt;Interconnect (DB in OCI)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App-to-DB latency&lt;/td&gt;
&lt;td&gt;in-OCI&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;same datacenter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&amp;lt; 2ms&lt;/strong&gt; between paired regions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buys down Azure MACC&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Yes — 1:1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (OCI spend)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oracle license BYOL&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service breadth&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Full OCI catalog&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Core DB services + GoldenGate + ZRCV&lt;/td&gt;
&lt;td&gt;Full OCI catalog&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Regions&lt;/td&gt;
&lt;td&gt;OCI's global footprint&lt;/td&gt;
&lt;td&gt;33 Azure regions&lt;/td&gt;
&lt;td&gt;12 paired regions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consoles&lt;/td&gt;
&lt;td&gt;OCI only&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Azure portal + OCI&lt;/strong&gt; (split)&lt;/td&gt;
&lt;td&gt;Azure + OCI (two estates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical total cost&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Lowest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Parity on DB, Azure prices around it&lt;/td&gt;
&lt;td&gt;Low DB cost + free link egress&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;App gravity beats everything.&lt;/strong&gt; If the applications, integration layer, identity, and the team's tooling live in Azure — and the CIO's strategy says they stay there — the database follows the apps. Fighting app gravity with a cheaper database quote is how you end up owning a latency problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commercials decide more of this than architects like to admit.&lt;/strong&gt; A large enterprise with a MACC to burn and an Azure-first mandate has an easy business case for Database@Azure: the same Oracle spend now counts against the Microsoft commitment. Conversely, if you're not carrying Azure commitments, OCI native is usually the cheaper estate — Oracle's compute, storage, and egress pricing undercut the equivalent hyperscaler line items, and that's before you price the second cloud's networking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency is the tiebreaker, not the headline.&lt;/strong&gt; Sub-2ms over the Interconnect is fine for most applications — batch, reporting, services with sane call patterns. Where it isn't fine is chatty OLTP: an app that makes hundreds of sequential database round-trips per user action multiplies every one of those milliseconds. Measure your app's round-trip count before you let anyone tell you 2ms is nothing — &lt;a href="https://uptimearchitect.com/blog/oracle-wait-events-decoded/" rel="noopener noreferrer"&gt;wait events don't lie&lt;/a&gt;, and &lt;code&gt;SQL*Net message from client&lt;/code&gt; on the database side of a chatty app is exactly where this shows up. (You can &lt;a href="https://github.com/pyaroslav/oracle-labs/tree/main/latency" rel="noopener noreferrer"&gt;measure this multiplication yourself&lt;/a&gt; — see the lab below.)&lt;/p&gt;

&lt;h2&gt;
  
  
  When each one wins
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;OCI native wins&lt;/strong&gt; when you're not anchored to Azure: greenfield builds, estates consolidating on Oracle's stack, Exadata Cloud@Customer hybrids, cost-driven migrations, and anywhere you want the whole OCI catalog next to the database. It's also the simplest &lt;em&gt;operationally&lt;/em&gt; — one console, one support path, no seam.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database@Azure wins&lt;/strong&gt; when the application estate is committed to Azure and the app-to-database path matters: same-datacenter latency, MACC burn-down, Azure-portal provisioning, Entra ID, and Exadata/Autonomous without leaving the building. It's the answer that lets an Azure-first enterprise run &lt;em&gt;real&lt;/em&gt; Oracle — &lt;a href="https://uptimearchitect.com/blog/oracle-cloud-migration-methods/" rel="noopener noreferrer"&gt;not a compromise port&lt;/a&gt; — and every migration method works against it, because it's Exadata under the hood.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Interconnect wins&lt;/strong&gt; when you want OCI's pricing and catalog &lt;em&gt;and&lt;/em&gt; Azure apps, and the app can tolerate single-digit milliseconds. Database in OCI, apps in Azure, private link, no egress fees over it. This is also the natural &lt;strong&gt;DR pattern&lt;/strong&gt;: primary in one cloud, &lt;a href="https://uptimearchitect.com/blog/oracle-data-guard-switchover-vs-failover/" rel="noopener noreferrer"&gt;Data Guard standby&lt;/a&gt; across the link in the other — a cross-cloud failover story that doesn't require moving the estate.&lt;/p&gt;

&lt;p&gt;And if your apps live on AWS or Google Cloud: the identical decision now exists there. &lt;strong&gt;Database@AWS&lt;/strong&gt; and &lt;strong&gt;Database@Google Cloud&lt;/strong&gt; are both generally available — Oracle-operated Exadata inside the other hyperscalers' datacenters — and AWS's own &lt;strong&gt;AWS Interconnect&lt;/strong&gt; added OCI connectivity in preview in 2026. Swap the vendor names and this whole article still applies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What teams get wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deciding on database features.&lt;/strong&gt; The database is identical in all three options. The decision is about apps, contracts, latency, and operations — never about the engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring the split control plane.&lt;/strong&gt; Database@Azure provisioning is Azure; deep database operations are OCI. Budget for your team learning both, or the first incident will do the teaching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating 2ms as free.&lt;/strong&gt; For chatty OLTP, interconnect latency compounds per round-trip. Profile the app's call pattern first; move the database into the same building only if the numbers say so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaving MACC on the table.&lt;/strong&gt; If the company has an Azure commitment, Database@Azure spend counts toward it 1:1 — finance may like the "more expensive" option better than your spreadsheet does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting the Interconnect exists.&lt;/strong&gt; Not every Azure app needs the database &lt;em&gt;in&lt;/em&gt; Azure. The middle path keeps OCI economics with Azure proximity — and nobody promotes it, because neither vendor's sales team owns it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming Azure ends the DR conversation.&lt;/strong&gt; Wherever the database lands, it still needs &lt;a href="https://uptimearchitect.com/blog/oracle-ha-decision-tree-rac-vs-data-guard/" rel="noopener noreferrer"&gt;a protection architecture&lt;/a&gt; — Data Guard across regions (or clouds), backups, and &lt;a href="https://uptimearchitect.com/blog/oracle-rman-recovery-runbook/" rel="noopener noreferrer"&gt;a tested restore&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Follow the apps.&lt;/strong&gt; If they're committed to Azure and the app-to-database path is chatty, put the database in the same building — &lt;strong&gt;Database@Azure&lt;/strong&gt;, which is real Oracle-operated Exadata inside Azure, bought through the Marketplace and burning down your MACC. If they're committed to Azure but the path tolerates a few milliseconds, consider the &lt;strong&gt;Interconnect&lt;/strong&gt; — database in OCI at OCI prices, sub-2ms away, no egress over the link. If the apps aren't anchored to Azure, &lt;strong&gt;OCI native&lt;/strong&gt; is the default: full catalog, one console, lowest total cost. The engine is identical everywhere — so decide on app gravity, commercial commitments, and latency, in that order. The same template now answers the AWS and Google Cloud versions of this question, too.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Measure it before you decide it.&lt;/strong&gt; The &lt;a href="https://github.com/pyaroslav/oracle-labs/tree/main/latency" rel="noopener noreferrer"&gt;&lt;code&gt;latency/&lt;/code&gt; lab&lt;/a&gt; runs this post's chatty-vs-batched experiment on your own machine: Oracle Database Free in Docker, with &lt;code&gt;tc netem&lt;/code&gt; injecting the 0ms / 2ms / 10ms of the three homes above. Same database, same rows — the chatty workload went from &lt;strong&gt;0.7s to 3.6s to 11.8s&lt;/strong&gt; as the latency rose, while the batched one barely moved (0.1s → 0.3s). The round-trip counts come from &lt;code&gt;v$mystat&lt;/code&gt;, and the run fails unless the elapsed times actually match the injected delay — so it only passes if the math is real. Chatty apps don't have a cloud problem; they have a round-trip problem that latency exposes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want to try the OCI side for free?&lt;/strong&gt; &lt;a href="https://uptimearchitect.com/blog/oracle-autonomous-database-oci-always-free/" rel="noopener noreferrer"&gt;Oracle Autonomous Database on OCI Always Free&lt;/a&gt; gives you a real, fully managed 26ai database — no license, no server, no cost — and it's the same Autonomous service that runs inside &lt;a href="mailto:Database@Azure"&gt;Database@Azure&lt;/a&gt;. Standing one up is the fastest way to feel how the OCI half of this decision operates before you commit an estate to it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is Oracle Database@Azure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Oracle Database@Azure is Oracle-operated database infrastructure — Exadata Database Service (Dedicated and Exascale), Autonomous Database (Serverless and Dedicated), Base Database Service, GoldenGate, and the Zero Data Loss Autonomous Recovery Service — running on Oracle hardware physically located inside Microsoft Azure datacenters. You purchase it through the Azure Marketplace, provision it from the Azure portal, and your Azure applications reach it at in-datacenter latency. It is available in 33 Azure regions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who operates Database@Azure — Microsoft or Oracle?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Oracle. The hardware is owned, patched, and operated by Oracle Cloud Infrastructure personnel; Microsoft provides the datacenter, the Azure-side networking, and the portal integration. Support is handled jointly, so you can raise issues through either vendor. Day to day you provision from the Azure portal, while deeper database operations surface through an OCI console linked to your Azure tenancy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does Database@Azure spend count toward my Azure commitment (MACC)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Oracle Database@Azure is purchased through the Azure Marketplace and consumption counts 1:1 toward a Microsoft Azure Consumption Commitment. For organizations carrying a large MACC, that often makes the business case by itself — the Oracle database spend reduces the Microsoft commitment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I bring my own Oracle licenses to Database@Azure or OCI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, both. Database@Azure and OCI database services support bring-your-own-license (BYOL) as well as license-included pricing, so existing Oracle Database licenses and support agreements carry over to either destination.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between Database@Azure and the OCI–Azure Interconnect?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Database@Azure puts the database physically inside the Azure datacenter — same-building latency, Azure Marketplace billing, MACC burn-down. The Interconnect keeps the database in OCI and links it privately to a paired Azure region at under 2 milliseconds round-trip with no ingress/egress charges over the link. The Interconnect preserves OCI pricing and the full OCI catalog; Database@Azure wins when the app-to-database path is latency-critical or the commercials favor Azure. The older Oracle Database Service for Azure (ODSA) portal service has been superseded in practice by Database@Azure for new projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Database@Azure more expensive than running in OCI?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The database services are priced comparably, but the estate around them usually is not: OCI compute, storage, and egress typically cost less than the Azure equivalents, so OCI native tends to have the lowest total cost. Database@Azure changes the calculation when a MACC is in play, because the spend burns down an existing Microsoft commitment — the accounting can favor it even when the sticker price does not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I migrate an existing Oracle database into Database@Azure?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The same way you would migrate to Exadata in OCI — because it is Exadata. All the standard methods apply: Data Pump for simple moves with an outage, Data Guard for near-zero-downtime same-endian migrations with easy rollback, transportable tablespaces with RMAN CONVERT for very large or cross-endian moves, GoldenGate when you are crossing versions or need a fallback, and ZDM to automate whichever fits. Autonomous targets accept logical methods only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do Database@AWS and Database@Google Cloud work the same way?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — both are generally available and follow the same model: Oracle-operated Exadata and Autonomous services physically inside AWS and Google Cloud datacenters, purchased through the respective marketplaces and counting toward those clouds' commitments. AWS's own AWS Interconnect service added OCI connectivity in preview in 2026. The decision framework — app gravity, commercial commitments, then latency — is identical; only the vendor names change.&lt;/p&gt;

&lt;p&gt;Wherever the database lands, the architecture questions that follow are the same ones this blog keeps coming back to: &lt;a href="https://uptimearchitect.com/blog/oracle-cloud-migration-methods/" rel="noopener noreferrer"&gt;which migration method&lt;/a&gt; gets you there with the downtime you can afford, &lt;a href="https://uptimearchitect.com/blog/oracle-ha-decision-tree-rac-vs-data-guard/" rel="noopener noreferrer"&gt;which protection architecture&lt;/a&gt; keeps it alive once it's there, and &lt;a href="https://uptimearchitect.com/blog/oracle-rman-recovery-runbook/" rel="noopener noreferrer"&gt;whether you can prove a restore&lt;/a&gt; on the worst day. The cloud changes the building; it doesn't change the job.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://uptimearchitect.com/blog/oracle-database-at-azure-vs-oci/" rel="noopener noreferrer"&gt;uptimearchitect.com&lt;/a&gt;. I write here in a personal capacity — questions or feedback are welcome via the &lt;a href="https://uptimearchitect.com/contact/" rel="noopener noreferrer"&gt;contact page&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>database</category>
      <category>azure</category>
      <category>devops</category>
    </item>
    <item>
      <title>Oracle RMAN Recovery Runbook: Restore, Recover, Prove It</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Wed, 22 Jul 2026 21:14:32 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/oracle-rman-recovery-runbook-restore-recover-prove-it-50mo</link>
      <guid>https://dev.to/uptimearchitect/oracle-rman-recovery-runbook-restore-recover-prove-it-50mo</guid>
      <description>&lt;p&gt;The worst day of an Oracle DBA's career is a restore they've never rehearsed. A datafile is gone, or a &lt;code&gt;DELETE&lt;/code&gt; ran without a &lt;code&gt;WHERE&lt;/code&gt;, or a storage array quietly corrupted a handful of blocks — and now the only thing between you and a résumé-updating outage is a backup you &lt;em&gt;hope&lt;/em&gt; works and a procedure you &lt;em&gt;think&lt;/em&gt; you remember.&lt;/p&gt;

&lt;p&gt;This is the runbook for that day. It rests on one distinction most people fumble under pressure — &lt;strong&gt;restore is not recover&lt;/strong&gt; — and it maps each failure to the specific commands that fix it, from a single corrupt block to a whole database rewound to the second before someone's mistake. Everything targets &lt;strong&gt;Oracle 19c&lt;/strong&gt;, with notes for 23ai/26ai.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version.&lt;/strong&gt; &lt;strong&gt;RESTORE&lt;/strong&gt; copies datafiles back from your backups; &lt;strong&gt;RECOVER&lt;/strong&gt; rolls them forward by applying redo. Most recoveries are &lt;code&gt;RESTORE DATABASE&lt;/code&gt; then &lt;code&gt;RECOVER DATABASE&lt;/code&gt;.&lt;br&gt;
To rewind &lt;em&gt;past&lt;/em&gt; a human error, use &lt;strong&gt;point-in-time recovery&lt;/strong&gt;: &lt;code&gt;SET UNTIL&lt;/code&gt; (time or SCN) at the top of a &lt;code&gt;RUN&lt;/code&gt; block, then restore, recover, and &lt;code&gt;OPEN RESETLOGS&lt;/code&gt;. For a few bad blocks, &lt;strong&gt;block media recovery&lt;/strong&gt; fixes just those without touching the datafile. Lost the controlfile? &lt;code&gt;NOMOUNT&lt;/code&gt;, &lt;code&gt;SET DBID&lt;/code&gt;, &lt;code&gt;RESTORE CONTROLFILE FROM AUTOBACKUP&lt;/code&gt;. And the rule under all of it: &lt;strong&gt;an untested backup is a hope, not a recovery plan.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Restore vs recover — the distinction that saves you
&lt;/h2&gt;

&lt;p&gt;Two verbs, two completely different actions, and confusing them is how people make a bad day worse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RESTORE&lt;/strong&gt; copies datafiles &lt;em&gt;back from your backups&lt;/em&gt;. It rewinds those files to whenever the backup was
taken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RECOVER&lt;/strong&gt; takes those restored files and &lt;em&gt;rolls them forward&lt;/em&gt; by applying archived and online redo —
either all the way to the present, or to a point in time you choose.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You almost never restore alone; a restore on its own throws away every transaction since the backup. The&lt;br&gt;
normal shape of a recovery is &lt;strong&gt;restore, then recover&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RMAN&amp;gt; RESTORE DATABASE;    -- copy datafiles back from the most recent backup
RMAN&amp;gt; RECOVER DATABASE;    -- apply redo to roll them forward to now
RMAN&amp;gt; ALTER DATABASE OPEN;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hold that pair in your head — everything below is a variation on &lt;em&gt;which files&lt;/em&gt; you restore and &lt;em&gt;how far&lt;/em&gt;&lt;br&gt;
you recover.&lt;/p&gt;
&lt;h2&gt;
  
  
  The failure → action map
&lt;/h2&gt;

&lt;p&gt;You don't run the same command for a single corrupt block as for a lost database. Match the failure to&lt;br&gt;
the &lt;strong&gt;smallest&lt;/strong&gt; recovery that fixes it — that's what keeps your outage small:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What failed&lt;/th&gt;
&lt;th&gt;What you run&lt;/th&gt;
&lt;th&gt;Who's down&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A few corrupt blocks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RECOVER ... BLOCK&lt;/code&gt; / &lt;code&gt;RECOVER CORRUPTION LIST&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;those blocks only — DB stays open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One datafile / tablespace&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RESTORE&lt;/code&gt; + &lt;code&gt;RECOVER DATAFILE&lt;/code&gt;/&lt;code&gt;TABLESPACE&lt;/code&gt; (online)&lt;/td&gt;
&lt;td&gt;that datafile only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Many datafiles / whole DB&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RESTORE&lt;/code&gt; + &lt;code&gt;RECOVER DATABASE&lt;/code&gt; (mounted)&lt;/td&gt;
&lt;td&gt;full outage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A bad &lt;code&gt;DELETE&lt;/code&gt; / &lt;code&gt;DROP&lt;/code&gt; (logical)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Flashback&lt;/strong&gt; (fast) or point-in-time recovery / &lt;code&gt;RECOVER TABLE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;varies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Controlfile lost&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RESTORE CONTROLFILE FROM AUTOBACKUP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;full outage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SPFILE lost&lt;/td&gt;
&lt;td&gt;&lt;code&gt;RESTORE SPFILE FROM AUTOBACKUP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;full outage&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  One datafile, without taking the database down
&lt;/h2&gt;

&lt;p&gt;The best-case recovery, and the most common in real life: a single datafile or tablespace is lost while the rest of the database keeps serving. Take &lt;em&gt;just that datafile&lt;/em&gt; offline, restore and recover it, bring it back — users on every other tablespace never notice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RMAN&amp;gt; ALTER DATABASE DATAFILE 7 OFFLINE;
RMAN&amp;gt; RESTORE DATAFILE 7;
RMAN&amp;gt; RECOVER DATAFILE 7;
RMAN&amp;gt; ALTER DATABASE DATAFILE 7 ONLINE;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Swap &lt;code&gt;DATAFILE 7&lt;/code&gt; for &lt;code&gt;TABLESPACE users&lt;/code&gt; to work at the tablespace level.) This is why restore &lt;em&gt;speed&lt;/em&gt; matters more than most teams think — the typical recovery is one file, not the whole estate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The whole database: restore then recover
&lt;/h2&gt;

&lt;p&gt;If the database is down — many files lost, or you're rebuilding on fresh hardware — mount it and run the pair against everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RMAN&amp;gt; STARTUP MOUNT;
RMAN&amp;gt; RESTORE DATABASE;
RMAN&amp;gt; RECOVER DATABASE;
RMAN&amp;gt; ALTER DATABASE OPEN;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is &lt;strong&gt;complete recovery&lt;/strong&gt;: every committed transaction, right up to the moment of failure, is back.&lt;br&gt;
No &lt;code&gt;RESETLOGS&lt;/code&gt; needed, because you recovered all the way to the present — the redo timeline is unbroken.&lt;/p&gt;
&lt;h2&gt;
  
  
  Rewinding past a bad DELETE: point-in-time recovery
&lt;/h2&gt;

&lt;p&gt;Complete recovery brings you back to &lt;em&gt;now&lt;/em&gt; — which is useless when &lt;em&gt;now&lt;/em&gt; already contains the problem: a &lt;code&gt;DELETE&lt;/code&gt; without a &lt;code&gt;WHERE&lt;/code&gt;, a bad release, a truncated table that committed an hour ago. For that you need &lt;strong&gt;incomplete recovery&lt;/strong&gt; — stop applying redo &lt;em&gt;just before&lt;/em&gt; the mistake.&lt;/p&gt;

&lt;p&gt;Here's the rule that catches people out: &lt;strong&gt;&lt;code&gt;SET UNTIL&lt;/code&gt; goes at the top of a &lt;code&gt;RUN&lt;/code&gt; block, before &lt;em&gt;both&lt;/em&gt; &lt;code&gt;RESTORE&lt;/code&gt; and &lt;code&gt;RECOVER&lt;/code&gt;.&lt;/strong&gt; Set it only before &lt;code&gt;RECOVER&lt;/code&gt; and your restored files may already carry&lt;br&gt;
timestamps past your target, and RMAN can't rewind them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RMAN&amp;gt; RUN {
  SET UNTIL TIME "TO_DATE('2026-07-19 14:29:00','YYYY-MM-DD HH24:MI:SS')";
  RESTORE DATABASE;
  RECOVER DATABASE;
}
RMAN&amp;gt; ALTER DATABASE OPEN RESETLOGS;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can target a &lt;strong&gt;time&lt;/strong&gt;, an exact &lt;strong&gt;SCN&lt;/strong&gt; (&lt;code&gt;SET UNTIL SCN 12345678&lt;/code&gt; — no ambiguity about "which 14:29"), a &lt;strong&gt;log sequence&lt;/strong&gt;, or a named &lt;strong&gt;restore point&lt;/strong&gt;. When you know the SCN of the damage, use it — it's precise where a timestamp can be a second off in either direction.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OPEN RESETLOGS&lt;/code&gt; is &lt;strong&gt;mandatory&lt;/strong&gt; after any incomplete recovery. It resets the online redo logs and starts a new database &lt;strong&gt;incarnation&lt;/strong&gt; — a fresh redo timeline branching off the old one. Two consequences worth internalizing: take a &lt;strong&gt;full backup immediately&lt;/strong&gt; afterward (your old backups now belong to a previous&lt;br&gt;
incarnation), and know that RMAN can still navigate incarnations if you ever need to go back further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two faster paths when they apply:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Flashback Database&lt;/strong&gt; (if you enabled it) rewinds the &lt;em&gt;entire&lt;/em&gt; database in minutes with no restore at all — &lt;code&gt;FLASHBACK DATABASE TO SCN 12345678;&lt;/code&gt;. It's the first thing to reach for on a logical error, and it's exactly why [The Oracle HA Decision Tree] &lt;a href="https://uptimearchitect.com/blog/oracle-ha-decision-tree-rac-vs-data-guard/" rel="noopener noreferrer"&gt;https://uptimearchitect.com/blog/oracle-ha-decision-tree-rac-vs-data-guard/&lt;/a&gt;) pairs backups &lt;em&gt;with Flashback&lt;/em&gt; as the only real answer to human error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RECOVER TABLE&lt;/code&gt;&lt;/strong&gt; (12c+) restores a single dropped or damaged table from your RMAN backups — Oracle spins up a temporary auxiliary instance behind the scenes, extracts just that table to a point in time, and imports it back. No full-database rewind, no downtime for everyone else over one table.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Just a few bad blocks: block media recovery
&lt;/h2&gt;

&lt;p&gt;When storage corrupts a &lt;em&gt;handful&lt;/em&gt; of blocks rather than a whole file, restoring the entire datafile is wildly disproportionate. &lt;strong&gt;Block media recovery&lt;/strong&gt; repairs only the corrupt blocks — and the datafile stays &lt;strong&gt;online&lt;/strong&gt; the whole time, so mean-time-to-recovery collapses.&lt;/p&gt;

&lt;p&gt;RMAN records corrupt blocks in &lt;code&gt;V$DATABASE_BLOCK_CORRUPTION&lt;/code&gt; during backups and &lt;code&gt;VALIDATE&lt;/code&gt;. You can then fix everything on that list in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RMAN&amp;gt; VALIDATE DATABASE;          -- populates V$DATABASE_BLOCK_CORRUPTION
RMAN&amp;gt; RECOVER CORRUPTION LIST;    -- repairs every block RMAN has flagged
-- or a single, known block:
RMAN&amp;gt; RECOVER DATAFILE 7 BLOCK 1234;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two limits to know going in: block media recovery is an &lt;strong&gt;Enterprise Edition&lt;/strong&gt; feature, and it repairs &lt;strong&gt;physical&lt;/strong&gt; corruption only — it cannot fix &lt;em&gt;logical&lt;/em&gt; block corruption (for that, restore/recover the&lt;br&gt;
object or use Flashback). Catch corruption early by adding &lt;strong&gt;&lt;code&gt;CHECK LOGICAL&lt;/code&gt;&lt;/strong&gt; to your validates and backups: &lt;code&gt;BACKUP VALIDATE CHECK LOGICAL DATABASE;&lt;/code&gt; scans for both physical and logical damage before it becomes a 3am page.&lt;/p&gt;
&lt;h2&gt;
  
  
  Losing the controlfile or the SPFILE
&lt;/h2&gt;

&lt;p&gt;The controlfile is the map RMAN uses to find everything — lose every copy and you can't even &lt;code&gt;MOUNT&lt;/code&gt;. If you have &lt;strong&gt;controlfile autobackup&lt;/strong&gt; on (you should: &lt;code&gt;CONFIGURE CONTROLFILE AUTOBACKUP ON;&lt;/code&gt;), recovery is mechanical — with one catch. Without a controlfile &lt;em&gt;and&lt;/em&gt; without a recovery catalog, RMAN doesn't know&lt;br&gt;
your &lt;strong&gt;DBID&lt;/strong&gt;, and it needs the DBID to locate the autobackup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RMAN&amp;gt; STARTUP NOMOUNT;
RMAN&amp;gt; SET DBID 320066378;                    -- from your records, not the database
RMAN&amp;gt; RESTORE CONTROLFILE FROM AUTOBACKUP;
RMAN&amp;gt; ALTER DATABASE MOUNT;
RMAN&amp;gt; RECOVER DATABASE;
RMAN&amp;gt; ALTER DATABASE OPEN RESETLOGS;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lost &lt;strong&gt;SPFILE&lt;/strong&gt; follows the same shape (&lt;code&gt;RESTORE SPFILE FROM AUTOBACKUP&lt;/code&gt; from &lt;code&gt;NOMOUNT&lt;/code&gt;). The one thing to do &lt;em&gt;today&lt;/em&gt;: &lt;strong&gt;write your DBID down somewhere outside the database.&lt;/strong&gt; It's the piece you can't recover &lt;em&gt;from&lt;/em&gt; the database when the database is precisely what you've lost.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The recovery decision: match the failure to the smallest recovery that fixes it. A corrupt block doesn't need a database restore; a human error doesn't need bare-metal recovery.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A{What failed?}
  A -- "A few corrupt blocks" --&amp;gt; B[Block media recovery&amp;lt;br/&amp;gt;RECOVER CORRUPTION LIST&amp;lt;br/&amp;gt;datafile stays online]
  A -- "One datafile / tablespace" --&amp;gt; C[Offline it, RESTORE + RECOVER&amp;lt;br/&amp;gt;the file, bring it online&amp;lt;br/&amp;gt;rest of DB stays up]
  A -- "Whole DB / new hardware" --&amp;gt; D[MOUNT, RESTORE + RECOVER&amp;lt;br/&amp;gt;DATABASE, OPEN&amp;lt;br/&amp;gt;complete recovery]
  A -- "Human error, already committed" --&amp;gt; E{Flashback enabled?}
  E -- Yes --&amp;gt; F[FLASHBACK DATABASE&amp;lt;br/&amp;gt;minutes, no restore]
  E -- No --&amp;gt; G[Point-in-time recovery&amp;lt;br/&amp;gt;SET UNTIL, RESTORE, RECOVER&amp;lt;br/&amp;gt;OPEN RESETLOGS]
  A -- "Controlfile / SPFILE" --&amp;gt; H[NOMOUNT, SET DBID&amp;lt;br/&amp;gt;RESTORE ... FROM AUTOBACKUP]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prove it before you need it
&lt;/h2&gt;

&lt;p&gt;Everything above assumes the backup you're restoring actually works — and most teams don't find out whether it does until the worst possible moment. Two RMAN commands close that gap without a full restore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RESTORE DATABASE VALIDATE&lt;/code&gt;&lt;/strong&gt; confirms the backups a restore &lt;em&gt;would&lt;/em&gt; need exist and aren't corrupt —   without writing a single datafile. Add &lt;strong&gt;&lt;code&gt;CHECK LOGICAL&lt;/code&gt;&lt;/strong&gt; to scan for logical corruption too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RESTORE DATABASE PREVIEW&lt;/code&gt;&lt;/strong&gt; reports the exact backups and archived logs RMAN would use, so you can confirm your recovery window is intact &lt;em&gt;before&lt;/em&gt; an incident, not during one.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RMAN&amp;gt; RESTORE DATABASE VALIDATE CHECK LOGICAL;
RMAN&amp;gt; RESTORE DATABASE PREVIEW;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But validation is not a rehearsal. &lt;strong&gt;The only real test is an actual restore to a scratch host, on a schedule.&lt;/strong&gt; A backup you've never restored is a hope; a restore you ran last month is a plan. If you take one thing from this post, take that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What teams get wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confusing restore and recover&lt;/strong&gt; — restoring without recovering (and silently discarding everything since the backup), or assuming "restore" finished the job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;SET UNTIL&lt;/code&gt; in the wrong place&lt;/strong&gt; — after &lt;code&gt;RESTORE&lt;/code&gt; instead of before it, so the restored files overshoot the target time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting &lt;code&gt;OPEN RESETLOGS&lt;/code&gt;&lt;/strong&gt; after incomplete recovery — or not realizing it forks a new incarnation, and failing to take a fresh backup immediately after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No controlfile autobackup, or a DBID nobody wrote down&lt;/strong&gt; — the recovery you can't even begin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full restore when a smaller tool would do&lt;/strong&gt; — reaching for &lt;code&gt;RESTORE DATABASE&lt;/code&gt; when Flashback (minutes) or &lt;code&gt;RECOVER TABLE&lt;/code&gt; (one object) fixes it with a fraction of the downtime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never testing a restore.&lt;/strong&gt; The backup that runs green every night and has never once been restored is the one that fails when it finally matters.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Restore&lt;/strong&gt; copies datafiles back; &lt;strong&gt;recover&lt;/strong&gt; rolls them forward with redo — most recoveries are &lt;code&gt;RESTORE DATABASE&lt;/code&gt; then &lt;code&gt;RECOVER DATABASE&lt;/code&gt;, and you &lt;code&gt;OPEN&lt;/code&gt; when you've recovered to the present. To rewind past a mistake, wrap &lt;code&gt;SET UNTIL&lt;/code&gt; / &lt;code&gt;RESTORE&lt;/code&gt; / &lt;code&gt;RECOVER&lt;/code&gt; in a &lt;code&gt;RUN&lt;/code&gt; block and &lt;code&gt;OPEN RESETLOGS&lt;/code&gt;. Fix a few bad blocks with &lt;code&gt;RECOVER CORRUPTION LIST&lt;/code&gt; while the datafile stays online; recover a lost controlfile from autobackup after &lt;code&gt;SET DBID&lt;/code&gt; in &lt;code&gt;NOMOUNT&lt;/code&gt;; and reach for &lt;strong&gt;Flashback&lt;/strong&gt; or &lt;code&gt;RECOVER TABLE&lt;/code&gt; before a full restore when the problem is logical. Then do the part nobody enjoys and everybody needs: &lt;strong&gt;test the restore on a schedule&lt;/strong&gt;, because an untested backup was never a recovery plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between RESTORE and RECOVER in RMAN?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RESTORE copies datafiles back from your RMAN backups, rewinding them to the time the backup was taken. RECOVER then applies archived and online redo logs to roll those restored files forward — either to the present (complete recovery) or to a chosen point in time (incomplete recovery). A normal recovery is RESTORE DATABASE followed by RECOVER DATABASE; restoring without recovering would discard every transaction since the backup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I recover an Oracle database to a point in time before a mistake?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use database point-in-time recovery (incomplete recovery). Put a SET UNTIL clause (a time, SCN, log sequence, or restore point) at the top of a RUN block, before both RESTORE DATABASE and RECOVER DATABASE, so the target applies to both. After recovery you must open the database with ALTER DATABASE OPEN RESETLOGS, which starts a new incarnation — take a full backup immediately afterward. If Flashback Database is enabled, FLASHBACK DATABASE TO SCN is usually faster because it needs no restore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I recover just a few corrupt blocks without restoring the whole datafile?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes — that is block media recovery, and the datafile stays online the entire time. RMAN records corrupt blocks in V$DATABASE_BLOCK_CORRUPTION during backups and VALIDATE; RECOVER CORRUPTION LIST then repairs everything on that list, or RECOVER DATAFILE n BLOCK m fixes a specific block. It is an Enterprise Edition feature and repairs physical corruption only — it cannot fix logical block corruption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What do I do if I lose the control file?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If controlfile autobackup is enabled, start the instance in NOMOUNT, set the DBID (SET DBID), run RESTORE CONTROLFILE FROM AUTOBACKUP, then MOUNT, RECOVER DATABASE, and OPEN RESETLOGS. Without a recovery catalog, RMAN needs the DBID to find the autobackup, so record your DBID somewhere outside the database. A lost SPFILE is recovered the same way with RESTORE SPFILE FROM AUTOBACKUP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I have to OPEN RESETLOGS after recovery?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only after incomplete recovery (point-in-time recovery) or after recovering with a backup control file. Complete recovery to the present does not need RESETLOGS. When you do open with RESETLOGS, Oracle starts a new database incarnation, so you should take a fresh full backup right away because prior backups belong to the earlier incarnation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I test that my RMAN backups actually work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use RESTORE DATABASE VALIDATE (add CHECK LOGICAL) to confirm the needed backups exist and are not corrupt without restoring, and RESTORE DATABASE PREVIEW to see exactly which backups and archived logs a recovery would use. But those checks are not a rehearsal — the only real proof is a scheduled, actual restore to a separate scratch host. A backup that has never been restored is a hope, not a recovery plan.&lt;/p&gt;

&lt;p&gt;Recovery is the floor under every other high-availability choice — RAC and Data Guard survive hardware&lt;br&gt;
and site failures, but only backups and Flashback survive a bad &lt;code&gt;DELETE&lt;/code&gt;. For where each fits, see&lt;br&gt;
&lt;a href="https://uptimearchitect.com/blog/oracle-ha-decision-tree-rac-vs-data-guard/" rel="noopener noreferrer"&gt;The Oracle HA Decision Tree&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Practice this before you need it.&lt;/strong&gt; The &lt;a href="https://github.com/pyaroslav/oracle-labs/tree/main/rman" rel="noopener noreferrer"&gt;&lt;code&gt;rman/&lt;/code&gt; recovery lab&lt;/a&gt;&lt;br&gt;
lets you break a throwaway Oracle Database Free with Docker and recover it &lt;em&gt;for real&lt;/em&gt;: delete a datafile&lt;br&gt;
and &lt;code&gt;RESTORE&lt;/code&gt;/&lt;code&gt;RECOVER&lt;/code&gt; it, corrupt a block and repair just that block with &lt;strong&gt;block media recovery&lt;/strong&gt;,&lt;br&gt;
rewind past a committed &lt;code&gt;DELETE&lt;/code&gt; with &lt;strong&gt;point-in-time recovery&lt;/strong&gt;, and prove a restore works with&lt;br&gt;
&lt;code&gt;RESTORE ... VALIDATE&lt;/code&gt;. It row-checks every recovery, so it only passes if the data actually comes back.&lt;br&gt;
The first time you run a restore should never be in production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://uptimearchitect.com/blog/oracle-rman-recovery-runbook/" rel="noopener noreferrer"&gt;uptimearchitect.com&lt;/a&gt;. I write here in a personal capacity — questions or feedback are welcome via the &lt;a href="https://uptimearchitect.com/contact/" rel="noopener noreferrer"&gt;contact page&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>database</category>
      <category>devops</category>
      <category>sre</category>
    </item>
    <item>
      <title>Your First Oracle Autonomous Database on OCI Always Free</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Sun, 19 Jul 2026 23:19:31 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/your-first-oracle-autonomous-database-on-oci-always-free-554b</link>
      <guid>https://dev.to/uptimearchitect/your-first-oracle-autonomous-database-on-oci-always-free-554b</guid>
      <description>&lt;p&gt;The single best way to keep your Oracle skills sharp without a license, a server, or a finance conversation is &lt;strong&gt;OCI Always Free&lt;/strong&gt;. It gives you a real, fully managed Oracle database — Autonomous&lt;br&gt;
Database, running the current &lt;strong&gt;23ai&lt;/strong&gt; release — that stays free indefinitely. It's the ideal sandbox: a place to test commands, try new-release features, and generate real screenshots, all on infrastructure that's yours and costs nothing.&lt;/p&gt;

&lt;p&gt;Here's how to stand one up in a few minutes, what's genuinely worth doing with it, and the one catch that catches people out. One ground rule first: do all of this on a &lt;strong&gt;personal&lt;/strong&gt; OCI accoun, never an employer's tenancy.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version.&lt;/strong&gt; Sign up at oracle.com/cloud/free, create an &lt;strong&gt;Autonomous Database&lt;/strong&gt; with the &lt;strong&gt;Always Free&lt;/strong&gt; toggle on, and connect through the browser via &lt;strong&gt;Database Actions → SQL&lt;/strong&gt; — no client install. You get a real, managed &lt;strong&gt;Oracle 23ai&lt;/strong&gt; database with &lt;strong&gt;AI Vector Search included&lt;/strong&gt;, free forever. The one catch: it &lt;strong&gt;stops after 7 days of inactivity&lt;/strong&gt; and is &lt;strong&gt;reclaimed after ~90 days&lt;/strong&gt; stopped, so log in occasionally and it's yours to keep.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  What "Always Free" actually includes
&lt;/h2&gt;

&lt;p&gt;OCI's Always Free tier is genuinely free forever — not a 30-day trial — and the database piece is the star:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;2× Autonomous Database&lt;/strong&gt;, each with &lt;strong&gt;1 OCPU and 20 GB&lt;/strong&gt; of storage — the focus of this post.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Arm Ampere A1 compute:&lt;/strong&gt; up to &lt;strong&gt;4 OCPUs + 24 GB RAM&lt;/strong&gt; total — enough to also run Oracle Database Free yourself on a VM, if you want the self-managed side too.&lt;/li&gt;
&lt;li&gt;Object storage, networking, and a handful of other services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The catch worth knowing up front:&lt;/strong&gt; an Always Free Autonomous Database &lt;strong&gt;stops automatically after 7 days of inactivity&lt;/strong&gt; (your data is preserved), and if it stays stopped for &lt;strong&gt;90 cumulative days&lt;/strong&gt; it can be &lt;strong&gt;reclaimed and permanently deleted&lt;/strong&gt;. "Activity" means an actual connection running SQL — so just log in and run something every week or two and it's yours indefinitely. Set a calendar nudge; that 90-day clock is how most people lose their sandbox.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — Create a personal OCI account
&lt;/h2&gt;

&lt;p&gt;Sign up at &lt;strong&gt;oracle.com/cloud/free&lt;/strong&gt; with a personal email. Identity verification asks for a card, but Always Free resources are never charged — the card is for identity and for &lt;em&gt;if&lt;/em&gt; you later opt into paid resources (you won't need to). Pick a &lt;strong&gt;home region&lt;/strong&gt; close to you; if you also want the free Ampere VM, choose a region with A1 capacity. Your home region is permanent, so choose deliberately.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2 — Provision the Autonomous Database
&lt;/h2&gt;

&lt;p&gt;In the Console, go to &lt;strong&gt;Oracle Database → Autonomous Database → Create Autonomous Database&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workload type:&lt;/strong&gt; &lt;em&gt;Transaction Processing (ATP)&lt;/em&gt; or &lt;em&gt;Data Warehouse (ADW)&lt;/em&gt;. ATP is the natural default for a general-purpose learning sandbox — it behaves like the OLTP databases you meet day to day. Pick ADW only if you're specifically playing with analytics/columnar workloads. (JSON and APEX flavors also exist; ignore them for now.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always Free:&lt;/strong&gt; toggle it &lt;strong&gt;on&lt;/strong&gt;. This is the switch that matters — it's easy to miss, and without it you're provisioning a paid instance.&lt;/li&gt;
&lt;li&gt;Set a strong &lt;strong&gt;ADMIN&lt;/strong&gt; password, leave everything else at defaults, and click &lt;strong&gt;Create&lt;/strong&gt;. It's ready in a minute or two.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step 3 — Connect (no client install needed)
&lt;/h2&gt;

&lt;p&gt;The fastest path needs nothing on your laptop. From the database's detail page, open &lt;strong&gt;Database Actions → SQL&lt;/strong&gt; — a full browser-based SQL worksheet (SQL Developer Web). Log in as &lt;code&gt;ADMIN&lt;/code&gt; and you're querying:&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="c1"&gt;-- prove you're on a current release&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;banner_full&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;version&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;database_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;open_mode&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;database&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;v$version&lt;/code&gt; confirms exactly which 23ai build your Always Free ADB is on the day you provision it —&lt;br&gt;
worth checking, because Oracle keeps the managed service current for you.&lt;/p&gt;

&lt;p&gt;For a desktop client or an application, download the &lt;strong&gt;wallet&lt;/strong&gt; (the mTLS credentials bundle) from the database page and point your tool at the connection strings inside it. But for learning and quick tests, Database Actions in the browser is all you need.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4 — Put something in it
&lt;/h2&gt;

&lt;p&gt;A sandbox is more useful with data. This runs as-is under the &lt;code&gt;ADMIN&lt;/code&gt; schema:&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;demo_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;        &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;  &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;amount&lt;/span&gt;    &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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="n"&gt;created&lt;/span&gt;   &lt;span class="nb"&gt;DATE&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;SYSDATE&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;demo_orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&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;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'Customer '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;LEVEL&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="n"&gt;DBMS_RANDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VALUE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&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;FROM&lt;/span&gt;   &lt;span class="n"&gt;dual&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;LEVEL&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&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;rows_loaded&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="k"&gt;AVG&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;avg_amount&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;demo_orders&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's a thousand rows to experiment against — enough to try indexes, execution plans, or a query you&lt;br&gt;
want to tune.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 5 — Try something only the new releases can do
&lt;/h2&gt;

&lt;p&gt;The reason to sandbox on a &lt;em&gt;current&lt;/em&gt; release is to play with what's new — and on 23ai the headline is &lt;strong&gt;AI Vector Search&lt;/strong&gt;: storing embeddings (the numeric fingerprints of text, images, etc.) right next to your relational data and querying them by &lt;em&gt;similarity&lt;/em&gt; instead of exact match. It's included on Always&lt;br&gt;
Free at no extra cost.&lt;/p&gt;

&lt;p&gt;You'd normally generate embeddings from a model, but you can see the whole mechanism with tiny hand-built vectors — no model required:&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;TABLE&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;        &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;content&lt;/span&gt;   &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;embedding&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FLOAT32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;-- 3 dimensions, just to watch it work&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="k"&gt;VALUES&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="s1"&gt;'cat'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[0.9, 0.1, 0.0]'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="k"&gt;VALUES&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="s1"&gt;'kitten'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[0.8, 0.2, 0.0]'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'airplane'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[0.0, 0.1, 0.9]'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- which rows are most "similar" to cat? (smallest cosine distance)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&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="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TO_VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[0.9, 0.1, 0.0]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;4&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;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;   &lt;span class="n"&gt;docs&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;  &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&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;kitten&lt;/code&gt; comes back as the nearest neighbor to &lt;code&gt;cat&lt;/code&gt; and &lt;code&gt;airplane&lt;/code&gt; as the farthest — the same operation that powers semantic search and RAG, just with three dimensions instead of a thousand. Swap in real&lt;br&gt;
embeddings from a model and add an &lt;strong&gt;HNSW&lt;/strong&gt; or &lt;strong&gt;IVF&lt;/strong&gt; vector index, and this is production AI search. On a free database you fully control.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;No OCI account? Run it locally.&lt;/strong&gt; The &lt;a href="https://github.com/pyaroslav/oracle-labs/tree/main/vector-search" rel="noopener noreferrer"&gt;AI Vector Search lab&lt;/a&gt;&lt;br&gt;
runs this exact &lt;code&gt;VECTOR&lt;/code&gt; / &lt;code&gt;VECTOR_DISTANCE&lt;/code&gt; demo on Oracle Database Free with Docker — vector search ships in the free image too — so you can watch &lt;code&gt;kitten&lt;/code&gt; land nearest &lt;code&gt;cat&lt;/code&gt; in about two minutes, no&lt;br&gt;
cloud signup: &lt;code&gt;./run.sh up &amp;amp;&amp;amp; ./run.sh all&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What this sandbox is good for — and what it isn't
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verifying before you publish.&lt;/strong&gt; Run the exact commands from a blog post, runbook, or Stack Exchange answer on a real instance instead of trusting memory. (It's how the SQL in these posts gets checked.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Original screenshots and reports.&lt;/strong&gt; The OCI console, Database Actions, real query output —  concrete assets that make your writing credible. You can even generate &lt;a href="https://uptimearchitect.com/blog/how-to-read-an-awr-report/" rel="noopener noreferrer"&gt;AWR-style performance reports&lt;/a&gt; to practice reading them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learning the cloud-native side.&lt;/strong&gt; Provisioning, scaling, automatic backups, and the managed-service model you'll meet on real &lt;a href="https://uptimearchitect.com/blog/oracle-cloud-migration-methods/" rel="noopener noreferrer"&gt;Oracle-to-cloud migrations&lt;/a&gt; —
Autonomous is one of the most common migration targets.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What it &lt;em&gt;won't&lt;/em&gt; do: because Always Free ADB is a fully managed service, you don't configure theinfrastructure — so you won't stand up &lt;a href="https://uptimearchitect.com/blog/oracle-ha-decision-tree-rac-vs-data-guard/" rel="noopener noreferrer"&gt;RAC or Data Guard&lt;/a&gt; on it (those are separate, self-managed exercises). But as a zero-cost, always-on, current-release Oracle database you fully control, it's very hard to beat.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is OCI Always Free actually free, or a trial?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It is genuinely free forever, not a time-limited trial. The Always Free tier includes two Autonomous Databases (1 OCPU and 20 GB each), Arm Ampere A1 compute (up to 4 OCPUs and 24 GB RAM), and some storage and networking — all at no charge for as long as you use them. Oracle also offers a separate 30-day free trial with credits, but the Always Free resources persist beyond it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What version of Oracle Database does Always Free Autonomous run?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Always Free Autonomous Database runs Oracle Database 23ai, the current release, and Oracle keeps the managed service patched and current for you. Run &lt;code&gt;SELECT banner_full FROM v$version&lt;/code&gt; in Database Actions to see the exact build on the day you provision it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use AI Vector Search on the free tier?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. AI Vector Search is a core Oracle Database 23ai feature and is included at no additional charge on Autonomous Database, including the Always Free tier. You can create VECTOR columns, run VECTOR_DISTANCE similarity queries, and build HNSW or IVF vector indexes without paying anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Will my Always Free database get deleted?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It can, if you abandon it. An Always Free Autonomous Database stops automatically after 7 days of inactivity (its data is preserved), and if it stays stopped for 90 cumulative days it may be reclaimed and permanently deleted. Connecting and running SQL resets the inactivity clock, so logging in every week or two keeps it indefinitely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I run RAC or Data Guard on Always Free Autonomous Database?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Autonomous Database is a fully managed service, so you do not configure the underlying infrastructure — RAC and Data Guard are not something you set up on it. Oracle handles high availability and backups for you behind the scenes. To practice building RAC or Data Guard yourself, use your own Enterprise Edition environment or lab, not Always Free ADB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need to install anything to connect?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. From the database detail page, open Database Actions and use the browser-based SQL worksheet — nothing to install. For desktop clients or applications, download the wallet (mTLS credentials) from the database page and use the connection strings it contains.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://uptimearchitect.com/blog/oracle-autonomous-database-oci-always-free/" rel="noopener noreferrer"&gt;uptimearchitect.com&lt;/a&gt;. I write here in a personal capacity — questions or feedback are welcome via the &lt;a href="https://uptimearchitect.com/contact/" rel="noopener noreferrer"&gt;contact page&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>database</category>
      <category>cloud</category>
      <category>ai</category>
    </item>
    <item>
      <title>Side-Project Cemetery: I built a graveyard for your abandoned repos (and a button that brings them back)</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Sat, 11 Jul 2026 20:30:46 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/side-project-cemetery-i-built-a-graveyard-for-your-abandoned-repos-and-a-button-that-brings-them-3aee</link>
      <guid>https://dev.to/uptimearchitect/side-project-cemetery-i-built-a-graveyard-for-your-abandoned-repos-and-a-button-that-brings-them-3aee</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Every abandoned repo was once someone's 2 AM obsession.&lt;/p&gt;

&lt;p&gt;That's the most honest artifact of passion a developer owns — not the polished portfolio piece, but the thing you started at midnight because you &lt;em&gt;couldn't not&lt;/em&gt;, pushed to for eleven straight days, and then... life happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Side-Project Cemetery&lt;/strong&gt; turns any GitHub profile into a moonlit graveyard. Type a username and every public repo that hasn't seen a push in 180+ days rises from the ground as a headstone — birth year, death year, how long it was &lt;em&gt;loved fiercely&lt;/em&gt;, candles for its stars, and an affectionate epitaph:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Its last commit was hope. Its next was never."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Written in JavaScript. Died of callback exhaustion."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Not dead — just aggressively on hold since 2025."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But here's the part the challenge theme is really about. Passion doesn't die — it naps. So every grave has a &lt;strong&gt;🔥 Rekindle&lt;/strong&gt; button. Click it, and the app reads the repo's README and writes a pitch designed to re-seduce you with your own idea — closing with &lt;strong&gt;"Your first step tonight:"&lt;/strong&gt;, one small, concrete action derived from the actual repo. Then it prints you a &lt;em&gt;Resurrection Certificate&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The tone rule for the whole app: loving humor, never mockery. These are graves, but we come with flowers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🪦 &lt;strong&gt;Live: &lt;a href="https://pyaroslav.github.io/side-project-cemetery/" rel="noopener noreferrer"&gt;https://pyaroslav.github.io/side-project-cemetery/&lt;/a&gt;&lt;/strong&gt; — bring your own username. I'll wait.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0mtx3ijzgeed5qjbf2g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy0mtx3ijzgeed5qjbf2g.gif" alt="Visiting my cemetery and rekindling a flame" width="600" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is my own graveyard — I'm not exempt. Two graves, and both epitaphs read &lt;em&gt;"loved fiercely for one wild night,"&lt;/em&gt; because both were single-evening obsessions I never came back to. The app roasted me with:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;OracleDatabaseTuning&lt;/code&gt; &lt;em&gt;rests here, one&lt;/em&gt; &lt;code&gt;git push&lt;/code&gt; &lt;em&gt;short of glory.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feowbbyslj8bfv13wnk5m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feowbbyslj8bfv13wnk5m.png" alt="My own tiny graveyard" width="800" height="598"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Then I hit &lt;strong&gt;Rekindle&lt;/strong&gt; on it. This is the built-in offline engine — no API key involved:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;OracleDatabaseTuning&lt;/code&gt; &lt;em&gt;isn't a corpse. It's a nap that got out of hand. You started it because the idea wouldn't leave you alone at night. You built the whole first spark of it in a single sitting, which means the hard part — starting — is a thing you've already done here. [...] The distance between a dead repo and a living one is a single commit — and you've written harder code half-asleep. Tonight it doesn't need a rewrite. It needs five minutes and the version of you that started it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your first step tonight:&lt;/strong&gt; Open the README, find the "Oracle Database Tuning Tool" section, and write one true sentence under it about where this actually stands today.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flciskjii1qfuoepqaqtr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flciskjii1qfuoepqaqtr.png" alt="The Rekindle pitch" width="800" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Point it at your favorite prolific maintainer and you get a proper necropolis — headstones sorted most-loved first, and truly dedicated abandoners get a &lt;em&gt;"🚶 Walk deeper — N more souls rest here"&lt;/em&gt; gate after the first 24 graves. Drop your best epitaph in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/pyaroslav" rel="noopener noreferrer"&gt;
        pyaroslav
      &lt;/a&gt; / &lt;a href="https://github.com/pyaroslav/side-project-cemetery" rel="noopener noreferrer"&gt;
        side-project-cemetery
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      🪦 A moonlit graveyard for your abandoned repos — with loving epitaphs and a 🔥 Rekindle button. Built for the DEV Weekend Challenge: Passion Edition.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;🪦 Side-Project Cemetery&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;Every abandoned repo was once someone's 2 AM obsession.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Enter any GitHub username and wander a moonlit cemetery of that person's
&lt;strong&gt;abandoned side projects&lt;/strong&gt; — public, original repositories that haven't been
pushed to in over six months. Each one gets a headstone and a loving
slightly funny epitaph.&lt;/p&gt;
&lt;p&gt;Then comes the turn: pick any grave and hit &lt;strong&gt;🔥 Rekindle&lt;/strong&gt;. The app reads the
repo, writes a passionate one-paragraph revival pitch aimed straight at its
creator, and hands you one concrete first step for tonight — because passion
doesn't die, it just naps.&lt;/p&gt;
&lt;p&gt;Built for the &lt;strong&gt;DEV Weekend Challenge: Passion Edition&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run it&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;It's a pure static site — three files, no build step, no dependencies.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c1"&gt;cd&lt;/span&gt; side-project-cemetery
python3 -m http.server 8000
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; open http://localhost:8000&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Any static file server works. You can also deep-link straight to a cemetery:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;http://localhost:8000/?u=sindresorhus
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hero&lt;/strong&gt; — type a…&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/pyaroslav/side-project-cemetery" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Three static files. No build step, no framework, no server.&lt;/strong&gt; The night scene — moon, twinkling stars, drifting fog, fireflies — is pure CSS: radial gradients, blurred shapes on slow keyframe loops, six 4px dots with per-instance custom properties. No image assets at all, and it all switches off under &lt;code&gt;prefers-reduced-motion&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The graveyard is the GitHub REST API, unauthenticated.&lt;/strong&gt; A repo becomes a headstone when it's public, not a fork (a fork is someone else's passion), and &lt;code&gt;pushed_at&lt;/code&gt; is over 180 days old. "Loved fiercely for N days" is &lt;code&gt;pushed_at − created_at&lt;/code&gt; — the window it actually received work. Under a day renders as &lt;em&gt;"one wild night."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Epitaphs are deterministic.&lt;/strong&gt; Reloading a graveyard shouldn't reshuffle the dead, so each repo's epitaph is seeded from a hash of its name across 20 templates, with a language→cause-of-death map (~25 languages). A de-clustering pass keeps one page of graves from repeating itself. Everything works with zero API keys.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI is an upgrade, never a requirement&lt;/strong&gt; — my favorite design constraint of the weekend. Save a &lt;strong&gt;Gemini&lt;/strong&gt; key in ✨ AI settings and the whole cemetery gets AI-written epitaphs in &lt;em&gt;one batched call&lt;/em&gt; (&lt;code&gt;gemini-flash-latest&lt;/code&gt;, &lt;code&gt;responseSchema&lt;/code&gt;-constrained JSON, silent fallback to the local engine on any failure). It read my dead Oracle-tuning experiment and wrote:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"You dreamed of optimization for zero days, proving the fastest database code is the code never written."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9si11owkl8rfafum8p70.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9si11owkl8rfafum8p70.png" alt="AI epitaphs on my graveyard" width="800" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rekindle sends the README — base64-decoded client-side — and gets back &lt;code&gt;{paragraph, step}&lt;/code&gt;. For that same repo, Gemini pitched me &lt;em&gt;"an automated performance detective that rescues database administrator sanity"&lt;/em&gt; and assigned tonight's step: create &lt;code&gt;tune.py&lt;/code&gt; and write one mock Oracle wait event. That's uncomfortably actionable — same grave, two engines, and you can compare them yourself in the screenshots.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft8go91umd2j5iyhrn3j4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft8go91umd2j5iyhrn3j4.png" alt="The AI Rekindle pitch" width="800" height="545"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fun war story: my first test key failed with the original model list — new Google AI keys get a 404 on &lt;code&gt;gemini-2.5-flash&lt;/code&gt; and a quota error on &lt;code&gt;gemini-2.0-flash&lt;/code&gt;, and my retry chain only advanced on 404s. If you ship a BYO-key app, lead with the &lt;code&gt;-latest&lt;/code&gt; rolling alias and retry on &lt;em&gt;any&lt;/em&gt; model error, or every fresh-key user sees your fallback path and nothing else.&lt;/p&gt;

&lt;p&gt;Save an &lt;strong&gt;ElevenLabs&lt;/strong&gt; key and a &lt;em&gt;🔊 Hear the eulogy&lt;/em&gt; button appears: your epitaph and revival pitch, read aloud over the fog (&lt;code&gt;eleven_multilingual_v2&lt;/code&gt;, trimmed at sentence boundaries so the voice never dies mid-thought — a bug I actually shipped and fixed). Keys live in &lt;code&gt;localStorage&lt;/code&gt;, go only to their own services, and the app is never broken, never blank, never waiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first step is derived, not canned.&lt;/strong&gt; If the README mentions a TODO, the step targets it. Otherwise it uses the README's first heading, or a real command for the repo's language, or folds the repo's description into a task. The goal: make the next commit feel five minutes away. Because it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this one
&lt;/h3&gt;

&lt;p&gt;&lt;br&gt;
I picked this idea because my own graveyard called me out. Both of my dead repos — an Oracle database tuning experiment and a CS50 AI course project — were &lt;em&gt;one wild night&lt;/em&gt; each: started in a burst of 2 AM conviction, abandoned by breakfast. Building this app meant staring at that pattern for a whole weekend. It worked: &lt;code&gt;cs50ai&lt;/code&gt; is getting its "first step tonight" — tonight.&lt;/p&gt;

&lt;p&gt;I built a cemetery, but I really built the opposite: a machine for noticing that every grave in it is one commit away from being a garden again.&lt;/p&gt;

&lt;p&gt;Go visit yours. Bring flowers. Leave with a first step. 🔥&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built solo within the challenge window. Stack: vanilla HTML/CSS/JS, GitHub REST API, Google Gemini (Flash) + ElevenLabs (both BYO-key, both optional), zero servers.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>Migrating Oracle to the Cloud: Which Method, and When</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Wed, 08 Jul 2026 19:21:51 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/migrating-oracle-to-the-cloud-which-method-and-when-4o8g</link>
      <guid>https://dev.to/uptimearchitect/migrating-oracle-to-the-cloud-which-method-and-when-4o8g</guid>
      <description>&lt;p&gt;Moving an Oracle database to the cloud is a solved problem five times over — Data Pump, transportable tablespaces, Data Guard, GoldenGate, and ZDM all do it. The hard part isn't the destination; it's picking the &lt;em&gt;method&lt;/em&gt;, because the method — not the cloud — decides your downtime, your risk, and whether you can roll back when something goes sideways at 2am on cutover night.&lt;/p&gt;

&lt;p&gt;There's no single best method. The right one falls out of four questions: &lt;strong&gt;how much downtime can you afford&lt;/strong&gt;, &lt;strong&gt;are you crossing a boundary&lt;/strong&gt; (endianness, version, platform, character set), &lt;strong&gt;how big is the database&lt;/strong&gt;, and — the one people forget — &lt;strong&gt;what does the target even allow?&lt;/strong&gt; Answer those and the method is nearly decided for you. This guide walks the decision for both OCI and Oracle Database@Azure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version.&lt;/strong&gt; &lt;strong&gt;Logical&lt;/strong&gt; methods (Data Pump, GoldenGate) &lt;em&gt;recreate&lt;/em&gt; objects, so they cross any boundary — version, platform, endianness, character set — but you pay in downtime (Data Pump) or licensing and complexity (GoldenGate). &lt;strong&gt;Physical&lt;/strong&gt; methods (RMAN/restore, transportable tablespaces, Data Guard) &lt;em&gt;copy blocks&lt;/em&gt;, so they're fast and faithful but locked to the &lt;strong&gt;same endianness&lt;/strong&gt; (and, for Data Guard, the same version). &lt;strong&gt;Data Guard&lt;/strong&gt; gives near-zero downtime and the best rollback when the architecture &lt;em&gt;doesn't&lt;/em&gt; change; &lt;strong&gt;GoldenGate&lt;/strong&gt; gives near-zero downtime when it &lt;em&gt;does&lt;/em&gt;. &lt;strong&gt;ZDM&lt;/strong&gt; is the free Oracle tool that automates whichever fits. And the &lt;strong&gt;target caps your options&lt;/strong&gt;: Exadata and Database@Azure allow everything; Autonomous allows logical only.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The two axes that pick your method
&lt;/h2&gt;

&lt;p&gt;Before you compare tools, answer two questions. They eliminate most of the options on their own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. How much downtime can you afford?&lt;/strong&gt; Every method is either an &lt;em&gt;outage&lt;/em&gt; (the database is down for the whole move, and the outage scales with data size) or &lt;em&gt;near-zero&lt;/em&gt; (the target is built and synced while the source keeps serving, and only a brief cutover is downtime). Data Pump and plain RMAN restore are outages. Data Guard, GoldenGate, and incremental XTTS are near-zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Are you crossing a boundary?&lt;/strong&gt; The big one is &lt;strong&gt;endianness&lt;/strong&gt;. Logical methods recreate each block on load, so they're endian-agnostic — they don't care. Physical methods copy blocks verbatim, so they require the &lt;strong&gt;same endian format&lt;/strong&gt; on both ends. Check it before you design anything:&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;platform_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endian_format&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;transportable_platform&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;endian_format&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;platform_name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;platform_name&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;transportable_platform&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;platform_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;platform_id&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;database&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classic cloud migration is getting &lt;em&gt;off&lt;/em&gt; a big-endian UNIX box — &lt;strong&gt;Solaris SPARC, AIX, HP-UX&lt;/strong&gt; — and onto &lt;strong&gt;little-endian Linux x86-64&lt;/strong&gt; on Exadata or in Azure. That's a cross-endian move, which rules out a straight RMAN restore or a Data Guard standby entirely. You're left with a logical method, or transportable tablespaces with an explicit &lt;code&gt;RMAN CONVERT&lt;/code&gt;. Version changes and character-set changes&lt;br&gt;
behave the same way: logical handles them, physical mostly doesn't.&lt;/p&gt;

&lt;p&gt;Hold those two answers. Now the methods.&lt;/p&gt;
&lt;h2&gt;
  
  
  The five methods
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Downtime&lt;/th&gt;
&lt;th&gt;Crosses endian / version?&lt;/th&gt;
&lt;th&gt;Best when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Data Pump&lt;/strong&gt; (&lt;code&gt;expdp&lt;/code&gt;/&lt;code&gt;impdp&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Logical export/import — recreates objects&lt;/td&gt;
&lt;td&gt;Proportional to size (full outage)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; — any boundary&lt;/td&gt;
&lt;td&gt;Small/medium DB, version or charset change, partial move, → Autonomous&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transportable / XTTS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Copy datafiles + Data Pump metadata; XTTS rolls forward with RMAN incrementals&lt;/td&gt;
&lt;td&gt;Low (XTTS = just the final read-only increment)&lt;/td&gt;
&lt;td&gt;Cross-endian via &lt;code&gt;RMAN CONVERT&lt;/code&gt;; cross-version within rules&lt;/td&gt;
&lt;td&gt;Very large DB, cross-endian platform exit, want a short cutover&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Data Guard&lt;/strong&gt; (standby → switchover)&lt;/td&gt;
&lt;td&gt;Block-identical standby at the target, then switchover&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Near-zero&lt;/strong&gt; (seconds–minutes)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — same endian &lt;em&gt;and&lt;/em&gt; same version&lt;/td&gt;
&lt;td&gt;Large DB, same architecture, near-zero downtime + easy rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;GoldenGate&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Logical replication — initial load, then change capture/apply, then cut over&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Near-zero&lt;/strong&gt; (sub-minute)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; — version, platform, endian, even heterogeneous&lt;/td&gt;
&lt;td&gt;Large DB + minimal downtime &lt;em&gt;and&lt;/em&gt; crossing a boundary; need fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ZDM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Free Oracle tool that &lt;em&gt;orchestrates&lt;/em&gt; the methods above&lt;/td&gt;
&lt;td&gt;= whatever method it runs&lt;/td&gt;
&lt;td&gt;= whatever method it runs&lt;/td&gt;
&lt;td&gt;Migrating to OCI/Exadata/Database@Azure and you want it automated&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Data Pump: the universal translator
&lt;/h3&gt;

&lt;p&gt;Logical export/import recreates every object on the target, which is exactly &lt;em&gt;why&lt;/em&gt; it crosses version, platform, endianness, and character set that the physical methods can't. The cost is downtime&lt;br&gt;
proportional to data volume — the unload → reload → rebuild-indexes cycle, not just raw bytes.&lt;code&gt;PARALLEL&lt;/code&gt; and network mode (a direct &lt;code&gt;INSERT … SELECT&lt;/code&gt; over a database link, no dump file) help, but for a large database the outage can still be unacceptable.&lt;/p&gt;

&lt;p&gt;One myth to kill: the "source and target can't differ by more than two releases" rule applies &lt;strong&gt;only over a network link&lt;/strong&gt; — dump-file transfers span a far wider version range. For moving a &lt;em&gt;whole&lt;/em&gt;&lt;br&gt;
database there's &lt;strong&gt;Full Transportable Export/Import (FTEX)&lt;/strong&gt;, which combines transportable datafiles for your user data with Data Pump metadata for the rest (source 11.2.0.3+ → target 12c+). And for&lt;br&gt;
&lt;strong&gt;Autonomous Database&lt;/strong&gt;, Data Pump &lt;em&gt;is&lt;/em&gt; the answer: stage the dump files in object storage, run as &lt;code&gt;ADMIN&lt;/code&gt; (never &lt;code&gt;SYS&lt;/code&gt;), and exclude the objects ADB manages itself — Oracle's canonical recipe is&lt;br&gt;
&lt;code&gt;exclude=cluster,indextype,db_link&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Transportable tablespaces and XTTS: physical, but portable
&lt;/h3&gt;

&lt;p&gt;Transportable tablespaces copy datafiles directly and carry only a small Data Pump metadata export — so the bulk data is never logically unloaded, which is why it's far faster than Data Pump for large databases. Same-endian is a plain file copy; &lt;strong&gt;cross-endian requires &lt;code&gt;RMAN CONVERT&lt;/code&gt;&lt;/strong&gt; to flip each block's byte order. Classic TTS needs the tablespaces &lt;strong&gt;&lt;code&gt;READ ONLY&lt;/code&gt;&lt;/strong&gt; for the entire copy window — and &lt;em&gt;that read-only window is your downtime.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;XTTS&lt;/strong&gt; (cross-platform transportable with RMAN incremental backups) is the trick that shrinks it: take a level-0 backup while the source stays fully online, roll the target copy forward with level-1 incrementals (still online, no downtime), and only the &lt;strong&gt;final&lt;/strong&gt; increment needs the tablespaces read only — so cutover is near-constant regardless of database size. This is the MAA-recommended way off an old big-endian UNIX box onto Linux x86-64. It's Enterprise Edition only, and mind the things that don't&lt;br&gt;
transport: &lt;code&gt;TIMESTAMP WITH TIME ZONE&lt;/code&gt; columns are skipped (Data Pump warns you) across mismatched time-zone file versions, TDE column-encrypted tables can't be in a transport set at all, and external tables, directory objects, and BFILEs aren't in the datafiles — copy and recreate those by hand.&lt;/p&gt;
&lt;h3&gt;
  
  
  Data Guard: relocate without transforming
&lt;/h3&gt;

&lt;p&gt;When the move &lt;em&gt;doesn't&lt;/em&gt; cross a boundary — same endianness, same version, same character set — Data Guard is the gold standard. Build a physical standby at the target, let Redo Apply catch it up while the source keeps serving traffic, then &lt;strong&gt;switch over&lt;/strong&gt;. The only downtime is the switchover itself: seconds to minutes. And it has the best rollback story of any method — after cutover the old primary automatically becomes a standby of the new one, so if the cloud primary misbehaves you simply &lt;strong&gt;switch back&lt;/strong&gt;, with no data loss.&lt;/p&gt;

&lt;p&gt;The constraints are the flip side of that fidelity: &lt;strong&gt;same endian&lt;/strong&gt; (a physical standby is block-for-block, so cross-endian is out), &lt;strong&gt;same DB version&lt;/strong&gt; (a switchover is &lt;em&gt;not&lt;/em&gt; an upgrade path — upgrading in flight is a separate transient-logical procedure), &lt;strong&gt;Enterprise Edition&lt;/strong&gt;, and the source in &lt;strong&gt;&lt;code&gt;ARCHIVELOG&lt;/code&gt; + &lt;code&gt;FORCE LOGGING&lt;/code&gt;&lt;/strong&gt;. This is the lift-and-shift method: same architecture in, same architecture out, tiny cutover. It runs on the same&lt;br&gt;
switchover/failover machinery covered in &lt;a href="https://uptimearchitect.com/blog/oracle-ha-decision-tree-rac-vs-data-guard/" rel="noopener noreferrer"&gt;The Oracle HA Decision&lt;br&gt;
Tree&lt;/a&gt; and detailed in &lt;a href="https://uptimearchitect.com/blog/oracle-data-guard-switchover-vs-failover/" rel="noopener noreferrer"&gt;Data Guard Switchover vs&lt;br&gt;
Failover&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  GoldenGate: near-zero downtime across &lt;em&gt;any&lt;/em&gt; boundary
&lt;/h3&gt;

&lt;p&gt;GoldenGate is logical replication: load the target (typically with Data Pump), then capture changes from the source's redo and apply them to keep the target in sync, and when replication lag is near zero, quiesce the app and reconnect it to the target. Cutover is bounded only by drain + reconnect, so a sub-minute outage is achievable. It's the one method that gives near-zero downtime &lt;strong&gt;and&lt;/strong&gt; crosses every boundary — version (11g → 26ai in a single move), platform, endianness, even heterogeneous (Oracle ↔&lt;br&gt;
non-Oracle) — with transformation in flight. Bidirectional replication gives you a genuine &lt;em&gt;fallback&lt;/em&gt; after cutover, which is nearly unique among migration methods.&lt;/p&gt;

&lt;p&gt;The price is real: GoldenGate is &lt;strong&gt;separately licensed&lt;/strong&gt; (or consumed as the managed OCI GoldenGate service), it's the &lt;strong&gt;most complex&lt;/strong&gt; method to run (supplemental logging on the source, Extract/Replicat processes, lag monitoring), and it &lt;strong&gt;can't replicate everything&lt;/strong&gt; — certain materialized views (such as those&lt;br&gt;
created &lt;code&gt;WITH ROWID&lt;/code&gt;), some LOB/UDT/spatial types, and tables with no usable row key fall outside it and get reloaded via Data Pump instead. Reach for it when you need minimal downtime on a&lt;br&gt;
large database that's &lt;em&gt;also&lt;/em&gt; crossing a boundary, or when you want that fallback net.&lt;/p&gt;
&lt;h3&gt;
  
  
  ZDM: the orchestrator, not a sixth method
&lt;/h3&gt;

&lt;p&gt;You rarely wire these up by hand for a real migration. &lt;strong&gt;Zero Downtime Migration (ZDM)&lt;/strong&gt; is Oracle's free, supported, CLI-driven tool that &lt;em&gt;orchestrates&lt;/em&gt; them: it never moves data itself — it drives RMAN, Data Guard, Data Pump, and GoldenGate for you, wrapped in pre-checks, pause/resume, evaluation dry-runs, auditing, and fallback. It comes in two families that map exactly onto the physical/logical split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Physical online&lt;/strong&gt; — RMAN + Data Guard standby + switchover. Same platform/endian, Enterprise Edition; near-zero cutover (typically under 15 minutes). ZDM can fold a version upgrade into the same job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logical online&lt;/strong&gt; — Data Pump initial load + GoldenGate change capture. Crosses boundaries. Minimal downtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Each also has an &lt;em&gt;offline&lt;/em&gt; variant that trades the near-zero cutover for not needing GoldenGate or a standby — and offline physical is the only path for Standard Edition.) If you're migrating to OCI, Exadata, or Database@Azure, ZDM is how you do it &lt;em&gt;repeatably&lt;/em&gt; — especially across a fleet — instead of hand-scripting. It inherits every constraint of the method underneath, so it doesn't change the &lt;em&gt;decision&lt;/em&gt;; it automates the &lt;em&gt;execution&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The target decides what's even possible
&lt;/h2&gt;

&lt;p&gt;Here's the gate people skip: &lt;strong&gt;the method you can use is capped by how much of the target database you control.&lt;/strong&gt; Physical methods need SYSDBA, OS/file access, and a database you can mount and recover.&lt;br&gt;
Logical methods need only a SQL connection and a privileged schema. Two of the three common targets give you a full database; one does not.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OCI Exadata Database Service &amp;amp; Base Database Service&lt;/strong&gt; — full Enterprise Edition databases you  administer, with SYSDBA and OS access. &lt;strong&gt;Everything works&lt;/strong&gt;: RMAN, physical Data Guard, transportable tablespaces, Data Pump, GoldenGate, and ZDM in both modes. The most flexible target.- &lt;strong&gt;Oracle Database@Azure&lt;/strong&gt; — the &lt;em&gt;same&lt;/em&gt; Exadata Database Service, running on Oracle-managed Exadata hardware physically located &lt;strong&gt;inside Azure data centers&lt;/strong&gt;, on your Azure VNet and billed through Azure.
Because it's the same service, you get the &lt;strong&gt;same full toolbox&lt;/strong&gt;, including standing up a &lt;strong&gt;physical Data Guard standby straight from on-prem into Azure and switching over&lt;/strong&gt; — something you can't do into a managed PaaS database. That's the genuine both-clouds differentiator. (Don't confuse it with the older &lt;em&gt;"Oracle Database Service for Azure,"&lt;/em&gt; which keeps the database in OCI and bridges over the Azure interconnect — a connectivity pattern, not the same target.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous Database (ADB)&lt;/strong&gt; — fully managed, &lt;strong&gt;no SYSDBA, no file access&lt;/strong&gt;. So physical methods are  out entirely: &lt;strong&gt;into Autonomous it's logical only&lt;/strong&gt; — Data Pump (dump files from object storage) or GoldenGate. Its character set defaults to Unicode (&lt;strong&gt;AL32UTF8&lt;/strong&gt;) and can't be changed after provisioning, so a non-Unicode source has to converge to  Unicode — run the &lt;strong&gt;Cloud Premigration Advisor Tool (CPAT)&lt;/strong&gt; first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb: Exadata and Database@Azure = physical + logical; Autonomous = logical only.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The decision, in one tree
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Choosing an Oracle cloud migration method. The target and any boundary crossing eliminate options first; downtime tolerance picks between what's left. ZDM automates whichever physical or logical path you land on.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A{Target = Autonomous DB?} -- Yes --&amp;gt; B{Downtime budget?}
  B -- Outage OK --&amp;gt; B1[Data Pump&amp;lt;br/&amp;gt;via object storage]
  B -- Near-zero --&amp;gt; B2[GoldenGate&amp;lt;br/&amp;gt;logical online]
  A -- "No — you control the DB&amp;lt;br/&amp;gt;(Exadata / Base DB / Database@Azure)" --&amp;gt; C{Crossing endian&amp;lt;br/&amp;gt;or upgrading version?}
  C -- "Yes (cross-endian / cross-version)" --&amp;gt; D{Downtime budget?}
  D -- Outage OK --&amp;gt; D1[Data Pump or&amp;lt;br/&amp;gt;XTTS + RMAN CONVERT]
  D -- Near-zero --&amp;gt; D2[GoldenGate&amp;lt;br/&amp;gt;logical online]
  C -- "No (same arch)" --&amp;gt; E{Downtime budget?}
  E -- Near-zero --&amp;gt; E1[Data Guard&amp;lt;br/&amp;gt;physical online / ZDM]
  E -- Outage OK --&amp;gt; E2[RMAN restore or&amp;lt;br/&amp;gt;Data Pump]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What teams get wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Picking the destination before the method.&lt;/strong&gt; The cloud is the easy part; the &lt;em&gt;method&lt;/em&gt; is where downtime and risk actually live. Decide how you'll move &lt;em&gt;before&lt;/em&gt; you argue about where.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assuming Data Guard or RMAN can cross endianness.&lt;/strong&gt; They can't — block-identical means same endian.
A big-endian UNIX box (AIX/Solaris/HP-UX) → Linux x86-64 is a &lt;em&gt;logical&lt;/em&gt; or &lt;em&gt;XTTS-with-&lt;code&gt;CONVERT&lt;/code&gt;&lt;/em&gt; job, never a straight physical restore. Check &lt;code&gt;V$TRANSPORTABLE_PLATFORM&lt;/code&gt; first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treating a Data Guard switchover as an upgrade path.&lt;/strong&gt; Same version required. Upgrading during the   move is a different (transient-logical) procedure — don't plan to "just switch over to the new version."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting what the target allows.&lt;/strong&gt; Designing a physical migration &lt;em&gt;into Autonomous&lt;/em&gt;, which can't accept one. Confirm the target's capabilities before you design the migration, not after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Underestimating GoldenGate.&lt;/strong&gt; "Near-zero downtime" sounds free; it's separately licensed, the most complex method to operate, and it won't replicate every object. Budget for the license &lt;em&gt;and&lt;/em&gt; the labor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sizing downtime as the whole copy.&lt;/strong&gt; For Data Guard, XTTS, and GoldenGate the copy and sync are &lt;em&gt;online&lt;/em&gt;; only the cutover is downtime — minutes, not the hours it takes to move the data.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Want to drill the decision?&lt;/strong&gt; The &lt;a href="https://github.com/pyaroslav/oracle-labs/tree/main/migration-methods" rel="noopener noreferrer"&gt;cloud-migration method-selection lab&lt;/a&gt; gives you six realistic scenarios — source platform, version, size, downtime budget, and target (OCI, Database@Azure, Autonomous) — and you pick the best method and justify it, with a &lt;code&gt;grade.sh&lt;/code&gt; self-check. No install  and no cloud account: it trains the &lt;em&gt;judgment&lt;/em&gt;, not the keystrokes. (Two scenarios hide a trap real teams fall into.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;Migration is one piece of running Oracle in the cloud — the rest of the cluster lives in &lt;a href="https://uptimearchitect.com/pillars/cloud-migration/" rel="noopener noreferrer"&gt;Cloud &amp;amp; Migration&lt;/a&gt;. The near-zero-downtime methods here lean on the same switchover/failover machinery as &lt;a href="https://uptimearchitect.com/blog/oracle-ha-decision-tree-rac-vs-data-guard/" rel="noopener noreferrer"&gt;The Oracle HA Decision Tree&lt;/a&gt;. And the question that decides &lt;em&gt;where&lt;/em&gt; you land&lt;br&gt;
with the full toolbox above — &lt;strong&gt;OCI versus Oracle Database@Azure&lt;/strong&gt; — is its own deep dive, coming next in this pillar.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the best way to migrate an Oracle database to the cloud?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is no single best method — the right one is determined by four things: how much downtime you can afford, whether you are crossing a boundary (endianness, version, platform, or character set), the database size, and what the target allows. Data Pump is simplest but means a full outage; Data Guard gives near-zero downtime when the architecture does not change; GoldenGate gives near-zero downtime when it does; transportable tablespaces (XTTS) move very large databases with a short cutover; and ZDM automates whichever of these fits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between physical and logical Oracle migration?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Physical methods (RMAN restore, transportable tablespaces, Data Guard) copy data blocks verbatim, so they are fast and faithful but require the same endianness on both ends — and, for Data Guard, the same database version. Logical methods (Data Pump, GoldenGate) recreate each object on the target, so they cross version, platform, endianness, and character-set boundaries, at the cost of downtime (Data Pump) or licensing and complexity (GoldenGate).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I use Data Guard to migrate across different platforms or versions?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Only within strict limits. A physical standby is block-for-block identical to the primary, so it requires the same endian format and the same database version — a switchover is not an upgrade path. It is ideal for a same-architecture lift-and-shift (for example, on-prem Linux x86-64 to OCI Exadata) with near-zero downtime and easy rollback. If you are crossing endianness or changing version, use a logical method (GoldenGate or Data Pump) or transportable tablespaces with RMAN CONVERT instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I migrate to Oracle Autonomous Database?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Logical methods only. Autonomous Database is fully managed with no SYSDBA or file-system access, so physical methods (RMAN restore, Data Guard standby, transportable tablespaces) are not possible. Use Data Pump — with dump files staged in object storage, run as ADMIN rather than SYS, excluding objects ADB manages (cluster, indextype, db_link) — for an outage-tolerant move, or GoldenGate for near-zero downtime. Autonomous defaults to the Unicode AL32UTF8 character set, which cannot be changed after provisioning, so run the Cloud Premigration Advisor Tool (CPAT) first if your source is not already Unicode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Oracle Zero Downtime Migration (ZDM)?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ZDM is a free, Oracle-supported tool that orchestrates a migration to OCI, Exadata, or &lt;a href="mailto:Database@Azure"&gt;Database@Azure&lt;/a&gt;. It does not move data itself — it drives the underlying methods (RMAN, Data Guard, Data Pump, GoldenGate) with pre-checks, pause/resume, and fallback. It has a physical mode (RMAN + Data Guard switchover, for same-endian/same-version moves) and a logical mode (Data Pump + GoldenGate, which crosses boundaries and is the only path into Autonomous). It inherits the constraints of whichever method it runs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does GoldenGate give true zero downtime?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Near-zero, not literally zero. The source stays online during the initial load and change replication; the only downtime is the brief window to drain in-flight transactions and reconnect the application to the target, which can be sub-minute. Bidirectional replication can keep the original source current after cutover to enable a fallback. Note that GoldenGate is separately licensed (or consumed as the managed OCI GoldenGate service) and is the most complex method to operate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I migrate Oracle from AIX or Solaris (big-endian) to the cloud?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is a cross-endian move, because AIX and Solaris SPARC are big-endian while Linux x86-64 (the usual cloud target) is little-endian. A straight RMAN restore or a Data Guard standby will not work across different endianness. Use cross-platform transportable tablespaces (XTTS) with RMAN CONVERT — which uses incremental backups to keep downtime to the final read-only increment — or a logical method (GoldenGate for near-zero downtime, or Data Pump if an outage is acceptable).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is Oracle Database@Azure different from OCI for migration?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For migration purposes it behaves the same as OCI Exadata Database Service, because that is exactly what it is — Oracle-managed Exadata infrastructure running inside Azure data centers. So the full toolbox applies, including physical Data Guard standby-in and switchover from on-prem straight into Azure. That sets it apart from Autonomous Database (logical methods only) and from the older "Oracle Database Service for Azure," which keeps the database in OCI and connects over the Azure interconnect rather than running Exadata in Azure itself.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://uptimearchitect.com/blog/oracle-cloud-migration-methods/" rel="noopener noreferrer"&gt;uptimearchitect.com&lt;/a&gt;. I write here in a personal capacity — questions or feedback are welcome via the &lt;a href="https://uptimearchitect.com/contact/" rel="noopener noreferrer"&gt;contact page&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>database</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>ORA-00060 Deadlock: Find It, Fix It, Prevent It</title>
      <dc:creator>Uptime Architect</dc:creator>
      <pubDate>Wed, 08 Jul 2026 19:20:23 +0000</pubDate>
      <link>https://dev.to/uptimearchitect/ora-00060-deadlock-find-it-fix-it-prevent-it-4o9a</link>
      <guid>https://dev.to/uptimearchitect/ora-00060-deadlock-find-it-fix-it-prevent-it-4o9a</guid>
      <description>&lt;p&gt;&lt;code&gt;ORA-00060: deadlock detected while waiting for resource&lt;/code&gt; is one of the most misunderstood errors Oracle throws. The two myths that cause the most damage: that it rolled back your &lt;em&gt;transaction&lt;/em&gt; (it&lt;br&gt;
didn't — just one statement), and that it's a database &lt;em&gt;tuning&lt;/em&gt; problem you fix with a parameter (it isn't — it's almost always an application bug). Oracle already broke the deadlock for you. Your job is to read the trace it left, find the two statements that collided, and stop it happening again.&lt;/p&gt;

&lt;p&gt;Here's exactly what the error means, how to read the deadlock graph, the handful of patterns that cause deadlocks, and the fix for each.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The short version.&lt;/strong&gt; A deadlock is a &lt;em&gt;circular&lt;/em&gt; wait — session A holds a row B wants while B holds a row A wants. Oracle &lt;strong&gt;detects it automatically&lt;/strong&gt; (within a few seconds) and breaks it by &lt;strong&gt;rolling&lt;br&gt;
back one statement&lt;/strong&gt; of one session — the "victim" — which gets &lt;code&gt;ORA-00060&lt;/code&gt;. The transaction &lt;strong&gt;survives&lt;/strong&gt; (everything before that statement is intact); the &lt;em&gt;other&lt;/em&gt; session proceeds as if nothing&lt;br&gt;
happened. The trace file holds a &lt;strong&gt;deadlock graph&lt;/strong&gt; naming the two transactions, the rows, and the two SQL statements. Mode-6 (&lt;code&gt;X&lt;/code&gt;) deadlocks mean &lt;em&gt;fix your app's lock order&lt;/em&gt;; mode-4 (&lt;code&gt;S&lt;/code&gt;) deadlocks mean &lt;em&gt;look at the data structure&lt;/em&gt; (unindexed foreign key, ITL shortage, bitmap index).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  What ORA-00060 actually does
&lt;/h2&gt;

&lt;p&gt;When two sessions wait on each other's locks, neither can proceed — that's a deadlock. Oracle runs a background detector that notices the wait-for cycle (typically within ~3 seconds) and breaks it&lt;br&gt;
&lt;strong&gt;without any timeout or configuration from you&lt;/strong&gt;. Precisely what happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It rolls back one &lt;em&gt;statement&lt;/em&gt;, not the transaction.&lt;/strong&gt; Only the single DML that closed the cycle is  undone. Everything that transaction did before it is still there, and still holds its locks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The transaction is not terminated.&lt;/strong&gt; The session that gets &lt;code&gt;ORA-00060&lt;/code&gt; is still in an open transaction, holding a half-finished change. Oracle hands it back to you to decide: retry the
statement, roll back to a savepoint, or roll back the whole thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only one session errors.&lt;/strong&gt; The &lt;em&gt;other&lt;/em&gt; session in the cycle gets its lock and &lt;strong&gt;continues normally&lt;/strong&gt; — it never knows a deadlock occurred. Either session can be the victim; Oracle chooses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The application has to handle it.&lt;/strong&gt; In PL/SQL, declare your own exception and bind it to the error with &lt;code&gt;PRAGMA EXCEPTION_INIT(deadlock_detected, -60)&lt;/code&gt;, then retry after a short backoff. (There is no built-in &lt;code&gt;DEADLOCK_DETECTED&lt;/code&gt; exception — ORA-00060 isn't one of PL/SQL's predefined exceptions.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the single most common belief — &lt;em&gt;"a deadlock rolled back my transaction"&lt;/em&gt; — is false. It rolled back &lt;strong&gt;one statement&lt;/strong&gt; and left the rest of your transaction (and its locks) in place, waiting for your code to react.&lt;/p&gt;
&lt;h2&gt;
  
  
  Deadlock or just blocking? (the #1 confusion)
&lt;/h2&gt;

&lt;p&gt;A stuck session looks the same whether it's deadlocked or merely &lt;em&gt;blocked&lt;/em&gt; — but they're opposite problems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Deadlock (&lt;code&gt;ORA-00060&lt;/code&gt;)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Ordinary blocking&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Shape&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Circular&lt;/strong&gt; — A waits on B &lt;em&gt;and&lt;/em&gt; B waits on A&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;One-way&lt;/strong&gt; — A waits on B only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Oracle's response&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Auto-detects and breaks it&lt;/strong&gt; in seconds&lt;/td&gt;
&lt;td&gt;Does &lt;strong&gt;nothing&lt;/strong&gt; — waits indefinitely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ends when&lt;/td&gt;
&lt;td&gt;Immediately — victim gets &lt;code&gt;ORA-00060&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The &lt;strong&gt;blocker commits or rolls back&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wait event&lt;/td&gt;
&lt;td&gt;(transient, then the victim errors)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;enq: TX - row lock contention&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error raised?&lt;/td&gt;
&lt;td&gt;Yes — to the victim&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; — just a hung session&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tell: a session parked on &lt;strong&gt;&lt;code&gt;enq: TX - row lock contention&lt;/code&gt;&lt;/strong&gt; is &lt;strong&gt;blocked, not deadlocked&lt;/strong&gt;. It will sit there forever until the holder commits, and it will &lt;em&gt;never&lt;/em&gt; raise &lt;code&gt;ORA-00060&lt;/code&gt;. Mutual wait =&lt;br&gt;
deadlock; one-way wait = blocking. Don't go hunting for a deadlock graph when the real problem is one long-running transaction holding a lock — that's a different fix (find and commit/kill the blocker).&lt;br&gt;
The wait interface that surfaces that event is covered in &lt;a href="https://uptimearchitect.com/blog/oracle-wait-events-decoded/" rel="noopener noreferrer"&gt;Oracle Wait Events, Decoded&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Reading the deadlock graph
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ORA-00060&lt;/code&gt; is a critical error, so Oracle writes a trace file and notes it in the &lt;strong&gt;alert log&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORA-00060: Deadlock detected. ... More info in file
   /opt/oracle/diag/rdbms/.../trace/&amp;lt;sid&amp;gt;_ora_&amp;lt;pid&amp;gt;.trc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open that trace and find the &lt;strong&gt;&lt;code&gt;Deadlock graph&lt;/code&gt;&lt;/strong&gt; — the whole diagnosis is in this one block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deadlock graph:
                       ---------Blocker(s)--------  ---------Waiter(s)---------
Resource Name          process session holds waits  process session holds waits
TX-0006001a-000004f2        19     137     X             24     159           X
TX-00030028-000003a1        24     159     X             19     137           X
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as the cycle it describes: session 137 &lt;strong&gt;holds&lt;/strong&gt; a lock (mode &lt;code&gt;X&lt;/code&gt;) that session 159 &lt;strong&gt;waits&lt;/strong&gt; for, and session 159 holds one that 137 waits for. The fields that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource name / enqueue type.&lt;/strong&gt; &lt;code&gt;TX-…&lt;/code&gt; is a &lt;strong&gt;transaction (row-level) enqueue&lt;/strong&gt; — the usual case.&lt;code&gt;TM-…&lt;/code&gt; is a &lt;strong&gt;table/DML enqueue&lt;/strong&gt; — the fingerprint of the unindexed-foreign-key problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;holds / waits mode.&lt;/strong&gt; &lt;code&gt;X&lt;/code&gt; = exclusive (&lt;strong&gt;mode 6&lt;/strong&gt;); &lt;code&gt;S&lt;/code&gt; = share (&lt;strong&gt;mode 4&lt;/strong&gt; — which also covers the ITL, unique-key, and bitmap cases below); modes 3/5 (&lt;code&gt;SX&lt;/code&gt;/&lt;code&gt;SSX&lt;/code&gt;) show up in TM (table-lock) cases. &lt;em&gt;This mode is your best clue to the cause.&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rows waited on.&lt;/strong&gt; Just below the graph, Oracle prints the exact &lt;strong&gt;rowid / object#&lt;/strong&gt; each session was blocked on — which row, and which table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The two SQL statements.&lt;/strong&gt; The trace then prints each session's &lt;code&gt;current SQL statement&lt;/code&gt; — the two DMLs that crossed. That's the bug, named.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mode is the shortcut: &lt;strong&gt;mode 6 (&lt;code&gt;X&lt;/code&gt;) = a row-lock collision → fix the application's lock order; mode 4 (&lt;code&gt;S&lt;/code&gt;) = a structural problem → look at the data&lt;/strong&gt; (a unique-key clash, an ITL shortage, a bitmap index, or an unindexed FK).&lt;/p&gt;

&lt;h2&gt;
  
  
  The causes, and the fix for each
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;ORA-00060 triage: read the deadlock graph, let the enqueue type and lock mode point you at the cause, and fix the cause — not a parameter.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart TD
  A([ORA-00060 in the alert log]) --&amp;gt; B[Open the trace&amp;lt;br/&amp;gt;read the Deadlock graph]
  B --&amp;gt; C{Enqueue type&amp;lt;br/&amp;gt;and mode?}
  C -- "TM (table lock)" --&amp;gt; D[Unindexed foreign key&amp;lt;br/&amp;gt;→ index the FK column]
  C -- "TX mode 6 (X)" --&amp;gt; E[Inconsistent update order&amp;lt;br/&amp;gt;→ lock rows in one consistent order]
  C -- "TX mode 4 (S)" --&amp;gt; F{Which structural cause?}
  F --&amp;gt; F1[Same unique/PK value inserted&amp;lt;br/&amp;gt;→ app logic / sequence]
  F --&amp;gt; F2[ITL shortage on a hot block&amp;lt;br/&amp;gt;→ raise INITRANS / PCTFREE]
  F --&amp;gt; F3[Bitmap index on an OLTP table&amp;lt;br/&amp;gt;→ use a B-tree index]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1. Inconsistent update order&lt;/strong&gt; — the classic TX mode-6 deadlock. Session A updates row X then Y; session B updates Y then X. Each holds an exclusive row lock the other wants. &lt;strong&gt;Fix:&lt;/strong&gt; update rows in a single, deterministic order everywhere (e.g., ascending by primary key) — if every code path locks in the same order, a cycle is impossible. Batch jobs are the usual offenders; sort the working set before the DML loop, and keep transactions short.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Unindexed foreign keys&lt;/strong&gt; — the most famous Oracle deadlock cause. When a child table's foreign-key column is &lt;strong&gt;not indexed&lt;/strong&gt; and a session updates the parent's key, deletes a parent row, or merges into the parent, Oracle takes a &lt;strong&gt;full-table lock on the child table&lt;/strong&gt; (a TM share lock) — because without the index it can't cheaply find the referencing rows. That coarse lock collides with other DML and deadlocks, blocking even &lt;em&gt;unrelated&lt;/em&gt; rows. (Note: plain &lt;em&gt;inserts&lt;/em&gt; into the parent don't trigger it.)&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; put a plain B-tree index on the child's FK column. Oracle's own rule of thumb is that foreign keys should almost always be indexed — the only exception is a parent key that's never updated or&lt;br&gt;
deleted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Bitmap indexes on OLTP tables&lt;/strong&gt; — a single bitmap key entry covers &lt;em&gt;many&lt;/em&gt; rows, so DML on one row&lt;br&gt;
locks the whole entry, and two sessions updating &lt;em&gt;different&lt;/em&gt; rows can collide. &lt;strong&gt;Fix:&lt;/strong&gt; don't put bitmap&lt;br&gt;
indexes on tables with concurrent DML — they're for low-cardinality, read-mostly warehouse data. Use a&lt;br&gt;
B-tree for OLTP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. ITL / &lt;code&gt;INITRANS&lt;/code&gt; shortage&lt;/strong&gt; — every block has an Interested Transaction List with one slot per concurrent transaction touching it; &lt;code&gt;INITRANS&lt;/code&gt; sets the initial count (default 1 for tables, 2 for&lt;br&gt;
indexes) and Oracle grows it &lt;em&gt;only if the block has free space&lt;/em&gt;. On a hot block with no room to grow, a transaction waits on the &lt;strong&gt;TX enqueue in mode 4&lt;/strong&gt; for a slot — and two such waits deadlock. &lt;strong&gt;Fix:&lt;/strong&gt; rebuild the hot segment with a higher &lt;code&gt;INITRANS&lt;/code&gt; and/or &lt;code&gt;PCTFREE&lt;/code&gt;, and spread the concurrency. (&lt;code&gt;MAXTRANS&lt;/code&gt; is deprecated — Oracle now allows up to 255 transactions per block automatically, space permitting — so the lever is &lt;code&gt;INITRANS&lt;/code&gt;/&lt;code&gt;PCTFREE&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Autonomous-transaction self-deadlock&lt;/strong&gt; — a &lt;code&gt;PRAGMA AUTONOMOUS_TRANSACTION&lt;/code&gt; routine tries to update a row the &lt;em&gt;calling&lt;/em&gt; transaction already locked. The autonomous transaction waits on the parent's lock, but the parent is suspended waiting for the autonomous child to return — an unbreakable cycle. &lt;strong&gt;Fix:&lt;/strong&gt; never&lt;br&gt;
let an autonomous transaction touch rows its caller has locked; keep it to independent resources (a separate logging table).&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevention checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Index every foreign key&lt;/strong&gt; whose parent key can be updated or whose parent rows can be deleted. This kills the #1 source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock resources in one deterministic order&lt;/strong&gt; (e.g., ascending PK) across the whole application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep transactions short&lt;/strong&gt; and commit promptly — don't hold locks across user think-time or remote calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No bitmap indexes on OLTP tables&lt;/strong&gt; — B-tree instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Raise &lt;code&gt;INITRANS&lt;/code&gt;/&lt;code&gt;PCTFREE&lt;/code&gt;&lt;/strong&gt; on hot blocks prone to ITL contention.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit explicit locking&lt;/strong&gt; — &lt;code&gt;SELECT … FOR UPDATE&lt;/code&gt;, &lt;code&gt;LOCK TABLE&lt;/code&gt;, and ORM pessimistic locking are where most "explicit override" deadlocks live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the app retry on &lt;code&gt;ORA-00060&lt;/code&gt;&lt;/strong&gt; — a deterministic retry after a short backoff resolves the transient cases cleanly. Then read the trace to fix the root pattern.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Want to see one for real?&lt;/strong&gt; The &lt;a href="https://github.com/pyaroslav/oracle-labs/tree/main/deadlock" rel="noopener noreferrer"&gt;deadlock lab&lt;/a&gt;&lt;br&gt;
induces an actual &lt;code&gt;ORA-00060&lt;/code&gt; on Oracle Database Free — two sessions lock the same two rows in opposite order — and prints the &lt;strong&gt;deadlock graph&lt;/strong&gt; straight from the trace. Then a &lt;code&gt;fixed&lt;/code&gt; drill runs the &lt;em&gt;same&lt;/em&gt; workload in a consistent order so the deadlock never forms. One command each; no Diagnostics Pack.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What teams get wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"The deadlock rolled back my transaction."&lt;/strong&gt; It rolled back &lt;em&gt;one statement&lt;/em&gt;. The transaction is still
open, holding its earlier work and locks — your code has to react.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confusing a deadlock with blocking.&lt;/strong&gt; A session on &lt;code&gt;enq: TX - row lock contention&lt;/code&gt; is &lt;em&gt;blocked&lt;/em&gt;, not
deadlocked — it'll wait forever, and no &lt;code&gt;ORA-00060&lt;/code&gt; is coming. Find the blocker; don't hunt a graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trying to "tune" it away.&lt;/strong&gt; There's no parameter that fixes a deadlock. The graph names the two SQL
statements; the fix is in the application (lock order) or the schema (index that FK).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ignoring the lock mode.&lt;/strong&gt; Mode 6 vs mode 4 tells you whether the bug is your &lt;em&gt;lock order&lt;/em&gt; or your
&lt;em&gt;data structure&lt;/em&gt;. Read it before you guess.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where this fits
&lt;/h2&gt;

&lt;p&gt;Deadlocks are a concurrency problem, and concurrency shows up in the performance picture — when one is the headline in your report, you've found it through the wait interface and the AWR Top Events. Name the event, read the graph, follow it to the SQL: the same method as &lt;a href="https://uptimearchitect.com/blog/how-to-read-an-awr-report/" rel="noopener noreferrer"&gt;How to Read an AWR Report Without Drowning&lt;/a&gt; and &lt;a href="https://uptimearchitect.com/blog/oracle-wait-events-decoded/" rel="noopener noreferrer"&gt;Oracle Wait Events, Decoded&lt;/a&gt;, applied to the one error that diagnoses itself if you let it.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What does ORA-00060 mean?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ORA-00060, "deadlock detected while waiting for resource," means two or more sessions were each holding a lock the other needed — a circular wait. Oracle automatically detected the deadlock and broke it by rolling back one statement of one session (the victim), which receives the ORA-00060 error. The other session proceeds normally. It is almost always caused by an application locking pattern, not by a database misconfiguration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does ORA-00060 roll back my whole transaction?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No. Oracle rolls back only the single statement that closed the deadlock cycle, not the entire transaction. The session that receives ORA-00060 is still in an open transaction with all of its prior work and locks intact. You decide what to do next: retry the statement, roll back to a savepoint, or roll back the whole transaction. The application should handle the error and typically retry after a short delay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the difference between a deadlock and blocking in Oracle?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A deadlock is a circular wait — session A waits on B while B waits on A — and Oracle detects and breaks it automatically within seconds, raising ORA-00060 to one victim. Ordinary blocking is one-directional — A waits on B only — and Oracle does nothing about it; the waiting session sits on the wait event "enq: TX - row lock contention" indefinitely until the blocker commits or rolls back, and never raises ORA-00060. A session on enq: TX - row lock contention is blocked, not deadlocked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I find which SQL caused an ORA-00060 deadlock?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Oracle writes a trace file when the deadlock occurs and records its path in the alert log. Open that trace and find the "Deadlock graph" section: it lists the two transactions (as TX or TM enqueues), the lock modes each holds and waits for, the rows each session was waiting on, and — printed just below — the current SQL statement of each session. Those two statements are the colliding DML; that is the bug to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What causes Oracle deadlocks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The common causes are: inconsistent update ordering (two sessions updating the same rows in opposite order — a TX mode-6 deadlock); unindexed foreign keys (a parent-key update, delete, or merge takes a full-table lock on the child table when the FK column is not indexed); bitmap indexes on tables with concurrent DML; an ITL/INITRANS shortage on a hot block (a TX mode-4 deadlock); and autonomous transactions that touch rows locked by their caller.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do unindexed foreign keys cause deadlocks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a child table's foreign-key column is not indexed and a session updates the parent table's key, deletes a parent row, or merges into the parent, Oracle cannot cheaply identify the referencing child rows, so it locks the entire child table with a table-level (TM) share lock to protect integrity. That coarse lock blocks other DML on the child table — including unrelated rows — and collides with concurrent transactions, producing deadlocks. Indexing the foreign-key column lets Oracle avoid the full-table lock.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do I prevent ORA-00060 deadlocks?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Index every foreign key whose parent key can be updated or whose rows can be deleted; lock rows in a single deterministic order across the whole application (for example ascending by primary key); keep transactions short and commit promptly; avoid bitmap indexes on OLTP tables; raise INITRANS/PCTFREE on hot blocks prone to ITL contention; audit explicit locking such as SELECT ... FOR UPDATE; and make the application catch ORA-00060 and retry after a short backoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Should my application retry after ORA-00060?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes. Because Oracle rolls back only one statement and leaves the transaction open, a deadlock is often transient — a deterministic retry after a brief randomized backoff resolves it cleanly. In PL/SQL, associate the error with a named exception using PRAGMA EXCEPTION_INIT(my_deadlock, -60) and handle it. Retrying is a safety net, not a cure: still read the deadlock graph and fix the underlying lock-order or schema problem so it stops recurring.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://uptimearchitect.com/blog/oracle-ora-00060-deadlock/" rel="noopener noreferrer"&gt;uptimearchitect.com&lt;/a&gt;. I write here in a personal capacity — questions or feedback are welcome via the &lt;a href="https://uptimearchitect.com/contact/" rel="noopener noreferrer"&gt;contact page&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>database</category>
      <category>sql</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
