Initialization and updates
Deephaven code often looks like it runs sequentially, line by line, just like standard Python. But with ticking tables, which update as new data arrives, the data your code reads may not have arrived yet — and this surprises many developers.
This guide explains:
- Why ticking table code behaves differently than standard Python.
- The two phases of execution: initialization and updates.
- How to safely mix Python code with Deephaven ticking tables.
- Clear "do this, don't do that" guidelines.
Prerequisites
Before reading this guide, you should understand:
- Create static tables
- Table types (static vs refreshing)
- Basic table operations
For details on how Deephaven processes updates internally, see Incremental update model.
The surprise: a table from an external source can start empty
Consider this code:
In standard Python, this would work fine — create the data source, then read from it. But with a Kafka consumer, or any other external source whose data arrives only in later update cycles, the table is empty when you try to read from it. The data hasn't arrived yet, and it can't arrive while your script is running unless the script waits for it with await_update, as Update cycles and the exclusive lock explains.
This is the core issue: Deephaven table operations define relationships that the engine keeps up to date as data arrives. They don't run just once over the data that exists when you create them.
Two phases: initialization and updates
Deephaven execution has two distinct phases: initialization and real-time updates.
Phase 1: Initialization
When your script runs, table operations execute and build the DAG (dependency graph), and initial calculations run on any existing data.
Tables fed by external sources, such as Kafka, may not have data from those sources yet.
Phase 2: Real-time updates
After initialization completes, or while the script waits in await_update:
- Data flows in from sources such as Kafka and time tables.
- The engine propagates changes through the DAG.
- Derived tables update incrementally.
The problem occurs when you try to extract data to Python during initialization, before the real-time data has arrived.
Update cycles and the exclusive lock
To see why the data can't arrive while your script runs, it helps to understand the update cycle in more detail.
The engine component that processes real-time updates is the update graph. During the real-time updates phase, it repeats update cycles: in each cycle, it applies new data from sources and propagates the changes through the DAG. Between cycles, it is idle.
- Between cycles (idle): Tables are consistent. Console commands and Application Mode scripts run here.
- During a cycle (updating): Sources receive data, changes propagate through the DAG, and listeners fire. Listeners receive the cycle's changes as a consistent
TableUpdateobject, as Pattern 4 shows.
Deephaven runs each console command and Application Mode script while holding the update graph's exclusive lock, so no update cycle can run while your script runs. This has two consequences:
- Reads in your script see a consistent table. Your script doesn't need a lock or a snapshot for consistency.
- Data from external sources can't arrive until the script finishes, or until it calls
await_update, which releases the lock while it waits.
Code on threads you start yourself doesn't hold this lock. To keep an update cycle from starting while that code reads, hold the update graph's shared lock, as Pattern 6 shows.
Quick reference
Here's a quick summary of the patterns. Details follow in the sections below.
Do this, don't do that
| DO | DON'T |
|---|---|
| Use table operations — let the engine handle consistency. | Pull values into Python variables when a table operation can compute the result. |
Wait for data with await_update before extracting. | Extract data right after creating a ticking table, assuming the data is already there. |
| Use listeners for reacting to updates, and declare every other table they read as a dependency. | Access ticking tables other than the source or declared dependencies inside listeners. |
Compare before/after values within one update with modified_prev. | Mix data from different update cycles. |
| Take the shared lock only for quick extractions, then process outside it. | Hold the update graph lock during long computations. |
Initialize state with do_replay=True or with a snapshot taken before listening. | Start a stateful listener without the table's existing rows. |
Common scenarios
| Scenario | Recommended approach |
|---|---|
| Wait for Kafka/external data before extracting | await_update before extraction. |
| Export ticking data to pandas periodically | snapshot_when with a trigger table, and a listener on the result. Each trigger delivers the whole new snapshot as added rows. |
| React to updates and call an external API | Listener with added data access. |
| Aggregate ticking data with custom Python logic | Built-in aggregations with agg_by or update_by where they cover the logic; otherwise, call Python functions in formulas. |
| Initialize ML model from table state | do_replay=True in listener, or snapshot before listening. |
| Join ticking data with an external database | Take a snapshot of the ticking table, then join in Python. |
| Debug why data looks inconsistent | Check whether your own threads read several tables without holding one shared lock around all the reads. Check whether listeners read tables they don't declare as dependencies. Log the row counts each read sees. |
Waiting for data: the await_update pattern
The most common initialization problem is trying to read data before it arrives. In an Application Mode startup script that needs a value from an external source once, at startup, use await_update to pause until the table receives an update:
Note
await_update waits for the table to update. It can occasionally return early on a spurious wakeup, so the example loops until the table has rows.
You can also specify a timeout in milliseconds: trades.await_update(timeout=5000) returns False if the timeout is reached before an update occurs.
When not to use await_update
- In listeners — Listeners already receive data when it's ready, and calling
await_updatefrom a listener stalls the update cycle the listener runs in. - For continuous processing — Use table operations or listeners instead.
- In the console — Console code runs interactively. Wait for data to arrive, then run the next command.
Safe patterns for reading table data
Once data has arrived, or when you don't need to wait for it, these patterns get results out of ticking tables: keep the logic in the engine, keep a static copy, or take copies on a schedule.
Pattern 1: Let the engine do the work
The safest approach is to express your logic using Deephaven table operations rather than extracting data to Python. The engine guarantees consistency for all table operations.
If you need custom Python logic, consider calling a Python function in a formula. Keep the function to its arguments: the engine keeps the column values it passes consistent, but not other state the function reads.
Pattern 2: Use snapshot for point-in-time extraction
When you want a static Deephaven table that holds a ticking table's current rows, use snapshot. This example starts the time table 10 seconds in the past, so it already has rows when the snapshot runs:
The snapshot creates a static table that won't change, so you can process it in Python without later update cycles changing the data underneath you.
Note
to_pandas takes its own snapshot of a refreshing table, so calling it directly on a ticking table also returns a consistent DataFrame. Use snapshot when you want a static Deephaven table to keep working with.
Pattern 3: Use snapshot_when for controlled timing
When you need to extract data at specific intervals or in sync with other events, use snapshot_when:
snapshot_when is useful for:
- Reducing update frequency for downstream processing.
- Synchronizing data extraction with external systems.
- Batching updates for efficiency.
To export each snapshot to pandas, attach a listener to periodic_snapshot. Each trigger delivers the whole new snapshot as added rows, so the dictionary that added returns holds the full set of rows to export.
Safe patterns for reacting to updates
Pattern 4: Use listeners with TableUpdate
When you use table listeners, the TableUpdate object gives you safe, consistent access to changed data:
The added, modified, removed, and modified_prev methods return dictionaries with column names as keys and NumPy arrays as values. This data is a consistent snapshot from the current update cycle.
Caution
Keep listener execution fast. Until a running listener returns, the current update cycle can't finish and the next one can't start.
Pattern 5: Initialize listener state from existing rows
A listener that keeps state, such as a running total, usually needs to start from the rows the table already has. There are two ways to do that:
- Replay with
do_replay=True: The listener receives the existing rows as its first update. Use this when one function can handle both initialization and later updates. - Snapshot before listening: Your code processes a snapshot of the existing rows, then the listener handles later updates. Use this when initialization needs whole-table logic, such as a pandas computation.
With do_replay
When setting up a listener with listen, use do_replay=True to receive the table's existing data as the first update:
With a snapshot before listening
When initialization needs whole-table logic, take a snapshot first, then attach a listener for ongoing updates. Because the script holds the exclusive lock, no update cycle runs between the snapshot and the listen call, so the listener doesn't miss any rows:
Safe patterns for your own threads
Pattern 6: Use locking for consistent reads from your own threads (advanced)
Code on a thread you start yourself doesn't hold the exclusive lock that script code runs under. When several reads on such a thread must see the same update cycle, hold the update graph's shared lock around all of them with shared_lock.
A single to_pandas call doesn't need the lock, because to_pandas already snapshots its table.
In this example, extracting two tables under one lock guarantees both DataFrames come from the same cycle:
The thread reuses the script's execution context, because a thread you start doesn't inherit it.
The thread can't take the shared lock until the script finishes and releases the exclusive lock. Don't call thread.join in the same script, or the script hangs.
Caution
Keep lock duration minimal. See Don't hold locks longer than necessary.
Unsafe patterns
Don't read undeclared tables inside listeners
What can go wrong:
- The engine doesn't guarantee that other tables have finished updating for the current cycle.
- You may see partial or stale data from other tables.
Do instead: Within a listener, read only the table you are listening to, the ticking tables you declare as dependencies, and static tables:
Don't mix data from different update cycles
What can go wrong:
self.previous_dataholds the rows added in the previous cycle, not the earlier values of the rows in this update, so the comparison pairs unrelated rows.- If you store row keys instead, they may no longer be valid in a later cycle, because rows can be removed or shifted. See Incremental update model.
Do instead: Choose one of these approaches:
- For a before/after comparison, compare the previous and current values of modified rows within the same update.
modified_prevreturns the previous values of the modified rows. If the update also shifts those rows,modified_prevcan return values from the wrong rows; see Removed or modified values. - To carry information to the next cycle, store only computed results. Don't store raw row data.
Don't hold locks longer than necessary
When code on a thread you start yourself holds the shared lock, as in Pattern 6, no update cycle can run.
Script code holds the exclusive lock from start to finish, so a long computation in the script itself also blocks updates. Run long work on your own thread, or in a listener that hands it off to another thread.
What can go wrong:
- The lock blocks the entire update graph.
- All ticking tables freeze.
- Console commands wait, because they need the exclusive lock.
Do instead: Extract data quickly, release the lock, then process:
Related documentation
- Table listeners — React to table updates
- Incremental update model — Deep dive into how updates work
- Synchronization and locking — Advanced locking mechanisms
- Table types — Static vs refreshing tables
- Reduce update frequency — Optimize update processing