Tika Pipes

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Baseline date: 2026-10-12. This page was written against Apache Tika 4.1.0 (the latest release on Maven Central on that date) with the 3.3.x differences noted, Java 21 and later for the examples (Tika 4.x needs Java 17, 3.x needs Java 11), Spring Boot 4.x on Spring Framework 7 for the integration pointers and the Docker image apache/tika:4.1.0-1. Tika Pipes is the part of Tika that parses documents in separate, disposable JVMs. A pipes iterator lists the documents, a fetcher reads each one, a forked JVM parses it, an emitter writes the result and a reporter records what happened. In 4.x it is no longer an optional extra: tika-app’s batch mode, every parsing endpoint of tika-server, tika-grpc and the PipesForkParser class all run on it.

This page covers the model, the tika-app quick start, the pipes section of tika-config.json, the parse modes, emit batching and the emit strategy, the plugins and how to write one, the Java API, shared server mode and troubleshooting. Related topics are on their own pages: Configuration for the rest of tika-config.json and TikaLoader, Robustness, Limits & Timeouts for the full timeout model and the limits, Tika App (Command Line & GUI) for every tika-app flag, Tika Server (REST API) for /pipes, /async and the pipes-backed endpoints, Tika gRPC, Docker & Kubernetes Deployment for sizing forks in containers, and Security for the trust model.

How this page was checked. The Java examples were compiled and run against Tika 4.1.0 on Java 25 with tika-pipes-fork-parser, tika-pipes-core, tika-pipes-file-system, tika-pipes-csv and tika-parsers-standard-package (as a pom dependency) on the classpath. The tika-app runs used the 4.1.0 TikaCLI class with its dependencies on the classpath, which is what the launcher in the zip distribution does. Every output shown below is real output of those runs, with the working folder shown as /srv/bookshelf. Class names, method names, JSON keys and defaults were read from the 4.1.0 sources (PipesConfig, PipesForkParser, PipesForkParserConfig, ParseMode, EmitStrategy, PipesResult, FetchEmitTuple, TikaAsyncCLI, the file-system and Kafka plugins) and the 4.1.x documentation. The cloud, HTTP, JDBC, Kafka, Solr, Elasticsearch and OpenSearch plugins were not run: their configuration is quoted from the 4.1.x plugin pages and, for Kafka, cross-checked against the 4.1.0 source. The sample files are the Bookshelf intake set used across this section: manuscript.docx, catalogue.pdf, scanned-loan-form.pdf, publisher-email.eml (with samples.zip attached), submission.zip (a separate archive holding a copy of the manuscript and the catalogue), novel.epub, reviews-es.html, legacy-catalogue.txt (windows-1252) and renamed-invoice.png (a PDF with the wrong name).

<dependencies>
    <!-- PipesForkParser; pulls in tika-pipes-core, the file-system plugin and the standard parsers -->
    <dependency>
        <groupId>org.apache.tika</groupId>
        <artifactId>tika-pipes-fork-parser</artifactId>
        <version>4.1.0</version>
    </dependency>
    <!-- PipesParser and AsyncProcessor without PipesForkParser -->
    <dependency>
        <groupId>org.apache.tika</groupId>
        <artifactId>tika-pipes-core</artifactId>
        <version>4.1.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.tika</groupId>
        <artifactId>tika-parsers-standard-package</artifactId>
        <version>4.1.0</version>
        <type>pom</type>
    </dependency>
    <!-- one jar per extra plugin, for example the CSV iterator used below -->
    <dependency>
        <groupId>org.apache.tika</groupId>
        <artifactId>tika-pipes-csv</artifactId>
        <version>4.1.0</version>
    </dependency>
</dependencies>
dependencies {
    implementation(platform("org.apache.tika:tika-bom:4.1.0"))
    implementation("org.apache.tika:tika-pipes-fork-parser")
    implementation("org.apache.tika:tika-pipes-core")
    implementation("org.apache.tika:tika-parsers-standard-package")
    implementation("org.apache.tika:tika-pipes-csv")
}

Source: Apache Tika 4.1.x, Tika Pipes from Java (Dependencies). The tika-pipes-csv, tika-pipes-kafka, tika-pipes-s3, tika-pipes-opensearch, tika-pipes-es, tika-pipes-solr, tika-pipes-jdbc and tika-pipes-http 4.1.0 jars were checked on Maven Central; the plugin zips are not published there.

What Tika Pipes is

Parsing a file you did not write is risky. A crafted or merely broken PDF can make PDFBox allocate gigabytes, a ZIP can expand without end, a parser can loop forever, and a native library can crash the JVM. When the parser runs inside your application, all of that happens to your application. Tika’s security model says so plainly: calling parsers in-process opts out of the sandbox, and an out-of-memory error or an infinite loop is then the caller’s problem, not a Tika vulnerability. Process isolation is the sandbox, and Tika Pipes is how Tika provides it.

Tika Pipes splits the work between two kinds of process:

  • The parent (the driver) is your application, tika-app in batch mode, tika-server or tika-grpc. It lists the documents, hands them out, writes the results and keeps the books. It never parses.

  • The forks are child JVMs (class PipesServer) that the parent starts, feeds over a local socket and restarts when they die. Each fork fetches one document, parses it and either sends the result back or writes it itself.

numClients sets how many forks run at once. When a fork runs out of memory, times out or crashes, the parent records the failure for that one document, starts a fresh fork and carries on with the next document.

Tika Pipes: an iterator in the parent JVM lists documents, a queue feeds a pool of PipesClients, each forked JVM fetches and parses one document, the emitter writes the result directly or after passback and batching, and the reporter in the parent records each status

The four roles

A pipeline is assembled from four kinds of pluggable component. Each is named in tika-config.json and each is a PF4J plugin, so a source or a destination Tika does not ship can be added without rebuilding Tika.

Role What it does Where it runs Configured under

Pipes iterator

Enumerates the work: walks a folder, lists a bucket, reads a CSV or a JDBC result set, drains a Kafka topic. It produces one FetchEmitTuple per document.

Parent, on its own thread

pipes-iterator (singular, one per pipeline), keyed by component name

Fetcher

Turns a fetch key (a relative path, an object key, a URL) into the document bytes.

Fork

fetchers, keyed by an instance id you choose, then the component name

Emitter

Writes the parse result to a destination keyed by the emit key: a file, an object, a search document, a row, a Kafka record.

Fork (direct emit) or parent (after passback)

emitters, keyed by instance id, then component name

Reporter

Records the outcome (PipesResult status) of every document: counters, a JSON-lines ledger, a table, an index. Observational only.

Parent

pipes-reporters (plural, several allowed), keyed by component name

The FetchEmitTuple is the unit of work. It carries an id (yours, echoed in logs and reports), a FetchKey (fetcher id plus key), an EmitKey (emitter id plus key), optional Metadata to seed the parse (a content-type hint, a resource name, or business fields such as a shelf code), an optional per-document ParseContext (parse mode, limits, emit strategy) and an onParseException choice (EMIT or SKIP). The iterator builds the tuples; with the Java API you can build them yourself.

Source: Apache Tika 4.1.x, Tika Pipes (The four roles), Pipes Iterators and the 4.1.0 FetchEmitTuple source.

Ways in

The same machinery is reachable at six levels. Pick the highest one that does the job.

Entry point Module / artifact Use it when

tika-app -i <in> -o <out>

tika-app zip (TikaAsyncCLI)

A folder of files to a folder of JSON, Markdown or text, from a shell or a cron job. No code.

tika-app -a -c tika-config.json

tika-app zip

The same, with any iterator, fetcher, emitter and reporter from a config file.

tika-server /tika, /rmeta, /pipes, /async

tika-server-standard zip, apache/tika image

Other services send documents (or fetch keys) over HTTP. Parsing endpoints always run in forks in 4.x.

tika-grpc

apache/tika-grpc image

The same over gRPC, with fetcher management calls. Trusted networks only.

PipesForkParser

tika-pipes-fork-parser

Your Java code has a file or a stream and wants text and metadata back, crash-isolated. Local files and in-memory bytes only.

PipesParser

tika-pipes-core

One document at a time through a full pipes config: any fetcher, any emitter, result written by the fork or passed back.

AsyncProcessor

tika-pipes-core

The whole pipeline inside your JVM: queue, fork pool, emitter threads, reporters. This is what tika-app -i/-o runs.

PipesClient is the per-fork socket client wrapped by the last three; it is not a user-facing API.

The forked-JVM model

Each PipesClient in the parent owns one fork in the default per-client mode. The life of a fork:

  1. Lazy start. No fork starts when the pipeline is built. The first request that needs a client starts its fork, which loads the config and the plugins and then reports READY. Starting a fork costs seconds (about five seconds on the test machine for the first parse, with the standard parser package); that is why a PipesForkParser or a PipesParser must be created once and reused.

  2. Request and heartbeat. The parent sends the tuple over the socket; small in-memory documents travel inside the request (up to maxInlineBytes, 10 MB by default), anything larger is written to a file first. While parsing, the fork sends a heartbeat every heartbeatIntervalMillis (1000 ms). If nothing arrives for socketTimeoutMillis (60 s), the parent treats the fork as hung.

  3. Result. The fork returns a PipesResult with a status, a message and, when the result is passed back, the list of Metadata objects.

  4. Death and restart. An OutOfMemoryError, a timeout or a native crash ends the fork. The parent records OOM, TIMEOUT or UNSPECIFIED_CRASH for that document and starts a new fork on the next request. The fork is also recycled after maxFilesProcessedPerProcess documents (10 000), which bounds slow leaks in parser libraries.

  5. Reuse and idle shutdown. The client pool hands out the most recently used client first, so light traffic stays on warm forks. A fork that gets no work for socketTimeoutMillis exits on its own and is restarted transparently.

  6. Parent death. The parent passes its process id in TIKA_PIPES_PARENT_PID; each fork watches it and exits within milliseconds if the parent dies, so killed services do not leave orphaned forks (or orphaned tesseract processes).

Every fork has its own heap and its own temporary directory: a pipes-server-* subdirectory of the parent’s java.io.tmpdir, which the parent removes when the fork goes away. The first lines a fork logs show both, plus its cache budget (from the PipesForkParser run below, with -Xmx512m):

INFO  [main] 22:47:55,959 [fork] pipesClientId=0 o.a.t.pipes.core.server.PipesServer Cache memory budget: 134217728 bytes (default, a quarter of max heap)
INFO  [main] 22:47:55,966 [fork] pipesClientId=0 o.a.t.pipes.core.server.PipesServer watching parent pid 5900 for exit
INFO  [main] 22:47:56,105 [fork] pipesClientId=0 o.a.t.config.loader.TikaLoader temporary files go to java.io.tmpdir=/tmp/pipes-server-0-16529982140448677553
INFO  [main] 22:47:56,180 [fork] pipesClientId=0 o.a.t.pipes.core.server.PipesServer forked JVM max heap: 512 MB

