A scheduled bulk load ran COPY INTO and either failed with a parsing error, finished with LOAD_FAILED / PARTIALLY_LOADED files, or reported Copy executed with 0 files processed while the files are clearly in the bucket. This guide is for bulk COPY INTO <table> run by a scheduler or a person. If the files arrive through a pipe, see the Snowpipe troubleshooting post; the error-reading techniques below still apply.
First five minutes
- Find the failed statement and its query ID in
QUERY_HISTORYor your orchestrator log. - Check per-file results in
COPY_HISTORY(below). It tells you which file failed, the first error message, its line and column. - Do not rerun with
FORCE = TRUEuntil you know which files already loaded. That is the fastest way to create duplicates.
SELECT file_name, status, row_count, row_parsed, error_count,
first_error_message, first_error_line_number, first_error_column_name,
last_load_time
FROM TABLE(INFORMATION_SCHEMA.COPY_HISTORY(
TABLE_NAME => 'RAW.SALES.ORDERS',
START_TIME => DATEADD(hours, -24, CURRENT_TIMESTAMP())))
ORDER BY last_load_time DESC;
STATUS is one of Loaded, Load failed, Partially loaded or Load skipped. The Information Schema function covers the last 14 days; for older loads use SNOWFLAKE.ACCOUNT_USAGE.COPY_HISTORY, which has some latency (check the current documented latency).
Symptom → cause → fix
| Symptom | Likely cause | Fix |
|---|---|---|
Copy executed with 0 files processed | Files already loaded (load metadata), wrong path or PATTERN doesn't match | LIST the stage path and test the regex; check COPY_HISTORY for earlier loads |
Number of columns in file (n) does not match that of the corresponding table (m) | Extra/missing column, or delimiter inside unquoted text | Set FIELD_OPTIONALLY_ENCLOSED_BY, or load by column name |
Field delimiter ',' found while expecting record delimiter | Unquoted delimiter or embedded newline in a field | Quote fields at source; FIELD_OPTIONALLY_ENCLOSED_BY = '"' |
Numeric value 'abc' is not recognized | Header row loaded as data, or bad values | SKIP_HEADER = 1, NULL_IF, or transform on load |
Timestamp '...' is not recognized | Format differs from session default | Set TIMESTAMP_FORMAT / DATE_FORMAT in the file format |
User character length limit (n) exceeded by string | Value longer than target VARCHAR(n) | Widen the column, or TRUNCATECOLUMNS = TRUE if truncation is acceptable |
Invalid UTF8 detected in string | File not UTF-8 | Set ENCODING to the real encoding, or fix at source |
Failure using stage area / access denied | Storage integration or bucket policy changed | Check DESC INTEGRATION and the cloud-side role or SAS |
| Rows loaded twice | Rerun with FORCE = TRUE, or file renamed/rewritten upstream | Deduplicate; make the load idempotent |
Checks
Is the file where COPY is looking?
LIST @raw.sales.landing_stage/orders/2026/10/30/;
-- Test the same PATTERN that the COPY uses
LIST @raw.sales.landing_stage/orders/ PATTERN = '.*orders_.*[.]csv[.]gz';
PATTERN is a regular expression matched against the path, not a shell glob. *.csv does not mean what it does in a shell; use .*[.]csv.
Was it already loaded?
Snowflake keeps load metadata per table for 64 days and skips files whose name and checksum it has already loaded. A file with the same name and content is skipped silently; that is why the result says 0 files processed. Files older than 64 days whose load status is unknown are also skipped unless you set LOAD_UNCERTAIN_FILES = TRUE.
SELECT file_name, status, last_load_time
FROM TABLE(INFORMATION_SCHEMA.COPY_HISTORY(
TABLE_NAME => 'RAW.SALES.ORDERS',
START_TIME => DATEADD(days, -14, CURRENT_TIMESTAMP())))
WHERE file_name ILIKE '%orders_20261030%';
Look at the raw file through the file format
Query the staged file directly. This shows you exactly how Snowflake splits the columns:
SELECT METADATA$FILENAME, METADATA$FILE_ROW_NUMBER, $1, $2, $3, $4, $5
FROM @raw.sales.landing_stage/orders/2026/10/30/
(FILE_FORMAT => 'raw.sales.csv_orders', PATTERN => '.*orders_.*[.]csv[.]gz')
LIMIT 20;
If $5 holds half a customer name, you have an unquoted delimiter. If row 1 is the header, SKIP_HEADER is missing.
Find every bad row, not just the first
Before loading: VALIDATION_MODE
VALIDATION_MODE runs the COPY as a dry run and returns errors without loading anything:
COPY INTO raw.sales.orders
FROM @raw.sales.landing_stage/orders/2026/10/30/
FILE_FORMAT = (FORMAT_NAME = 'raw.sales.csv_orders')
VALIDATION_MODE = RETURN_ERRORS;
Options are RETURN_n_ROWS (for example RETURN_10_ROWS), RETURN_ERRORS and RETURN_ALL_ERRORS (also includes files partially loaded earlier with ON_ERROR = CONTINUE). VALIDATION_MODE does not work with a COPY that transforms data in a SELECT; validate those by querying the stage as shown above.
After a load: VALIDATE
For a COPY that already ran, VALIDATE returns the rejected rows from that job:
-- Last COPY run in this session
SELECT * FROM TABLE(VALIDATE(raw.sales.orders, JOB_ID => '_last'));
-- A specific query ID from the scheduler log
SELECT * FROM TABLE(VALIDATE(raw.sales.orders, JOB_ID => '01b7c2d4-0000-1a2b-0000-00045e2f1c3a'));
The output includes ERROR, FILE, LINE, CHARACTER, COLUMN_NAME and REJECTED_RECORD. Save it to a table and send it to the data owner:
CREATE OR REPLACE TABLE ops.load_errors_orders_20261030 AS
SELECT * FROM TABLE(VALIDATE(raw.sales.orders, JOB_ID => '01b7c2d4-0000-1a2b-0000-00045e2f1c3a'));
VALIDATE does not support COPY statements that transform data during the load.
Fix
Correct the file format
Most failures are file-format settings. A sturdy CSV format:
CREATE OR REPLACE FILE FORMAT raw.sales.csv_orders
TYPE = CSV
FIELD_DELIMITER = ','
SKIP_HEADER = 1
FIELD_OPTIONALLY_ENCLOSED_BY = '"'
NULL_IF = ('', 'NULL', '\\N')
EMPTY_FIELD_AS_NULL = TRUE
TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'
ENCODING = 'UTF8'
ERROR_ON_COLUMN_COUNT_MISMATCH = TRUE;
If the source adds columns now and then, load by header name instead of position. This needs PARSE_HEADER = TRUE in place of SKIP_HEADER:
COPY INTO raw.sales.orders
FROM @raw.sales.landing_stage/orders/2026/10/30/
FILE_FORMAT = (TYPE = CSV PARSE_HEADER = TRUE FIELD_OPTIONALLY_ENCLOSED_BY = '"'
ERROR_ON_COLUMN_COUNT_MISMATCH = FALSE)
MATCH_BY_COLUMN_NAME = CASE_INSENSITIVE;
Table columns with no matching header are loaded as NULL and extra file columns are ignored, so watch for silently empty columns after an upstream rename.
Choose ON_ERROR on purpose
ON_ERROR value | Behaviour | Use when |
|---|---|---|
ABORT_STATEMENT | Whole COPY fails on the first error (default for bulk COPY) | Financial or contractual data where partial loads are worse than none |
SKIP_FILE | Bad files are skipped, good files load | Many independent files; you reprocess the bad ones |
SKIP_FILE_10 / 'SKIP_FILE_5%' | Skip a file only above an error count or percentage | Tolerate a few bad rows per file |
CONTINUE | Load good rows, drop bad rows | Logs and telemetry where a few lost rows are acceptable |
Warning
ON_ERROR = CONTINUEhides data loss. The COPY reports success while rows are discarded. If you use it, alert onerror_count > 0inCOPY_HISTORYand keep theVALIDATEoutput.
Reload safely
Once the file format is fixed, a normal COPY picks up the files that failed, because failed files are not marked as loaded. For files that loaded partially or that you deliberately need to reload:
Caution
FORCE = TRUEloads every matching file again, whether or not it loaded before. Delete the affected rows first or you will have duplicates.
BEGIN;
DELETE FROM raw.sales.orders WHERE _source_file = 'orders/2026/10/30/orders_20261030.csv.gz';
COPY INTO raw.sales.orders (order_id, customer_id, amount, order_ts, _source_file)
FROM (SELECT $1, $2, $3, $4, METADATA$FILENAME
FROM @raw.sales.landing_stage/orders/2026/10/30/)
FILE_FORMAT = (FORMAT_NAME = 'raw.sales.csv_orders')
PATTERN = '.*orders_20261030[.]csv[.]gz'
FORCE = TRUE;
COMMIT;
Writing METADATA$FILENAME into a column is what makes this targeted delete possible. Note that a transforming COPY like this one cannot be checked with VALIDATION_MODE or VALIDATE.
Prevent it next time
- Store
METADATA$FILENAMEand a load timestamp on every raw table, so reloads are a delete plus a copy rather than a guess. - After each scheduled load, query
COPY_HISTORYand fail the pipeline if any file isLoad failedorPartially loaded, or iferror_count > 0. - Treat
0 files processedas a failure when files were expected; it usually means the upstream job didn't deliver or the path changed. - Keep file formats as named objects in version control, not inline in each COPY.
- Agree the file layout (delimiter, quoting, encoding, header, timestamp format) with the producer as a data contract.
- Don't rename or rewrite files in the landing area after delivery. A changed checksum makes Snowflake treat the file as new.