best-practices.md
281 lines · skills/dask/references/best-practices.md at 5f2bc03
Dask Best Practices
Reviewed with Dask/distributed 2026.8.0. File paths, deployment settings, and undefined application functions are illustrative; executed local checks and current official sources are in review.md.
Raw lines 1 to 4Performance Optimization Principles
Raw lines 5 to 6Start with Simpler Solutions First
Before implementing parallel computing with Dask, explore these alternatives:
- Better algorithms for the specific problem
- Efficient file formats (Parquet, HDF5, Zarr instead of CSV)
- Compiled code via Numba or Cython
- Data sampling for development and testing
These alternatives often provide better returns than distributed systems and should be exhausted before scaling to parallel computing.
Raw lines 7 to 16Chunk Size Strategy
Critical Rule: Chunks should be small enough that many fit in a worker's available memory at once.
Starting estimate: Allow several chunks per active thread plus outputs, temporary buffers, Python overhead, and shuffle data. Ten chunks per core is a heuristic, not a safe universal limit.
Why It Matters:
- Too large chunks: Memory overflow and inefficient parallelization
- Too small chunks: Excessive scheduling overhead
Example Calculation:
- 8 cores with 32 GB RAM
- 400 MB per chunk (32 GB / 8 cores / 10) would consume the entire nominal budget; start smaller to leave workspace and OS headroom. Measure uncompressed memory, not file size.
Monitor with the Dashboard
The Dask dashboard provides essential visibility into:
- Worker states and resource utilization
- Task progress and bottlenecks
- Memory usage patterns
- Performance characteristics
Access the dashboard to understand what's actually slow in parallel workloads rather than guessing at optimizations.
Raw lines 31 to 40Critical Pitfalls to Avoid
Raw lines 41 to 421. Don't Create Large Objects Locally Before Dask
Wrong Approach:
import pandas as pd
import dask.dataframe as dd
# Loads entire dataset into memory first
df = pd.read_csv('large_file.csv')
ddf = dd.from_pandas(df, npartitions=10)Correct Approach:
import dask.dataframe as dd
# Let Dask handle the loading
ddf = dd.read_csv('large_file.csv')Why: Loading data with pandas or NumPy first forces the scheduler to serialize and embed those objects in task graphs, defeating the purpose of parallel computing.
Key Principle: Use Dask methods to load data and use Dask to control the results.
Raw lines 43 to 662. Avoid Repeated compute() Calls
Wrong Approach:
results = []
for item in items:
result = dask_computation(item).compute() # Each compute is separate
results.append(result)Correct Approach:
computations = [dask_computation(item) for item in items]
results = dask.compute(*computations) # Single compute for allWhy: Calling compute in loops prevents Dask from:
- Parallelizing different computations
- Sharing intermediate results
- Optimizing the overall task graph
3. Don't Build Excessively Large Task Graphs
Symptoms:
- Millions of tasks in a single computation
- Severe scheduling overhead
- Long delays before computation starts
Solutions:
- Increase chunk sizes to reduce number of tasks
- Use
map_partitionsormap_blocksto fuse operations - Break computations into smaller pieces with intermediate persists
- Consider whether the problem truly requires distributed computing
Example Using map_partitions:
# Instead of applying function to each row
ddf['result'] = ddf.apply(complex_function, axis=1, meta=('result', 'float64'))
# Python row calls within partitions, not one Dask task per row
# Apply to entire partitions at once
ddf = ddf.map_partitions(lambda df: df.assign(result=complex_function(df)))Raw lines 88 to 110Infrastructure Considerations
Raw lines 111 to 112Scheduler Selection
Use Threads For:
- Numeric work with GIL-releasing libraries (NumPy, Pandas, scikit-learn)
- Operations that benefit from shared memory
- Single-machine workloads with array/dataframe operations
Use Processes For:
- Text processing and Python collection operations
- Pure Python code that's GIL-bound
- Operations that need process isolation
Use Distributed Scheduler For:
- Multi-machine clusters
- Need for diagnostic dashboard
- Asynchronous APIs
- Better data locality handling
Thread Configuration
Recommendation: Aim for roughly 4 threads per process on numeric workloads.
Rationale:
- Balance between parallelism and overhead
- Allows efficient use of CPU cores
- Reduces context switching costs
Memory Management
Persist Strategically: Persist reusable intermediates only after filtering and selecting columns; persistence blocks later optimizer pushdown. Distributed persistence stays on workers, while local persistence occupies local memory. It is not an automatic cure for memory errors.
# Persist intermediate results that are reused
intermediate = expensive_computation(data).persist()
result1 = intermediate.operation1().compute()
result2 = intermediate.operation2().compute()Clear Memory When Done:
# Explicitly delete large objects
del intermediateRaw lines 140 to 155Data Loading Best Practices
Raw lines 156 to 157Use Appropriate File Formats
For Tabular Data:
- Parquet: Columnar, compressed, fast filtering
- CSV: Only for small data or initial ingestion
For Array Data:
- HDF5: Good for numeric arrays
- Zarr: Cloud-native, parallel-friendly
- NetCDF: Scientific data with metadata
Optimize Data Ingestion
Read Multiple Files Efficiently:
# Use glob patterns to read multiple files in parallel
ddf = dd.read_parquet('data/year=2024/month=*/day=*.parquet')Specify Useful Columns Early:
# Only read needed columns
ddf = dd.read_parquet('data.parquet', columns=['col1', 'col2', 'col3'])Raw lines 169 to 182Common Patterns and Solutions
Raw lines 183 to 184Pattern: Embarrassingly Parallel Problems
For independent computations, use Futures:
from dask.distributed import Client
client = Client()
futures = [client.submit(func, arg) for arg in args]
results = client.gather(futures)Raw lines 185 to 195Pattern: Data Preprocessing Pipeline
Use Bags for initial ETL, then convert to structured formats:
import dask.bag as db
import json
# Process raw JSON Lines
bag = db.read_text('logs/*.json').map(json.loads)
bag = bag.filter(lambda x: x['status'] == 'success')
# Convert to DataFrame for analysis
ddf = bag.to_dataframe()Raw lines 196 to 210Pattern: Iterative Algorithms
Persist data between iterations:
data = dd.read_parquet('data.parquet')
data = data.persist() # Only when reusable partitions fit worker memory
for iteration in range(num_iterations):
data = update_function(data)
data = data.persist() # Persist updated versionRaw lines 211 to 222Debugging Tips
Raw lines 223 to 224Use Single-Threaded Scheduler
For debugging with pdb or detailed error inspection:
import dask
dask.config.set(scheduler='synchronous')
result = computation.compute() # Runs in single thread for debuggingRaw lines 225 to 234Check Task Graph Size
Before computing, check the number of tasks:
print(len(ddf.__dask_graph__())) # Should be reasonable, not millionsRaw lines 235 to 241Validate on Small Data First
Test logic on small subset before scaling:
# Test on first partition
sample = ddf.head(1000)
# Validate results
# Then scale to full datasetRaw lines 242 to 251Performance Troubleshooting
Raw lines 252 to 253Symptom: Slow Computation Start
Likely Cause: Task graph is too large Solution: Increase chunk sizes or use map_partitions
Raw lines 254 to 258Symptom: Memory Errors
Likely Causes:
- Chunks too large
- Too many intermediate results
- Memory leaks in user functions
Solutions:
- Decrease chunk sizes
- Use persist() strategically and delete when done
- Profile user functions for memory issues
Symptom: Poor Parallelization
Likely Causes:
- Data dependencies preventing parallelism
- Chunks too large (not enough tasks)
- GIL contention with threads on Python code
Solutions:
- Restructure computation to reduce dependencies
- Increase number of partitions
- Switch to multiprocessing scheduler for Python code