How many forks, how much CPU and heap

The default numClients is derived from the host: max(1, mincores - 2) / 2, 4, so roughly half the cores, never more than four (PipesConfig.defaultNumClients() in the source). The batch CLIs (tika-app -i/-o, tika-async-cli) use 2 when neither -n nor a config value is given, and PipesForkParser uses 1.

Each JVM sizes its garbage collector, JIT and fork-join pools from the host CPU count, so four forks on a 16-core host would otherwise start about 64 GC threads. Unless you pass your own, Tika injects -XX:ActiveProcessorCount (a fair slice of the cores, never below 2, with 2 cores reserved for the parent) and -XX:MaxRAMPercentage (so the forks' combined heap stays under the host’s memory) into each fork. The tika-app run on an 8-core laptop logged:

INFO  [main] 22:50:27,910 org.apache.tika.pipes.core.async.AsyncProcessor Using per-client server mode with 2 workers
INFO  [main] 22:50:27,914 org.apache.tika.pipes.core.PerClientServerManager pipes-cpu-sizing: hostCores=8, numClients=2, parentReserved=2, autoCap=slice=3, heap=MaxRAMPercentage=37

The documented rule of thumb: pick the largest numClients for which twice that number plus two cores still fits on the host; beyond it, more forks reduce throughput. Size -Xmx per fork so that three quarters of it covers the parsing working set, because a quarter is the default in-memory cache budget (tika.pipes.cacheMemoryBudgetBytes, a fork-side system property). The Docker & Kubernetes Deployment page applies this to container limits.

Source: Apache Tika 4.1.x, Forked-JVM CPU and Heap Sizing and Pipes Configuration (Process Management, Embedded-object cache memory budget).

Result statuses

Every document ends in one PipesResult.RESULT_STATUS, grouped into five categories. PipesResult and PipesForkResult have one predicate per category.

Predicate (category) Statuses What to do

isSuccess() (SUCCESS)

PARSE_SUCCESS, PARSE_SUCCESS_WITH_EXCEPTION, PARSE_EXCEPTION_NO_EMIT, EMIT_SUCCESS, EMIT_SUCCESS_PARSE_EXCEPTION, EMIT_SUCCESS_PASSBACK, PARTIAL_TIMEOUT, EMPTY_OUTPUT

Use the output. The *_EXCEPTION variants carry a partial parse with the stack trace in the message and in the container metadata; PARTIAL_TIMEOUT means the task deadline cut the parse short.

isTaskException() (TASK_EXCEPTION)

FETCH_EXCEPTION, EMIT_EXCEPTION, FETCHER_NOT_FOUND, EMITTER_NOT_FOUND, PRESET_NOT_FOUND, PAYLOAD_LIMIT_EXCEEDED

This document failed; the fork is fine. Log it and move on.

isProcessCrash() (PROCESS_CRASH)

OOM, TIMEOUT, UNSPECIFIED_CRASH

The fork died on this document and is restarted before the next one. Record the document; do not retry it with the same settings.

isInitializationFailure() (INITIALIZATION_FAILURE)

FETCHER_INITIALIZATION_EXCEPTION, EMITTER_INITIALIZATION_EXCEPTION, CLIENT_UNAVAILABLE_WITHIN_MS

Possibly transient: a backend is down or every fork was busy for maxWaitForClientMillis. Retry later.

isFatal() (FATAL)

FAILED_TO_INITIALIZE

The fork cannot start at all: bad config, bad class path, bad javaPath. Stop and fix it.

Source: the 4.1.0 PipesResult source and Tika Pipes from Java (Reading a result).

Getting started with tika-app

The quickest pipeline needs no configuration file. tika-app 4.x ships as a zip: unzip it and run the jar from inside, because the jar is a thin launcher that loads lib/ and plugins/ from beside itself. Given an input folder and an output folder, tika-app switches to Tika Pipes mode, walks the input recursively and writes one JSON file per document.

cd tika-app-4.1.0
java -jar tika-app-4.1.0.jar -i /srv/bookshelf/inbox -o /srv/bookshelf/extracts -n 2
tika-app: running in Tika Pipes mode (async dispatch via -a/--async); use --help for options.
INFO  [main] 22:50:27,910 org.apache.tika.pipes.core.async.AsyncProcessor Using per-client server mode with 2 workers
INFO  [pool-2-thread-3] 22:50:35,216 org.apache.tika.pipes.core.PipesClient clientId=0: server successfully started
INFO  [pool-2-thread-2] 22:50:35,382 org.apache.tika.pipes.core.PipesClient clientId=1: server successfully started
INFO  [main] 22:50:37,635 tika.pipes Successfully finished processing 9 files in 9765 ms

The output folder then holds catalogue.pdf.json, legacy-catalogue.txt.json, manuscript.docx.json, novel.epub.json, publisher-email.eml.json, renamed-invoice.png.json, reviews-es.html.json, scanned-loan-form.pdf.json and submission.zip.json. The emit key is the input’s relative path and the emitter appends .json. Each file is the RMETA list: the container first, then one object per embedded document, with the text in tk:content as Markdown (the 4.x default handler). For submission.zip that is three objects: the ZIP itself (whose content is the two headings # manuscript.docx and # catalogue.pdf), then /manuscript.docx and /catalogue.pdf with their own dc:title, dc:creator and text.

Source: Apache Tika 4.1.x, Getting Started with Tika Pipes (Quick Start with tika-app).

Pipes-mode options

These are the options of the pipes mode (TikaAsyncCLI in the 4.1.0 source). The Tika App page covers the rules that decide when tika-app enters this mode and the "in flux" status of the 4.x command line.

Option Meaning

-i, --inputDir

Input folder, walked recursively by the file-system iterator.

-o, --outputDir

Output folder for the file-system emitter.

-n, --numClients

Number of forks (default 2 for the CLI).

--Xmx

Heap for each fork, for example --Xmx 1g.

-T, --timeoutMillis

Total timeout per document (sets timeout-limits.totalTaskTimeoutMillis).

--handler

Content format: t text, h HTML, x XHTML, m Markdown (default), b body, i ignore (metadata only).

--concatenate

Parse mode CONCATENATE: one metadata object, all embedded text merged.

--content-only

Parse mode CONTENT_ONLY: write only the extracted content, no JSON (implies --concatenate).

-z / -Z

Parse mode UNPACK: extract the raw bytes of direct attachments (-z) or of all attachments (-Z).

--unpack-format, --unpack-mode, --unpack-include-metadata

REGULAR or FRICTIONLESS output, ZIPPED or DIRECTORY, per-file metadata sidecars.

--on-exists

What to do with an existing output file: exception (default), replace, skip.

--fileList

A file with one path per line instead of walking the input folder.

-p, --pluginsDir

Plugins folder (defaults to plugins/ beside the jar).

-c, --config

A tika-config.json; -i and -o override the basePath values in it.

A Markdown copy of each document, without any metadata, is one flag away:

java -jar tika-app-4.1.0.jar -i /srv/bookshelf/inbox -o /srv/bookshelf/markdown -n 2 --handler m --content-only
cat /srv/bookshelf/markdown/submission.zip.md
# manuscript.docx

The Lighthouse Keeper

Chapter one begins at dusk.

# catalogue.pdf

Bookshelf spring catalogue Forty titles from independent publishers.

And -Z unpacks every attachment of the publisher’s e-mail, including the files inside the attached ZIP:

java -jar tika-app-4.1.0.jar -i /srv/bookshelf/mail -o /srv/bookshelf/unpacked -Z
find /srv/bookshelf/unpacked | sort
/srv/bookshelf/unpacked
/srv/bookshelf/unpacked/publisher-email.eml-embed
/srv/bookshelf/unpacked/publisher-email.eml-embed/00000001.zip
/srv/bookshelf/unpacked/publisher-email.eml-embed/00000002.docx
/srv/bookshelf/unpacked/publisher-email.eml-embed/00000003.png
/srv/bookshelf/unpacked/publisher-email.eml-embed/00000004.pdf
/srv/bookshelf/unpacked/publisher-email.eml-embed/00000005.bin
/srv/bookshelf/unpacked/publisher-email.eml.json
/srv/bookshelf/unpacked/submission.zip-embed
/srv/bookshelf/unpacked/submission.zip-embed/00000001.docx
/srv/bookshelf/unpacked/submission.zip-embed/00000002.pdf
/srv/bookshelf/unpacked/submission.zip.json

The extracted files are numbered by their embedded id; the JSON beside them is the RMETA list, in which each embedded object carries tk:embedded-id so you can map 00000002.docx back to /samples.zip/chapter-one.docx. The text note inside the attached ZIP came out as 00000005.bin; the extension comes from the suffixStrategy of unpack-config (see UNPACK and unpack-config).

Source: the 4.1.0 TikaAsyncCLI source and Apache Tika 4.1.x, Parse Modes (CLI usage).

From flags to a configuration file

For anything beyond a folder walk, write a tika-config.json and run it with -c, or with -a -c when no -i and -o are given. The next section builds the Bookshelf file. Flags still override the file where they overlap.

java -jar tika-app-4.1.0.jar -a -c /srv/bookshelf/tika-config.json

The pipes configuration

A pipeline is configured in the same tika-config.json that configures parsers, detectors and limits (see Configuration). Five top-level keys belong to Pipes.

Key Contents

plugin-roots

A folder (or an array of folders) of plugin zips. Mandatory for PipesParser and AsyncProcessor, even if the folder is empty. Only PipesForkParser and the template-based tika-app modes (-i/-o, two directories, -z/-Z) fill it in for you; a complete pipes configuration passed to tika-app as its only argument, and a tika-server with /async enabled, fail at start-up without it.

fetchers

Map of fetcher id to one component: { "inbox": { "file-system-fetcher": { ... } } }.

emitters

Map of emitter id to one component, same shape.

pipes-iterator

One iterator, keyed by component name, with fetcherId and emitterId naming the fetcher and the emitter bound to every tuple it produces.

pipes-reporters

Reporters keyed by component name; several may run together.

pipes

The pipeline itself: forks, timeouts, parse mode, batching, emit strategy (table below).

The text format comes from the top-level content-handler-factory and the per-document limits from parse-context (timeout-limits, embedded-limits, output-limits, unpack-config, digesters), exactly as for the library.

The Bookshelf intake configuration

The Bookshelf intake service receives manuscripts, catalogues, scans and publisher e-mails into an inbox folder and needs one JSON extract per document plus a record of anything that failed. This is the file used for the AsyncProcessor run later on this page.

{
  "plugin-roots": "/srv/bookshelf/plugins",
  "content-handler-factory": {
    "basic-content-handler-factory": { "type": "MARKDOWN", "writeLimit": 1000000 }
  },
  "fetchers": {
    "inbox": {
      "file-system-fetcher": { "basePath": "/srv/bookshelf/inbox", "extractFileSystemMetadata": true }
    }
  },
  "emitters": {
    "extracts": {
      "file-system-emitter": {
        "basePath": "/srv/bookshelf/extracts",
        "fileExtension": "json",
        "onExists": "REPLACE",
        "prettyPrint": true
      }
    }
  },
  "pipes-iterator": {
    "file-system-pipes-iterator": {
      "basePath": "/srv/bookshelf/inbox",
      "countTotal": true,
      "fetcherId": "inbox",
      "emitterId": "extracts"
    }
  },
  "pipes-reporters": {
    "file-system-reporter": { "statusFile": "/srv/bookshelf/logs/status.json", "reportUpdateMs": 1000 },
    "file-system-jsonl-reporter": { "path": "/srv/bookshelf/logs/intake-audit.jsonl", "onExists": "REPLACE" }
  },
  "pipes": {
    "numClients": 2,
    "forkedJvmArgs": ["-Xmx512m"],
    "maxFilesProcessedPerProcess": 1000,
    "parseMode": "RMETA",
    "emitIntermediateResults": true,
    "emitStrategy": { "type": "DYNAMIC", "thresholdBytes": 100000 }
  },
  "parse-context": {
    "timeout-limits": { "totalTaskTimeoutMillis": 300000, "progressTimeoutMillis": 30000 }
  }
}

Notes on the file:

  • inbox and extracts are instance ids you choose: letters, digits, ., _ and - only. Ids beginning with two underscores are reserved for components the hosts wire up themselves (tika-server’s upload fetcher, PipesForkParser's internal fetcher, the inline-bytes fetcher) and a file that defines one fails at startup.

  • file-system-fetcher, file-system-emitter and file-system-pipes-iterator are component names, the getName() of each plugin factory. An unknown name fails the load and lists the names available.

  • basePath on the fetcher and the emitter is the sandbox: keys that resolve outside it, including absolute paths and ../ traversal, are rejected.

  • extractFileSystemMetadata adds fs:created, fs:modified and fs:accessed to each document’s metadata; the iterator adds tk:source-path.

  • onParseException is set per tuple (the iterators set EMIT); the 4.1.x configuration page notes that nothing reads it from the pipes section, although the key is accepted there.

  • Secrets in other components (an S3 key, an Elasticsearch API key) belong in environment variables referenced as ${env:NAME}, which TikaJsonConfig resolves at load time since 4.1.0; see Configuration for the exact syntax and its limits.

Source: Apache Tika 4.1.x, Pipes Configuration (Complete examples), Fetchers (Reserved IDs) and File System Plugin.

Every key of the pipes section

All defaults below were read from the 4.1.0 PipesConfig and EmitStrategyConfig sources. The pipes section is shared by the parent and the forks; each ignores the keys it does not use.

Key Default Meaning

Process management

numClients

max(1, mincores - 2) / 2, 4

Number of forks, each parsing one document at a time.

forkedJvmArgs

[]

JVM arguments for each fork: -Xmx, -Xms, GC flags and every fork-side system property (setting them on the parent has no effect on the forks).

javaPath

java

Java executable used to start forks.

maxFilesProcessedPerProcess

10000

Restart a fork after this many documents.

tempDirectory

unset

Deprecated in 4.1 (removal planned for 5.0); set -Djava.io.tmpdir on the parent instead.

Timeouts (the per-document deadlines live in parse-context.timeout-limits)

socketTimeoutMillis

60000

Silence (no heartbeat, no result) after which a fork is considered hung; also the idle shutdown timer.

heartbeatIntervalMillis

1000

Heartbeat interval from the fork; must be well below socketTimeoutMillis.

startupTimeoutMillis

60000

Read timeout until a new fork has completed its READY handshake (raise it when plugin loading is slow).

maxWaitForClientMillis

60000

How long a request waits for a free fork before CLIENT_UNAVAILABLE_WITHIN_MS.

maxTotalTaskTimeoutMillis

3600000

Ceiling for request-supplied timeout limits (tika-server, tika-grpc); limits in the server’s own config are trusted and not capped.

Parse behaviour

parseMode

RMETA

RMETA, CONCATENATE, CONTENT_ONLY, NO_PARSE or UNPACK (next section). Can be overridden per tuple through its ParseContext.

onParseException

EMIT

Accepted, but the effective value is the one on each tuple.

stopOnlyOnFatal

false

false (batch): stop on configuration errors such as a missing fetcher or emitter. true (servers): stop only on FAILED_TO_INITIALIZE.

Emit batching (used by AsyncProcessor)

numEmitters

1

Emitter threads in the parent.

queueSize

10000

Capacity of the tuple queue; offer blocks while it is full.

emitWithinMillis

10000

Flush the batch if nothing was emitted for this long.

emitMaxEstimatedBytes

100000

Flush the batch when its estimated size reaches this.

emitIntermediateResults

false

false: only successful parses reach the emitter. true: every tuple is emitted, including crashes (see Emit strategy and batching).

IPC payload limits

maxIpcPayloadBytes

104857600 (100 MB)

Largest single message between parent and fork, in both directions. Give the fork about three times this as heap.

maxInlineBytes

10485760 (10 MB)

Largest in-memory document sent inside the request instead of being spooled to disk. Must fit inside maxIpcPayloadBytes; the pair is checked at load time.

Emit strategy

emitStrategy.type

DYNAMIC

DYNAMIC (by size), EMIT_ALL (fork always emits) or PASSBACK_ALL (parent always emits).

emitStrategy.thresholdBytes

100000

Only with DYNAMIC: extracts above this are emitted by the fork. Setting it with another type is a config error.

Distributed state and shared mode

configStoreType

memory

Where runtime fetcher and emitter configs live: memory, file (a JSON file that the forks read too, with {"path": "..."} in configStoreParams) or ignite (Apache Ignite, for multi-host setups). See Tika gRPC for why tika-grpc needs file.

configStoreParams

{}

Backend-specific parameters, as a JSON string.

useSharedServer

false

Shared server ("YOLO") mode, see Shared server ("YOLO") mode.

Source: Apache Tika 4.1.x, Pipes Configuration and the 4.1.0 PipesConfig source.

Parse modes

The parse mode decides how embedded documents are handled and what the emitter writes. It is set as parseMode in the pipes section and can be overridden per document by putting a ParseMode into the tuple’s ParseContext.

Mode What happens Equivalent elsewhere

RMETA (default)

Recursive parse; one Metadata per document, container first, each embedded document with its own metadata, text and tk:embedded-exception.

/rmeta, tika-app -J, RecursiveParserWrapper

CONCATENATE

Same parse, but all text is merged into the container’s tk:content and the embedded documents' own metadata is dropped. Container failures land in tk:exception:container-exception.

/tika, the classic tika-app output

CONTENT_ONLY

Parsed like CONCATENATE, but a metadata filter keeps only tk:content and tk:exception:container-exception, and a stream emitter (file system, S3) writes the raw text with no JSON.

tika-app --content-only

NO_PARSE

No parser runs. Detection and any configured digester still run on the raw bytes, so you get Content-Type and, for example, tk:digest:SHA-256.

MIME triage and hash inventories

UNPACK

Parsed like RMETA and the raw bytes of embedded documents are written through an emitter.

/unpack, tika-app -z / -Z

The content format is independent of the mode: the top-level content-handler-factory (TEXT, HTML, XML, MARKDOWN, BODY, IGNORE; MARKDOWN when unset) applies to every mode that produces content. With CONTENT_ONLY set the emitter’s fileExtension to match the handler (md, txt, html, xml). The handlers themselves are described in Content Handlers & Output Formats and the RMETA list in Embedded Documents & Recursive Parsing.

{
  "content-handler-factory": { "basic-content-handler-factory": { "type": "MARKDOWN" } },
  "emitters": {
    "markdown": {
      "file-system-emitter": { "basePath": "/srv/bookshelf/markdown", "fileExtension": "md" }
    }
  },
  "pipes": { "parseMode": "CONTENT_ONLY" }
}

Source: Apache Tika 4.1.x, Parse Modes and the 4.1.0 ParseMode source.

UNPACK and unpack-config

UNPACK needs only "parseMode": "UNPACK". Its options live in parse-context.unpack-config (Java: UnpackConfig):

Key Default Meaning

emitter

the tuple’s emitter

Emitter id for the extracted bytes.

maxUnpackBytes

10 GiB

Maximum bytes extracted per container; -1 is unlimited, 0 means zero bytes.

includeOriginal

false

Also write the container itself.

zipEmbeddedFiles

false

Collect the extracted files into one ZIP at the container’s emit key plus -embedded.zip.

includeMetadata

on for FRICTIONLESS, off for REGULAR

Metadata for every extracted file.

zeroPadName

0

Zero-pad embedded ids in names (8 gives 00000001).

suffixStrategy

NONE

NONE, EXISTING or DETECTED file extensions.

keyBaseStrategy, emitKeyBase, embeddedIdPrefix

DEFAULT, empty, -

How emit keys are built (embeddedIdPrefix only with CUSTOM).

outputFormat

REGULAR

REGULAR or FRICTIONLESS (a Frictionless Data Package).

outputMode

ZIPPED

ZIPPED (one archive) or DIRECTORY (each file emitted on its own).

The tika-app -Z run above wrote one file per attachment under <emit key>-embed/; the shape you get depends on these keys and on the CLI flags, so check the output of a test run before a downstream job relies on it.

{
  "pipes": { "parseMode": "UNPACK" },
  "parse-context": {
    "unpack-config": { "zipEmbeddedFiles": true, "includeMetadata": true, "maxUnpackBytes": 1073741824 }
  }
}

Emit strategy and batching

When a fork finishes a document, someone has to call the emitter. emitStrategy decides who:

  • PASSBACK_ALL: the fork sends the metadata list back over the socket and the parent emits. The parent knows exactly what was written, and a fork crash means "not emitted". The cost is carrying every extract across the IPC link and holding it in the parent’s heap.

  • EMIT_ALL: the fork emits directly and only reports success. Cheap for the parent, but the emitter’s dependencies and credentials must work inside the fork, and batching is per document.

  • DYNAMIC (default): extracts above thresholdBytes (100 000) are emitted by the fork, smaller ones are passed back and batched. The right choice for AsyncProcessor and tika-app.

Passed-back extracts wait in the parent until a batch is flushed by size (emitMaxEstimatedBytes) or by time (emitWithinMillis), then the numEmitters threads write them. Batching matters for the search emitters, which send bulk requests.

emitIntermediateResults decides what happens to documents whose parse did not succeed. Two runs of the Bookshelf CSV pipeline with an absurd five-millisecond progressTimeoutMillis, so that every document timed out, showed the difference: with false (the default) the output folder stayed empty and only the reporter knew about the three documents; with true the emitter wrote one file per document, each containing just [{"tk:pipes-result":"TIMEOUT"}]. Use true when the destination itself must hold a record of every attempt, and a JSON-lines reporter otherwise.

Source: Apache Tika 4.1.x, Pipes Configuration (Emit Strategy, Async / Emit Batching) and the 4.1.0 EmitStrategy source.

Timeouts in one paragraph

Two limits in parse-context.timeout-limits bound each document: totalTaskTimeoutMillis (end to end, default one hour) and progressTimeoutMillis (how long the parse may go silent). When the total deadline passes, the fork first tries to stop cleanly and return what it has as PARTIAL_TIMEOUT (also with throwOnDeadline, which only records an exception in the container metadata); a hard watchdog in the parent kills the fork if that does not happen within the progress window, which yields TIMEOUT. The progress timeout produced this result in the failure run below: Server-side progress timeout: no progress for 11ms (limit: 5ms). The complete model, the 3.x renames and the sizing advice are on Robustness, Limits & Timeouts.

Reporters

Reporters are called once per document by the parent, with the tuple, the PipesResult and the elapsed time. Because they run in the parent, they see crashes that never reach an emitter. Every configured reporter receives every call; the first exception any of them throws is rethrown afterwards and stops the run.

The file-system-reporter rewrites a status file every reportUpdateMs (always set it: the value is the writer thread’s sleep, so leaving it at 0 rewrites the file in a tight loop). After the Bookshelf run:

{"started":"2026-10-12T20:50:08.097221Z","lastUpdate":"2026-10-12T20:50:16.378513Z","totalCountResult":{"totalCount":9,"status":"COMPLETED"},"statusCounts":{"PARSE_SUCCESS":9},"asyncStatus":"COMPLETED","crashMessage":""}

The write is not atomic, so a watcher (a dashboard, the Tika GUI) must treat a JSON parse error as a stale read and try again on the next poll.

The file-system-jsonl-reporter (new in 4.1.0) appends one line per document and flushes it before returning, so it survives the death of the parent too. With includes you can turn it into a crash ledger for a dead-letter process or for tika-eval. Lines from the successful run and from the timeout run:

{"id":"manuscript.docx","status":"PARSE_SUCCESS","category":"SUCCESS","message":null,"elapsedMs":118,"timestamp":"2026-10-12T20:50:15.381116Z"}
{"id":"BK-0002","status":"TIMEOUT","category":"PROCESS_CRASH","message":"java.lang.RuntimeException: Server-side progress timeout: no progress for 9ms (limit: 5ms)\n\tat org.apache.tika.pipes.core.server.PipesServer.checkProgressTimeout(PipesServer.java:699)\n\tat org.apache.t[stack trace shortened here]","elapsedMs":5985,"timestamp":"2026-10-12T20:56:08.179382Z"}
file-system-jsonl-reporter key Default Meaning

path

required

The JSONL file; parent folders are created.

onExists

EXCEPTION

EXCEPTION refuses to start, APPEND continues an old ledger, REPLACE truncates it.

includes / excludes

all statuses

Mutually exclusive sets of status names, for example ["OOM", "TIMEOUT", "UNSPECIFIED_CRASH", "FETCH_EXCEPTION", "EMIT_EXCEPTION"].

maxMessageLength

10000

Characters of the message (stack trace) to keep.

The other reporters are jdbc-reporter (rows in a table), opensearch-pipes-reporter and es-pipes-reporter (status documents in an index).

Source: Apache Tika 4.1.x, Pipes Reporters and File System Plugin (File System Reporter, File System JSONL Reporter).

Plugins

How plugins are loaded

Every fetcher, emitter, iterator and reporter in 4.x is a PF4J plugin. A plugin contributes one factory per component, each annotated with @org.pf4j.Extension and listed in META-INF/extensions.idx. The configuration never names a class: it names the factory’s getName(), such as file-system-fetcher.

Plugins reach a Tika host in one of two ways:

  • As zips in a plugins folder. This is the distribution layout. The tika-app and tika-server zips ship a plugins/ folder and plugin-roots points at it. Tika unzips each plugin into a sibling folder named after the zip and writes a .tika-extraction-complete marker. If a folder of that name exists next to its zip without the marker, Tika treats it as a failed extraction, deletes it and unzips again, so never unpack a plugin by hand next to its own zip. A folder unpacked by hand with no zip beside it is loaded as it is. In the apache/tika Docker images extra plugins go under /opt/tika-server/plugins/ (/tika-extras is for extra jars, not plugins); see Docker & Kubernetes Deployment.

  • As jars on the class path. For an embedding application, add the plugin jar (tika-pipes-s3, tika-pipes-opensearch, tika-pipes-csv and so on) as an ordinary Maven dependency. Forks are launched with the parent’s class path, so they see the same plugins. tika-pipes-fork-parser already depends on tika-pipes-file-system. A zip plugin of the same name takes precedence over a class-path plugin.

The plugin zips come from the distribution zips on the download page; they are not on Maven Central, the jars are. When plugin-roots points at a folder that does not exist, PF4J logs a warning and Tika carries on with the class-path plugins, which is what the runs on this page did:

WARN  [main] 22:47:56,199 [fork] pipesClientId=0 org.pf4j.AbstractPluginManager No '/srv/bookshelf/plugins' root
INFO  [main] 22:47:56,200 [fork] pipesClientId=0 org.pf4j.AbstractPluginManager No plugins

For plugin development, -Dtika.plugin.dev.mode=true (or TIKA_PLUGIN_DEV_MODE=true) treats each plugin-roots entry as one already exploded plugin folder, so you can point it at a module’s target/classes.

Source: Apache Tika 4.1.x, Tika Pipes from Java (Where plugins come from) and Writing a Pipes Plugin.

The plugin catalogue

Plugin (artifact) Component names Fetcher Emitter Iterator Reporter

File system (tika-pipes-file-system)

file-system-fetcher, file-system-emitter, file-system-pipes-iterator, file-system-reporter, file-system-jsonl-reporter

yes

yes

yes

yes

Amazon S3 (tika-pipes-s3)

s3-fetcher, s3-emitter, s3-pipes-iterator

yes

yes

yes

-

Google Cloud Storage (tika-pipes-gcs)

gcs-fetcher, gcs-emitter, gcs-pipes-iterator

yes

yes

yes

-

Azure Blob Storage (tika-pipes-az-blob)

az-blob-fetcher, az-blob-emitter, az-blob-pipes-iterator

yes

yes

yes

-

HTTP (tika-pipes-http)

http-fetcher

yes

-

-

-

Google Drive (tika-pipes-google-drive)

google-drive-fetcher

yes

-

-

-

Microsoft Graph (tika-pipes-microsoft-graph)

microsoft-graph-fetcher

yes

-

-

-

Atlassian JWT (tika-pipes-atlassian-jwt)

atlassian-jwt-fetcher

yes

-

-

-

JDBC (tika-pipes-jdbc)

jdbc-emitter, jdbc-pipes-iterator, jdbc-reporter

-

yes

yes

yes

CSV (tika-pipes-csv)

csv-pipes-iterator

-

-

yes

-

JSON (tika-pipes-json)

json-pipes-iterator

-

-

yes

-

Apache Kafka (tika-pipes-kafka)

kafka-emitter, kafka-pipes-iterator

-

yes

yes

-

Apache Solr (tika-pipes-solr)

solr-emitter, solr-pipes-iterator

-

yes

yes

-

Elasticsearch (tika-pipes-es)

es-emitter, es-pipes-reporter

-

yes

-

yes

OpenSearch (tika-pipes-opensearch)

opensearch-emitter, opensearch-pipes-reporter

-

yes

-

yes

The component names come from the 4.1.x plugin pages and the artifact ids from the module folders of the 4.1.0 source tree (tika-pipes/tika-pipes-plugins/tika-pipes-*). The file-system, CSV, HTTP, JDBC, Kafka, S3, Solr, Elasticsearch and OpenSearch jars were also checked on Maven Central.

Source: Apache Tika 4.1.x, Pipes Plugins (Plugin / Interface Matrix).

File system

The file-system plugin is the reference implementation and the one most pipelines start with.

Component Key Default Meaning

file-system-fetcher

basePath

none

Fetch keys are resolved under it and must stay inside it.

extractFileSystemMetadata

false

Add size and fs: timestamps to the metadata.

allowAbsolutePaths

false

Permission to run without basePath, using keys as raw absolute paths.

file-system-emitter

basePath

none

Output root; emit keys must stay inside it.

fileExtension

none

Appended to each output name (json, or md and friends with CONTENT_ONLY).

onExists

EXCEPTION

SKIP, REPLACE or EXCEPTION.

prettyPrint

false

Indented JSON.

atomicWrites

true

Write <name>.<uuid>.tmp and rename; set false on mounts without rename.

allowAbsolutePaths

false

As for the fetcher.

file-system-pipes-iterator

basePath

required

Folder to walk recursively (symbolic links are not followed).

countTotal

true

Count the files on a second thread so progress has a total.

basePath is the only containment. Without it there is none, and allowAbsolutePaths: true is how you assert that the keys come from a fully trusted source. The fetcher re-checks symbolic links with toRealPath(), the emitter does not, so a link already present under the emitter’s basePath can be written through.

Source: Apache Tika 4.1.x, File System Plugin (Security Notes).

Cloud stores: S3, Google Cloud Storage, Azure Blob

The three object-store plugins provide a fetcher, an emitter and an iterator each, with keys relative to a prefix. For Bookshelf, scans uploaded to a bucket could be read and the extracts written back to another prefix. Credentials are not in the file: credentialsProvider: "profile" (or the instance role) for S3, Application Default Credentials for GCS, and an environment variable for the Azure SAS token.

{
  "fetchers": {
    "scans": {
      "s3-fetcher": {
        "bucket": "bookshelf-intake", "region": "eu-west-1", "prefix": "scans/",
        "credentialsProvider": "profile", "profile": "bookshelf", "spoolToTemp": true
      }
    }
  },
  "emitters": {
    "scan-extracts": {
      "s3-emitter": {
        "bucket": "bookshelf-extracts", "region": "eu-west-1", "prefix": "scans/", "fileExtension": "json",
        "credentialsProvider": "profile", "profile": "bookshelf"
      }
    }
  },
  "pipes-iterator": {
    "s3-pipes-iterator": {
      "bucket": "bookshelf-intake", "region": "eu-west-1", "prefix": "scans/",
      "credentialsProvider": "profile", "profile": "bookshelf",
      "fetcherId": "scans", "emitterId": "scan-extracts"
    }
  }
}
{
  "fetchers": {
    "archive": {
      "az-blob-fetcher": {
        "endpoint": "https://bookshelfarchive.blob.core.windows.net",
        "container": "intake",
        "sasToken": "${env:BOOKSHELF_AZ_SAS}",
        "spoolToTemp": true
      }
    },
    "gcs-intake": {
      "gcs-fetcher": { "projectId": "bookshelf-prod", "bucket": "bookshelf-intake", "spoolToTemp": true }
    }
  }
}

Not run for this page; the key names are the ones on the 4.1.x S3, Azure Blob and GCS plugin pages.

HTTP, Google Drive and Microsoft Graph

These are fetcher-only plugins; pair them with an iterator that produces the right keys (a CSV of URLs, a JSON-lines work list, your own code) and any emitter. For http-fetcher the fetch key is the URL itself; it supports basic and JWT authentication, connection and timeout settings, extra headers and maxSpoolSize, and since 4.0 it verifies TLS certificates and host names by default (verifySsl: false opts out, which you should not do outside a test).

{
  "fetchers": {
    "publisher-portal": {
      "http-fetcher": {
        "userName": "bookshelf-intake",
        "password": "${env:PUBLISHER_PORTAL_PASSWORD}",
        "authScheme": "basic",
        "connectTimeoutMillis": 30000,
        "requestTimeoutMillis": 60000,
        "overallTimeoutMillis": 120000,
        "maxRedirects": 5
      }
    },
    "sharepoint": {
      "microsoft-graph-fetcher": {
        "clientSecretCredentialsConfig": {
          "tenantId": "${env:AZURE_TENANT_ID}",
          "clientId": "${env:AZURE_CLIENT_ID}",
          "clientSecret": "${env:AZURE_CLIENT_SECRET}"
        },
        "scopes": ["https://graph.microsoft.com/.default"],
        "spoolToTemp": true
      }
    }
  }
}

google-drive-fetcher takes a base64 service-account key (serviceAccountKeyBase64), subjectUser, scopes and applicationName; feed the key from an environment variable as well. An HTTP fetcher that accepts URLs from callers (tika-server /pipes, tika-grpc) is a server-side request forgery risk; see the Security page.

Work lists: CSV, JSON lines and JDBC

When the list of documents comes from a system rather than a folder, use a work-list iterator. The Bookshelf intake desk exports the day’s submissions as a CSV, with business fields that should end up in each extract:

doc_id,source_path,output_path,shelf,submitted_by
BK-0001,manuscript.docx,2026/BK-0001,fiction,ana.torres
BK-0002,catalogue.pdf,2026/BK-0002,catalogues,press
BK-0003,scanned-loan-form.pdf,2026/BK-0003,loans,desk

Replace the iterator in the Bookshelf file with:

{
  "pipes-iterator": {
    "csv-pipes-iterator": {
      "csvPath": "/srv/bookshelf/intake.csv",
      "idColumn": "doc_id",
      "fetchKeyColumn": "source_path",
      "emitKeyColumn": "output_path",
      "fetcherId": "inbox",
      "emitterId": "extracts"
    }
  }
}

Run with the Intake program of the Java section (and tika-pipes-csv on the class path), the pipeline wrote 2026/BK-0001.json, 2026/BK-0002.json and 2026/BK-0003.json, the ledger used the CSV ids ({"id":"BK-0001","status":"PARSE_SUCCESS",...}), and every column not consumed as a key became metadata: the manuscript’s extract carried "shelf": "fiction" and "submitted_by": "ana.torres" next to "dc:title": "The Lighthouse Keeper". The CSV must be UTF-8 with a header row (Excel format); a column whose header collides with a reserved Tika key is skipped with a log line.

The json-pipes-iterator reads a JSON-lines file in which each line is a whole serialized FetchEmitTuple (id, fetcher, fetchKey, emitter, emitKey, and optionally metadata, parse-context, onParseException, preset), so each line names its own fetcher and emitter; unknown fields are rejected. The jdbc-pipes-iterator walks a SELECT (select, idColumn, fetchKeyColumn, emitKeyColumn, fetchSize), and the jdbc-emitter writes chosen metadata keys into a table through an insert template with typed keys.

Search emitters: Solr, Elasticsearch and OpenSearch

The three search emitters send bulk requests, so they benefit from passback batching. Each takes the index or collection URL, an idField (the emit key becomes the document id), an attachmentStrategy for embedded documents (the 4.1.x examples use PARENT_CHILD) and an updateStrategy (ADD for Solr, OVERWRITE for the other two). The Elasticsearch and OpenSearch emitters also take a chunkStrategy for tk:chunks.

{
  "emitters": {
    "catalogue-index": {
      "es-emitter": {
        "esUrl": "https://search.bookshelf.internal:9200/bookshelf-docs",
        "idField": "doc_id",
        "attachmentStrategy": "PARENT_CHILD",
        "updateStrategy": "OVERWRITE",
        "apiKey": "${env:BOOKSHELF_ES_API_KEY}",
        "httpClientConfig": { "connectionTimeoutMillis": 10000, "socketTimeoutMillis": 60000 }
      }
    },
    "solr": {
      "solr-emitter": {
        "solrCollection": "bookshelf",
        "solrUrls": ["http://solr1.bookshelf.internal:8983/solr"],
        "idField": "id",
        "commitWithin": 1000,
        "attachmentStrategy": "PARENT_CHILD",
        "updateStrategy": "ADD"
      }
    }
  }
}

Restrict what reaches the index with a metadata filter (see Metadata) instead of indexing every key Tika produces. The solr-pipes-iterator reads documents back out of a collection (for example everything with status:NEEDS_REPARSE) to re-parse them. SolrJ moved to 10.0 in Tika 4.0, so the Solr plugin no longer supports Solr 8. For the index side see Solr indexing & updates and Elasticsearch ingest pipelines. Not run for this page.

Kafka

The Kafka plugin has two halves with different standing.

The kafka-emitter is a plain producer and a good fit for streaming extracts to downstream indexers. Its keys map onto the standard producer settings (topic, bootstrapServers, acks, lingerMs, batchSize, compressionType, enableIdempotence, maxRequestSize, deliveryTimeoutMs, transactionalId, and so on); Tika sets no values of its own, so an absent key means the Kafka client default. Read from the 4.1.0 KafkaEmitter source: the emitter sends one record per Metadata object, so an RMETA extract of publisher-email.eml becomes six records with the same key (the emit key). The value is a JSON object of the metadata fields, and only the first value of a multi-valued key is kept (with a warning in the log). Use CONCATENATE when downstream consumers expect one record per document.

{
  "emitters": {
    "extract-topic": {
      "kafka-emitter": {
        "topic": "bookshelf.extracts",
        "bootstrapServers": "kafka1.bookshelf.internal:9092,kafka2.bookshelf.internal:9092",
        "acks": "all",
        "enableIdempotence": true,
        "compressionType": "lz4",
        "maxRequestSize": 1048576,
        "clientId": "bookshelf-tika-pipes"
      }
    }
  },
  "pipes": { "parseMode": "CONCATENATE" }
}

The kafka-pipes-iterator is documented as a worked example rather than a supported production integration. In 4.1.0 it uses each record’s key as the tuple id, the fetch key and the emit key (the record value is not read). It leaves enable.auto.commit at Kafka’s default, so offsets are committed when records are polled, before Tika has parsed or emitted them: a crash in between loses those documents, which is at-most-once delivery. It drains the topic and stops after drainIdleMs (1000 ms) of quiet, so it suits a periodic batch run, not a long-running consumer. And a slow document holds up its partition. For long-running or OCR-heavy work, write your own consumer that commits after a successful result and drives tika-server, tika-grpc or PipesParser (see the AsyncProcessor section), and keep the Kafka emitter for the results.

The Kafka side of these trade-offs is covered in Kafka Consumers, Kafka Producers, Delivery Guarantees and Kafka Reliability and Exactly-Once Semantics. Not run for this page.

Source: Apache Tika 4.1.x, Apache Kafka Plugin and the 4.1.0 KafkaEmitter and KafkaPipesIterator sources.

Writing a plugin

A plugin is three kinds of class, one plugin.properties and a zip with a fixed layout. Using a Bookshelf fetcher for a supplier portal as the example:

  1. A org.pf4j.Plugin subclass with a (PluginWrapper) constructor, one per zip.

  2. One factory per component, annotated with @Extension (on the factory, never on the fetcher) and implementing the matching interface from org.apache.tika.pipes.api: fetcher.FetcherFactory, emitter.EmitterFactory, pipesiterator.PipesIteratorFactory or reporter.PipesReporterFactory. getName() is the name users write in tika-config.json and must be unique among all loaded plugins.

  3. The component itself, extending org.apache.tika.plugins.AbstractTikaExtension (or PipesIteratorBase from tika-pipes-iterator-commons, whose only abstract method is enqueue(), or PipesReporterBase). Its configuration arrives as a JSON string in ExtensionConfig.json(). Fetchers and emitters must be thread-safe: one instance serves every concurrent request for its id.

package org.bookshelf.tika;

import java.io.IOException;

import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.pipes.api.fetcher.Fetcher;
import org.apache.tika.pipes.api.fetcher.FetcherFactory;
import org.apache.tika.plugins.ExtensionConfig;
import org.pf4j.Extension;

@Extension
public class SupplierPortalFetcherFactory implements FetcherFactory {

    @Override
    public String getName() {
        return "bookshelf-supplier-fetcher";
    }

    @Override
    public Fetcher buildExtension(ExtensionConfig extensionConfig)
            throws IOException, TikaConfigException {
        return new SupplierPortalFetcher(extensionConfig);
    }

    @Override
    public Class<?> getConfigClass() {
        return SupplierPortalFetcherConfig.class;
    }
}
plugin.id=bookshelf-supplier-plugin
plugin.class=org.bookshelf.tika.BookshelfPipesPlugin
plugin.version=${project.version}
plugin.provider=Bookshelf
plugin.description=Fetches publisher files from the Bookshelf supplier portal

The zip holds plugin.properties at its root, classes/META-INF/extensions.idx, and lib/ with the plugin’s own jar and its runtime dependencies. Everything on the boundary between host and plugin is declared provided and must not be in lib/: pf4j, tika-core, tika-plugins-core, tika-pipes-api, tika-serialization, the logging API and, unless you deliberately bundle your own, Jackson. A plugin that ships its own tika-core loads cleanly and then finds no extensions or fails with a ClassCastException, because its FetcherFactory is a different class from the host’s. Follow the logging convention of the bundled plugins: literal fetch and emit keys only at TRACE.

The factory above was written from the 4.1.x guide and not compiled for this page. The parser-side extension points are on Extending Tika.

The Java API

PipesForkParser: crash-isolated parsing for your own code

PipesForkParser (module tika-pipes-fork-parser) is the 4.x replacement for the removed ForkParser. It is the simplest way to give an application process isolation: you pass a Path or a TikaInputStream, a fork parses it and the text and metadata come back in a PipesForkResult. Internally it writes a temporary tika-config.json with an internal file-system fetcher (UUID-named, allowAbsolutePaths: true), the PASSBACK_ALL emit strategy and your settings, and loads a PipesParser from it.

The Bookshelf intake check below parses every file in the inbox in Markdown and RMETA mode, lists embedded documents and handles the three outcome classes. It also parses a stream with no file behind it, which travels inline to the fork.

package org.bookshelf.intake;

import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Stream;

import org.apache.tika.config.TimeoutLimits;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.pipes.api.ParseMode;
import org.apache.tika.pipes.fork.PipesForkParser;
import org.apache.tika.pipes.fork.PipesForkParserConfig;
import org.apache.tika.pipes.fork.PipesForkParserException;
import org.apache.tika.pipes.fork.PipesForkResult;
import org.apache.tika.sax.BasicContentHandlerFactory;

public class IntakeFork {

    public static void main(String[] args) throws Exception {
        Path inbox = Path.of(args.length > 0 ? args[0] : "/srv/bookshelf/inbox");
        PipesForkParserConfig config = new PipesForkParserConfig()
                .setHandlerType(BasicContentHandlerFactory.HANDLER_TYPE.MARKDOWN)
                .setParseMode(ParseMode.RMETA)
                .setTimeoutLimits(new TimeoutLimits(
                        TimeoutLimits.DEFAULT_TOTAL_TASK_TIMEOUT_MILLIS, 30_000))
                .addJvmArg("-Xmx512m")
                .setMaxFilesPerProcess(500)
                .setNumClients(2);

        List<Path> files;
        try (Stream<Path> s = Files.list(inbox)) {
            files = s.filter(Files::isRegularFile).sorted().toList();
        }

        // create once, reuse: each instance owns a pool of forked JVMs
        try (PipesForkParser parser = new PipesForkParser(config)) {
            for (Path file : files) {
                report(file.getFileName().toString(), parser.parse(file));
            }
            // a stream without a file behind it: up to maxInlineBytes travels inside the request
            try (InputStream in = Files.newInputStream(inbox.resolve("catalogue.pdf"));
                 TikaInputStream tis = TikaInputStream.get(in)) {
                Metadata hint = new Metadata();
                hint.set(TikaCoreProperties.RESOURCE_NAME_KEY, "catalogue.pdf");
                report("stream:catalogue.pdf", parser.parse(tis, hint));
            }
        } catch (PipesForkParserException e) {
            // configuration and initialization problems are thrown, not returned
            System.err.println("configuration problem (" + e.getStatus() + "): " + e.getMessage());
        }
    }

    static void report(String name, PipesForkResult result) {
        if (result.isSuccess()) {
            List<Metadata> list = result.getMetadataList();   // container first
            String text = result.getContent();                 // container text only
            System.out.printf("%-24s %-14s docs=%d type=%s chars=%d%n", name, result.getStatus(),
                    list.size(), result.getMetadata().get(HttpHeaders.CONTENT_TYPE),
                    text == null ? 0 : text.length());
            for (int i = 1; i < list.size(); i++) {
                Metadata m = list.get(i);
                System.out.printf("%-24s   embedded %s %s%s%n", "",
                        m.get(TikaCoreProperties.EMBEDDED_RESOURCE_PATH),
                        m.get(HttpHeaders.CONTENT_TYPE),
                        m.get(TikaCoreProperties.EMBEDDED_EXCEPTION) == null ? "" : " EXCEPTION");
            }
        } else if (result.isProcessCrash()) {
            // OOM, TIMEOUT or UNSPECIFIED_CRASH: the fork restarts before the next parse
            System.out.printf("%-24s CRASH %s%n", name, result.getStatus());
        } else {
            // fetch or parse problem with this document only
            System.out.printf("%-24s FAILED %s %s%n", name, result.getStatus(), result.getMessage());
        }
    }
}
catalogue.pdf            PARSE_SUCCESS  docs=1 type=application/pdf chars=89
legacy-catalogue.txt     PARSE_SUCCESS  docs=1 type=text/plain; charset=windows-1252 chars=152
manuscript.docx          PARSE_SUCCESS  docs=1 type=application/vnd.openxmlformats-officedocument.wordprocessingml.document chars=76
novel.epub               PARSE_SUCCESS  docs=1 type=application/epub+zip chars=59
publisher-email.eml      PARSE_SUCCESS  docs=6 type=message/rfc822 chars=112
                           embedded /samples.zip/chapter-one.docx/figure1.png image/png
                           embedded /samples.zip/chapter-one.docx application/vnd.openxmlformats-officedocument.wordprocessingml.document
                           embedded /samples.zip/catalogue.pdf application/pdf
                           embedded /samples.zip/cover-note.txt text/plain; charset=windows-1252
                           embedded /samples.zip application/zip
renamed-invoice.png      PARSE_SUCCESS  docs=1 type=application/pdf chars=89
reviews-es.html          PARSE_SUCCESS  docs=1 type=text/html; charset=UTF-8 chars=420
scanned-loan-form.pdf    PARSE_SUCCESS  docs=1 type=application/pdf chars=131
submission.zip           PARSE_SUCCESS  docs=3 type=application/zip chars=35
                           embedded /manuscript.docx application/vnd.openxmlformats-officedocument.wordprocessingml.document
                           embedded /catalogue.pdf application/pdf
stream:catalogue.pdf     PARSE_SUCCESS  docs=1 type=application/pdf chars=89

The whole run took about 7 seconds of wall time, most of it the start of the first fork. renamed-invoice.png was detected as the PDF it is. In the RMETA list the embedded documents come after the container, deepest first (the image inside the attached manuscript precedes the manuscript itself). getContent() is only the container’s text: for the e-mail, 112 characters out of a much larger total, so iterate getMetadataList() and read TikaCoreProperties.TIKA_CONTENT from each element when you need everything.

PipesForkParserConfig

Method Default Effect

setHandlerType(HANDLER_TYPE) / setContentHandlerFactory(...)

TEXT, no write limit

Output format. PipesForkParser defaults to plain text, unlike tika-app and the pipes config, which default to Markdown.

setWriteLimit(int)

-1

Characters of content per document.

setParseMode(ParseMode)

RMETA

Any of the five modes; UNPACK needs an emitter, which PipesForkParser does not configure.

setMaxEmbeddedCount(int), setEmbeddedLimits(EmbeddedLimits)

unset

Embedded-document limits.

setTimeoutLimits(TimeoutLimits)

config default

new TimeoutLimits(totalTaskMillis, progressMillis).

addJvmArg(String), setJvmArgs(List)

none

Fork JVM arguments (-Xmx512m).

setJavaPath(String)

java from the PATH

Java executable for the forks (honoured since 4.1.0).

setMaxFilesPerProcess(int)

10000

Recycle the fork after this many documents.

setNumClients(int)

1

Forks for concurrent parse calls from several threads (expert setting).

setPluginsDir(Path)

plugins/ beside the jar, else in the working folder

Plugin zips; harmless warning when absent and the plugins are on the class path.

setUserConfigPath(Path)

none

A tika-config.json merged into the generated one (parsers, limits, metadata filters); the internal fetcher is always added.

setFetcherName(String)

fs

Stored in the config object, but not used by the 4.1.0 config generation: the internal fetcher always gets a generated __tika-internal-fetcher-<uuid> id (ConfigMerger source).

getPipesConfig()

-

The underlying PipesConfig, for anything else (for example setMaxInlineBytes).

Source: the 4.1.0 PipesForkParserConfig source and PipesForkParserConfig Javadoc.

Failures, observed

Two deliberate failures show the split between results and exceptions. A five-millisecond progress timeout makes every parse a process crash, which parse returns; a non-existent javaPath makes the fork impossible to start, which parse throws as a PipesForkParserException:

PipesForkParserConfig tight = new PipesForkParserConfig()
        .setTimeoutLimits(new TimeoutLimits(5, 5));
try (PipesForkParser parser = new PipesForkParser(tight)) {
    PipesForkResult r = parser.parse(Path.of("/srv/bookshelf/inbox/scanned-loan-form.pdf"));
    System.out.println(r.getStatus() + " crash=" + r.isProcessCrash() + " msg=" + r.getMessage().lines().findFirst().orElse(""));
}

PipesForkParserConfig badJava = new PipesForkParserConfig().setJavaPath("/opt/no-such-jdk/bin/java");
try (PipesForkParser parser = new PipesForkParser(badJava)) {
    parser.parse(Path.of("/srv/bookshelf/inbox/catalogue.pdf"));
} catch (PipesForkParserException e) {
    System.out.println(e.getStatus() + " initFailure=" + e.isInitializationFailure());
}
TIMEOUT crash=true msg=java.lang.RuntimeException: Server-side progress timeout: no progress for 11ms (limit: 5ms)
FAILED_TO_INITIALIZE initFailure=true

The second case also logged ERROR clientId=0: failed to start server with the underlying Cannot run program "/opt/no-such-jdk/bin/java". With a 300 ms limit instead of 5 ms, both documents parsed: the fork’s start-up does not count against the per-document deadline.

PipesForkParser throws PipesForkParserException for FAILED_TO_INITIALIZE, the fetcher and emitter initialization failures, FETCHER_NOT_FOUND, EMITTER_NOT_FOUND and CLIENT_UNAVAILABLE_WITHIN_MS, and returns everything else.

Source: the 4.1.0 PipesForkParser source (checkForApplicationError).

PipesParser: one document, any fetcher, any emitter

PipesParser (in tika-pipes-core) takes a FetchEmitTuple and returns a PipesResult, with everything else coming from a config file. The same code reads from a folder today and from a bucket tomorrow. It is thread-safe: each call borrows one of the numClients forks and waits up to maxWaitForClientMillis for one to free up.

Against the Bookshelf file (fetcher inbox, emitter extracts), this program writes one extract from the fork, gets one back instead, and asks for a file that is not there:

package org.bookshelf.intake;

import java.nio.file.Path;

import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.pipes.api.FetchEmitTuple;
import org.apache.tika.pipes.api.PipesResult;
import org.apache.tika.pipes.api.emitter.EmitKey;
import org.apache.tika.pipes.api.fetcher.FetchKey;
import org.apache.tika.pipes.core.EmitStrategy;
import org.apache.tika.pipes.core.EmitStrategyConfig;
import org.apache.tika.pipes.core.PipesParser;

public class OneDoc {
    public static void main(String[] args) throws Exception {
        try (PipesParser parser = PipesParser.load(Path.of("/srv/bookshelf/tika-config.json"))) {
            // 1. the fork writes the extract itself
            ParseContext emitAll = new ParseContext();
            emitAll.set(EmitStrategyConfig.class, new EmitStrategyConfig(EmitStrategy.EMIT_ALL));
            PipesResult written = parser.parse(new FetchEmitTuple("manuscript-001",
                    new FetchKey("inbox", "manuscript.docx"),
                    new EmitKey("extracts", "single/manuscript"), new Metadata(), emitAll));
            System.out.println("emit:     " + written.status());

            // 2. the result comes back instead
            ParseContext passback = new ParseContext();
            passback.set(EmitStrategyConfig.class, new EmitStrategyConfig(EmitStrategy.PASSBACK_ALL));
            PipesResult returned = parser.parse(new FetchEmitTuple("email-001",
                    new FetchKey("inbox", "publisher-email.eml"), EmitKey.NO_EMIT,
                    new Metadata(), passback));
            System.out.println("passback: " + returned.status());
            if (returned.isSuccess() && returned.emitData() != null) {
                for (Metadata m : returned.emitData().getMetadataList()) {
                    System.out.println("  " + m.get(TikaCoreProperties.EMBEDDED_RESOURCE_PATH)
                            + " | " + m.get("Content-Type"));
                }
            }

            // 3. a key that is not there
            PipesResult missing = parser.parse(new FetchEmitTuple("missing-001",
                    new FetchKey("inbox", "no-such-file.pdf"), new EmitKey("extracts", "missing")));
            System.out.println("missing:  " + missing.status() + " taskException=" + missing.isTaskException());
        }
    }
}
emit:     EMIT_SUCCESS
passback: PARSE_SUCCESS
  null | message/rfc822
  /samples.zip/chapter-one.docx/figure1.png | image/png
  /samples.zip/chapter-one.docx | application/vnd.openxmlformats-officedocument.wordprocessingml.document
  /samples.zip/catalogue.pdf | application/pdf
  /samples.zip/cover-note.txt | text/plain; charset=windows-1252
  /samples.zip | application/zip
missing:  FETCH_EXCEPTION taskException=true

The first call wrote /srv/bookshelf/extracts/single/manuscript.json (the emitter appended .json), which begins:

[ {
  "Content-Length" : "1418",
  "Content-Type" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  "dc:creator" : "Ana Torres",
  "dc:title" : "The Lighthouse Keeper",
  "fs:created" : "2026-10-12T20:47:24Z",
  "fs:modified" : "2026-10-12T20:47:24Z",
  "tk:content-handler-type" : "MARKDOWN",
  "tk:parse-time-millis" : "38",
  "tk:resource-name" : "manuscript.docx",
  "tk:source-path" : "manuscript.docx",
  "tk:content" : "The Lighthouse Keeper  \n\nThe Lighthouse Keeper\n\nChapter one begins at dusk.\n"
} ]

(shortened: the real file also lists fs:accessed, the detection keys, tk:parsed-by and the ZIP detector flag.) A passed-back result under PipesParser is yours to write: there is no emitter thread behind it.

Source: Apache Tika 4.1.x, Tika Pipes from Java (PipesParser, Getting the result back instead).

AsyncProcessor: the whole pipeline

AsyncProcessor adds the queue, the worker threads that feed the forks, the emitter threads and the reporters. You offer tuples; it does the rest. With an iterator in the config, the tuples come from the config too:

package org.bookshelf.intake;

import java.nio.file.Path;
import java.util.concurrent.TimeoutException;

import org.apache.tika.config.loader.TikaJsonConfig;
import org.apache.tika.pipes.api.FetchEmitTuple;
import org.apache.tika.pipes.api.pipesiterator.PipesIterator;
import org.apache.tika.pipes.core.async.AsyncProcessor;
import org.apache.tika.pipes.core.pipesiterator.PipesIteratorManager;
import org.apache.tika.plugins.TikaPluginManager;

public class Intake {
    public static void main(String[] args) throws Exception {
        Path configPath = Path.of(args[0]);
        TikaJsonConfig jsonConfig = TikaJsonConfig.load(configPath);
        PipesIterator iterator = PipesIteratorManager
                .load(TikaPluginManager.load(jsonConfig), jsonConfig)
                .orElseThrow(() -> new IllegalStateException("no pipes-iterator configured"));
        long start = System.currentTimeMillis();
        try (AsyncProcessor processor = AsyncProcessor.load(configPath, iterator)) {
            for (FetchEmitTuple tuple : iterator) {
                if (!processor.offer(tuple, 120_000)) {
                    throw new TimeoutException("queue stayed full for two minutes");
                }
            }
            processor.finished();                // no more input
            while (processor.checkActive()) {    // rethrows the first worker or emitter failure
                Thread.sleep(500);
            }
            System.out.println("processed " + processor.getTotalProcessed() + " documents in "
                    + (System.currentTimeMillis() - start) + " ms");
        }
    }
}
java -cp "intake.jar:lib/*" org.bookshelf.intake.Intake /srv/bookshelf/tika-config.json
processed 9 documents in 8313 ms

The extracts folder then held the nine .json files, status.json showed "statusCounts":{"PARSE_SUCCESS":9} and the JSONL ledger had nine lines (shown in the Reporters section). Swapping the iterator for the CSV one produced the 2026/BK-000n.json extracts described there.

An iterator is optional. Pass null as the second argument of AsyncProcessor.load and offer tuples you build yourself, from a Kafka consumer, a database cursor or an upload queue. offer blocks while the queue (queueSize) is full and returns false if it never drains. Per-document outcomes go to the reporters, not back to the caller; checkActive only surfaces failures that stop the pipeline. Because AsyncProcessor emits passed-back extracts itself, the default DYNAMIC strategy is the right one here.

Source: Apache Tika 4.1.x, Tika Pipes from Java (AsyncProcessor: the whole pipeline).

Lifecycle

  • PipesForkParser, PipesParser and AsyncProcessor are Closeable. Closing kills the forks and deletes the temporary files the parent created for them (for PipesForkParser, the generated config file). Use try-with-resources, or a container-managed bean with a destroy method.

  • Create each once and share it; they are thread-safe. A new instance per document starts a JVM per document.

  • Close every TikaInputStream you pass in: a stream without a file is spooled to a temporary file when it is larger than maxInlineBytes, and that file is removed only on close. Prefer parse(Path) when the document is already on disk; random-access formats (ZIP, OLE2, PDF) are then read in place.

  • The forks inherit the parent’s class path. A dependency you add to the application is visible to the forks; a -cp override in forkedJvmArgs that points at a different Tika version is unsupported (the IPC format is same-version only).

The Spring Boot Integration page of this section covers PipesForkParser as a Spring bean with a matching shutdown.

Shared server ("YOLO") mode

By default every client has its own fork, so a crash can only take down the document that caused it. "useSharedServer": true connects all clients to one forked JVM with several connection handlers. That saves the memory of numClients - 1 JVMs and is often faster: one warm JIT profile and one larger heap. The 4.1.x documentation reports one benchmark in which shared mode came within about 5 to 15 percent of in-process parsing while per-client mode was 20 to 35 percent slower.

{
  "pipes": {
    "numClients": 4,
    "useSharedServer": true,
    "forkedJvmArgs": ["-Xmx4g"],
    "emitStrategy": { "type": "PASSBACK_ALL" }
  },
  "parse-context": {
    "timeout-limits": { "progressTimeoutMillis": 60000 }
  }
}

The price is isolation, which is the reason Tika Pipes exists:

  • Any OOM, timeout or crash kills the shared process and every in-flight document, not only the bad one. The server is restarted and the clients reconnect, but retrying the lost requests is up to you.

  • With EMIT_ALL, or DYNAMIC for large extracts, a client whose document was emitted just before another document crashed the process sees only "crash" and cannot tell whether its extract reached the destination. PASSBACK_ALL, as above, removes that ambiguity.

  • All parses share one heap, so size it for numClients times the worst document, plus headroom.

  • Parsers run concurrently in one JVM, which can expose thread-safety bugs that per-client mode hides.

The project marks it experimental and not recommended for production. Consider it only for well-behaved corpora under hard memory limits, after testing with your own documents. Not run for this page.

Troubleshooting and logging

Telling fork lines from parent lines

Forks inherit the parent’s stdout and stderr, which suits containers: everything reaches the container log. The bundled fork logging configuration (pipes-fork-server-default-log4j2.xml) marks fork lines with [fork], and both sides print pipesClientId=N, so you can follow one fork across the boundary:

INFO  [main] 22:47:56,180 [fork] pipesClientId=0 org.apache.tika.pipes.core.server.PipesServer forked JVM max heap: 512 MB
INFO  [pool-2-thread-3] 22:50:35,216 org.apache.tika.pipes.core.PipesClient clientId=0: server successfully started

The 4.1.0 forks in these runs also logged one WORKER_TIMING line per document at INFO, with the deserialization, fetch, parse and emit times in microseconds and the tuple id. For PipesForkParser the id is the absolute file path, so treat these lines as containing file names:

INFO  [main] 22:48:01,909 [fork] pipesClientId=0 org.apache.tika.pipes.timing.worker WORKER_TIMING id=/srv/bookshelf/inbox/submission.zip req_deser_us=535 ctx_merge_us=495 intermediate_wait_us=2540 handoff_us=472 fetch_us=997 parse_us=14872 emit_us=9 worker_wall_us=15896 intermediate_us=130 resp_ser_us=362 resp_write_us=138 resp_ack_us=1120 resp_bytes=3152

Knobs

Setting Read by Effect

-Dtika.pipes.server.stdio=discard

parent

Drop all fork stdout and stderr (used for the runs on this page to keep the console readable). Default: inherit.

-Dlog4j.configurationFile=... in forkedJvmArgs

fork

Your own fork logging config instead of the bundled one.

-Dtika.pipes.cacheMemoryBudgetBytes=... in forkedJvmArgs

fork

In-memory cache budget per fork; 0 or less disables it.

-Djava.io.tmpdir=...

parent (and inherited)

Where spooled input, unpacked files and fork folders go. Never on tmpfs.

tika-app -v

tika-app

Root logger at DEBUG for one run.

TIKA_PIPES_PARENT_PID

fork (set automatically)

Parent-death watch.

A property that "does nothing" is usually set on the wrong JVM: fork-side properties belong in forkedJvmArgs, and set on the parent (java -D..., MAVEN_OPTS, Surefire argLine) they are silently ignored by the forks.

Logging levels follow a convention: ERROR and WARN never contain literal fetch or emit keys (documents are identified by the tuple id), INFO is lifecycle, DEBUG is per-document progress and counts, TRACE adds the literal keys (paths, URLs, object keys). HTTP fetchers may still include response bytes in exception messages, limited by maxErrMsgSize.

Common problems

Symptom Cause and fix

Process exited with code 1 before connecting to socket

The fork died during start-up: a bad forkedJvmArgs flag, a missing class, too little heap. Read the lines tagged [fork] just before it.

FAILED_TO_INITIALIZE, Cannot run program ...

javaPath wrong (see the run above), or the fork cannot load the config. Fatal: fix and restart.

A JVM crash log in the parent’s output

A native crash in a fork. Tika points -XX:ErrorFile= at the fork’s temp folder and copies the hs_err_pid file into the parent’s log before cleaning up.

WARN ... No '<dir>' root, No plugins

plugin-roots folder missing. Harmless if the plugins are on the class path; otherwise unzip the distribution or set plugin-roots / setPluginsDir.

Unknown component name at load

Typo, or the plugin is not loaded. The error lists the names that are available.

CLIENT_UNAVAILABLE_WITHIN_MS

Every fork busy for maxWaitForClientMillis. Raise numClients (within the CPU rule), lower the load, or retry later.

PAYLOAD_LIMIT_EXCEEDED, or UNSPECIFIED_CRASH on large documents

The result or request exceeded maxIpcPayloadBytes. Raise it together with the fork heap, lower the write limit, or use EMIT_ALL for large extracts.

Crashed documents missing from the output

Expected with emitIntermediateResults: false. Add a file-system-jsonl-reporter with crash includes, or set emitIntermediateResults: true.

Leftover pipes-server-* folders or .tmp files

The whole process group was killed at once (container stop, SIGKILL), or a worker died mid-write with atomicWrites. Apply your own retention; tell crawlers to ignore *.tmp.

Host runs out of memory while spooling

java.io.tmpdir on tmpfs (/dev/shm). Move it to a disk-backed volume.

Source: Apache Tika 4.1.x, Pipes Troubleshooting and Pipes Configuration (Where temporary files go).

Memory, performance and robustness

  • Every fork is a JVM. Memory is roughly numClients times (fork heap plus JVM overhead) plus the parent. Start-up takes seconds, so short-lived pipelines and per-request construction waste most of their time starting forks.

  • Throughput scales with forks until the CPU rule (two cores per fork plus two for the parent) is exceeded. Shared mode trades isolation for throughput and memory.

  • IPC cost. PASSBACK_ALL and small-extract passback serialize every metadata list across the socket and hold it in the parent; EMIT_ALL avoids that but loses batching. Large documents are what DYNAMIC exists for.

  • Disk. Inputs larger than maxInlineBytes, streams from remote fetchers with spoolToTemp, unpacked embedded files and random-access parsers all use the temporary folder. 4.1.0 reduced spooling noticeably; budget the volume anyway.

  • Bounding the work. Write limits, embedded limits, maxUnpackBytes and the timeouts belong in parse-context (see Robustness, Limits & Timeouts). The fork protects the parent; the limits protect the fork’s time and your destination.

  • Leaks. maxFilesProcessedPerProcess recycles forks; lower it for parsers known to leak (OCR, some image libraries).

Pitfalls and security

  • Pipes is the sandbox, not an authentication layer. tika-server and tika-grpc run every parse in forks, but they are not security boundaries: whoever can reach /pipes, /async or the gRPC fetcher calls can make the server read any path or URL its fetchers allow. Keep them on trusted networks, behind authentication, with allowPipes off unless you need it. See Security and Tika Server.

  • basePath is the only containment for the file-system components. Never combine allowAbsolutePaths: true with keys from callers. Do not rely on symbolic-link containment on the emitter side.

  • Credentials go in environment variables (${env:NAME}) or the cloud SDKs' own credential chains, never in the file. The forks inherit the parent’s environment.

  • Do not retry crashes blindly. A document that produced OOM or TIMEOUT will usually do so again; route it to a dead-letter list (the JSONL ledger) and retry it with different limits, if at all.

  • Defaults differ by entry point. Markdown for tika-app and the pipes config, plain text for PipesForkParser; numClients from the CPU count, 2 for the CLIs, 1 for PipesForkParser. Set what you depend on.

  • getContent() is the container only in RMETA mode. Embedded text is in the other list elements.

  • Kafka iterator delivery is at most once. Use your own consumer when a lost document matters.

  • Fork-side settings on the parent are ignored. Heap, cache budget and fork logging go into forkedJvmArgs.

  • Keep parent and forks on the same Tika version. The IPC protocol is internal and same-version only.

From 1.x to 4.x

Tika in Action (2011, Tika 1.x) handles robustness in chapter 9 with the ForkParser, which streamed SAX events from a child process, and with careful thread and memory management in the caller. Batch processing meant your own loops, or later the tika-batch module behind tika-app’s -bc batch mode.

Version What changed for Pipes

1.x (the book)

ForkParser for isolation; tika-batch for directory runs; no fetchers or emitters.

2.0 (July 2021)

Tika Pipes arrives: PipesClient and PipesServer, fetchers (a FileSystemFetcher replaces tika-server’s enableFileUrl), emitters and pipes iterators, configured in tika-config.xml. 2.x adds GCS, OpenSearch and Solr components and a JDBC reporter.

3.x

Same model on Java 11, configured by class name in tika-config.xml (<fetchers>, <emitters>, <pipesIterator>, <pipes>, <async>), with defaults such as numClients 4 and a 60 s timeoutMillis. 3.3.0 adds per-file timeouts; 3.3.2 gates /pipes, /async and /status behind enableUnsecureFeatures. ForkParser and tika-batch still exist.

4.0 (August 2026)

Pipes becomes the engine of every service: tika-server’s parsing endpoints and tika-grpc run in forks. JSON configuration; components become PF4J plugins in tika-pipes-* modules, with tika-pipes-core and a separate tika-pipes-file-system plugin. PipesForkParser replaces the removed ForkParser; tika-batch and tika-app’s -bc mode are removed in favour of -a. New parse modes NO_PARSE, CONTENT_ONLY and UNPACK; new plugins (Elasticsearch, Google Drive, Microsoft Graph, Azure Blob, JSON, HTTP, Atlassian JWT, Ignite config store); unified timeout model; staleFetcherTimeoutSeconds and staleFetcherDelaySeconds removed; the tuple JSON key parseContext renamed parse-context; http-fetcher verifies TLS by default.

4.1 (September 2026)

file-system-jsonl-reporter, less spooling and a fork cache budget, environment-variable interpolation in tika-config.json, PipesForkParser honouring javaPath and socketTimeoutMillis, plugins no longer bundling Jackson, tempDirectory deprecated, a Kafka iterator that no longer stops on the first empty poll.

A 3.x pipeline looked like this. It is shown only to recognise old files: the element names (fetchers/fetcher, emitters/emitter, pipesIterator, pipes, async) were read from the 3.3.2 sources, the rest follows the 3.x conventions, and it was not run.

3.x only: tika-config.xml (shape, not run)
<properties>
  <pipes>
    <params>
      <numClients>2</numClients>
      <forkedJvmArgs><arg>-Xmx512m</arg></forkedJvmArgs>
    </params>
  </pipes>
  <fetchers>
    <fetcher class="org.apache.tika.pipes.fetcher.fs.FileSystemFetcher">
      <name>inbox</name>
      <basePath>/srv/bookshelf/inbox</basePath>
    </fetcher>
  </fetchers>
  <!-- emitters/emitter, pipesIterator and async follow the same pattern -->
</properties>

There is no automatic converter for these sections: --convert-config-xml-to-json converts only the parsers section, so rewrite pipes configuration by hand using the tables above. In code, org.apache.tika.pipes.PipesParser and friends moved to org.apache.tika.pipes.core and org.apache.tika.pipes.api (FetchEmitTuple, PipesResult, the fetcher and emitter interfaces); ForkParser users switch to PipesForkParser, whose results carry the text in TikaCoreProperties.TIKA_CONTENT instead of streaming SAX events.

Source: Apache Tika CHANGES.txt (2.0.0, 4.0.0 and 4.1.0 sections), Migrating to Tika 4.x and the 3.3.2 PipesConfigBase, FetcherManager and PipesIterator sources.

What was verified and what was not

Verified by running against Tika 4.1.0:

  • tika-app -i/-o with -n, --handler m --content-only and -Z, and the outputs shown.

  • PipesForkParser with a custom config on files and on a stream, the RMETA output, a forced TIMEOUT and a FAILED_TO_INITIALIZE from a bad javaPath.

  • PipesParser with EMIT_ALL, PASSBACK_ALL and a missing key (FETCH_EXCEPTION).

  • AsyncProcessor with the file-system iterator and with the CSV iterator, the file-system emitter, the file-system-reporter and the file-system-jsonl-reporter, and the effect of emitIntermediateResults.

  • The fork log lines, the CPU sizing line and the plugin-roots warning.

Read from the 4.1.0 source but not run: every default in the pipes table, the parse-mode semantics beyond RMETA, CONTENT_ONLY and UNPACK, the Kafka record mapping. Taken from the 4.1.x documentation only: the S3, GCS, Azure Blob, HTTP, Google Drive, Microsoft Graph, JDBC, JSON, Kafka, Solr, Elasticsearch and OpenSearch configuration keys, shared server mode and its benchmark figures, the plugin build, and tika-server and tika-grpc behaviour. The plugin factory example was not compiled. The 3.x XML block is a shape, not a tested file.