A Software Engineer at Teramind plays a pivotal role in designing, building, and scaling enterprise-grade employee monitoring, data loss prevention (DLP), and insider threat detection software. Because Teramind products are deployed across thousands of customer endpoints and massive cloud infrastructures, engineers here face unique challenges related to low-latency data collection, high-throughput pipeline processing, and real-time behavioral analysis. The systems you build and maintain directly impact how organizations protect their digital assets and optimize operational efficiency. This requires a deep understanding of system-level performance, security protocols, and robust full-stack architecture. Engineers work on a mix of lightweight endpoint agents, complex on-premise deployments, and highly scalable cloud architectures. Working as a Software Engineer at Teramind demands a balance of rapid execution and extreme technical precision. Because the company's core product involves deep system monitoring, the engineering team relies on highly optimized code that must run seamlessly in the background of target operating systems without degrading user experience or performance.
HR Screening
reportedInitial screening conducted by HR to assess candidate fit.
What to demonstrate
- Initial screening conducted by HR to assess candidate fit
- Depth in Python
How to prepare
- Be able to walk your CV end to end in two minutes, and say why this company specifically.
- Have your salary expectations, notice period and location constraints ready, and ask for the rest of the loop in writing.
Online Assessment
reportedEvaluation of critical thinking, logical reasoning, and basic coding proficiency.
What to demonstrate
- Evaluation of critical thinking, logical reasoning, and basic coding proficiency
- Depth in Python
How to prepare
- Answer aloud and timed: What are the time and space complexities of inserting and deleting elements from a doubly linked list?
- Answer aloud and timed: How does memory management differ when executing processes in a monolithic architecture versus microservices?
Technical Round
reportedLive coding, system architecture, and code reviews are the primary focus.
What to demonstrate
- Live coding, system architecture, and code reviews are the primary focus
- Depth in Python
How to prepare
- Answer aloud and timed: Implement a small end-to-end service with a Node.js backend and a React frontend that fetches, filters, and displays a list of active system processes.
- Answer aloud and timed: Write a program to download a CSV file from a provided Google Sheets URL, parse the data, and import it into a local database using your preferred language and tools.
Final Stage
reportedDeep dive with engineering leadership or executive management to discuss contract terms and technical expectations.
What to demonstrate
- Deep dive with engineering leadership or executive management to discuss contract terms and technical expectations
- Depth in Python
How to prepare
- Answer aloud and timed: Create a functional full-stack TTL cache implementation within a strict 30-minute window.
- Answer aloud and timed: How would you handle real-time state synchronization between a React frontend and a fast-updating backend data stream?
PracHub editorial advice for the preparation topics above.
Going into the loop without having done this.
To maximize your chances of success during the Teramind interview process, keep these practical tips in mind:
Going into the loop without having done this.
Prepare for Screen Sharing: You will be asked to share your screen and code live in front of the interviewer. Practice thinking out loud and structuring your workspace for maximum efficiency before the call.
Going into the loop without having done this.
Ensure you are fully comfortable with Teramind's internal tracking policy before proceeding deep into the interview process. The company requires all remote contractors to install their monitoring software, which tracks mouse movement, keyboard activity, and takes live screenshots to log billable hours.
Going into the loop without having done this.
Master Your Linux Basics: Be ready for rapid-fire questions on Linux terminal commands, process management, and basic networking. This is especially common if you reach the interview stages with senior engineering leadership.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
Implement a small end-to-end service with a Node.js backend and a React frontend that fetches, filters, and di
Implement a small end-to-end service with a Node.js backend and a React frontend that fetches, filters, and displays a list of active system processes.
Approach
- Interpretation: list the OS processes on the machine running the Node server. Fix the contract first, e.g.
GET /api/processes?q=node&minCpu=1&sort=cpu&limit=50returning[{pid, name, cpu, memMb}], so the backend and the React side can be built and tested independently. - Backend: an Express route runs
execFile('ps', ['-A', '-o', 'pid=', '-o', 'pcpu=', '-o', 'rss=', '-o', 'comm=']): no shell, so no injection, and one-oper column since POSIX reads all text after=as the header. Match^\s*(\d+)\s+(\S+)\s+(\d+)\s+(.*)$so names keep spaces;rssis KiB. - Filter, sort and limit on the server after parsing: validate
minCpuas a number, allowsortonly ifObject.hasOwn(SORTS, sort)(a plain lookup letsconstructorthrough), clamplimit, and return 400 otherwise. Parsing and filtering are O(n) in the process count; sorting is O(n log n). - Frontend: state for
processes,query,loadinganderror; auseEffectrefetches when the debounced query changes, cancels the previous request withAbortController, and polls every few seconds with the interval cleared in cleanup. Key rows bypid; render empty and error states. - Wiring and safety: proxy
/apithrough the frontend dev server (or enable CORS on Express) and return a 500 with a readable message ifpsfails. A process list reveals what runs on the host, so bind to localhost or put it behind auth. - Test what breaks easily: unit-test the parser on a captured
psoutput fixture (names with spaces, trailing newline), test the filter and sort function without Express, and mockfetchin a component test that checks the empty, error and filtered states.
Follow-up
- How would you add a Kill button safely? Behind auth, accept only an integer PID above 1 that isn't
process.pid(kill(0)hits your process group,-1every process you may signal), sendSIGTERM, and mapESRCH/EPERMto 404/403. - Why can the CPU column look wrong? On Linux,
ps%CPUis CPU time divided by the process's lifetime, not current usage; sample CPU time twice and divide the delta by the interval. - How would you make the list live instead of polling? Push changes over SSE or a WebSocket once a second, sending only added, removed and changed PIDs, and merge them into state keyed by
pid.
Write a program to download a CSV file from a provided Google Sheets URL, parse the data, and import it into a
Write a program to download a CSV file from a provided Google Sheets URL, parse the data, and import it into a local database using your preferred language and tools.
Approach
- Don't fetch the share link itself: take the ID from
/spreadsheets/d/<id>/and request.../d/<id>/export?format=csv&gid=<gid>, leaving outgidfor the first tab. The sheet must be readable by anyone with the link, or you get an HTML sign-in page, not CSV. - Download defensively: set a timeout, follow redirects (the export endpoint typically redirects to a download host), check that
Content-Typeistext/csvbefore parsing, and decode withutf-8-sigso a byte-order mark doesn't end up in the first column's name. - Parse with a real CSV parser (
csv.readerorDictReader), neversplit(','): quoted fields can legally contain commas, quotes and newlines. Normalize headers into safe, unique column names and decide up front how to treat blank rows and rows longer than the header. - Insert with parameterized
executemany, never by formatting values into SQL, and quote identifiers that come from the header. Storing every column as TEXT is safe; inferring INTEGER or REAL is a bonus, but spreadsheet columns often mix types, so validate before trusting a guess. - Make reruns safe: replace the table atomically (explicit
BEGIN, drop, create, insert, commit) or upsert on a natural key. Python'ssqlite3autocommits DDL that runs outside a transaction, so without the explicitBEGINa failed load can leave the old data dropped. - Package it as a CLI (
import_sheet.py URL --db data.db --table orders), log rows imported and skipped, and exit non-zero on failure so cron or CI notices. For very large sheets, stream the download to disk and insert in batches instead of holding everything in memory.
Worked solution 30 min
Sheet URL to SQLite importer
export_urlpulls the spreadsheet ID and optionalgidout of an edit or share link, including multi-account/spreadsheets/u/1/d/links, and omitsgidwhen the link has none so Google exports the first tab; publish-to-web/d/e/links map topub?output=csvinstead.download_csvfetches with a 30-second timeout, raises on a non-CSV response (the private-sheet case) and returns decoded text with any BOM removed.import_csvturns headers into lowercase snake_case names, names blankscol_Nand suffixes duplicates, then skips blank rows, pads short rows, rejects rows with extra non-empty cells and stores empty cells as NULL.- The drop, create and bulk insert run inside one explicit transaction, so an error leaves the previous table untouched. Wire it to a CLI with
sqlite3.connect(db_path)andimport_csv(download_csv(url), conn, table).
import csv, io, re, urllib.request
def export_url(sheet_url):
"""Turn a Sheets edit/share or publish-to-web link into its CSV download link."""
m = re.search(r"/spreadsheets/(?:u/\d+/)?d/(e/[\w-]+|[\w-]+)", sheet_url)
if not m:
raise ValueError("not a Google Sheets URL")
gid = re.search(r"[#?&]gid=(\d+)", sheet_url) # no gid -> Google exports the first tab
base = f"https://docs.google.com/spreadsheets/d/{m.group(1)}"
if m.group(1).startswith("e/"): # publish-to-web link
return base + "/pub?output=csv" + (f"&gid={gid.group(1)}&single=true" if gid else "")
return base + "/export?format=csv" + (f"&gid={gid.group(1)}" if gid else "")
def download_csv(sheet_url, timeout=30):
with urllib.request.urlopen(export_url(sheet_url), timeout=timeout) as resp:
if "text/csv" not in resp.headers.get("Content-Type", ""):
raise RuntimeError("got a non-CSV response; is the sheet link-shared?")
return resp.read().decode("utf-8-sig") # drops a leading BOM if present
def import_csv(text, conn, table):
reader = csv.reader(io.StringIO(text, newline=""))
header = next(reader, None)
if not header:
raise ValueError("CSV has no header row")
cols = []
for i, h in enumerate(header): # safe, unique column names
name = re.sub(r"\W+", "_", h.strip().lower()).strip("_") or f"col_{i + 1}"
while name in cols:
name += "_dup"
cols.append(name)
n, rows = len(cols), []
for rec_no, row in enumerate(reader, start=2):
if not any(cell.strip() for cell in row):
continue # skip blank rows
if any(cell.strip() for cell in row[n:]):
raise ValueError(f"record {rec_no} has more cells than the header")
rows.append([cell or None for cell in (row + [""] * n)[:n]]) # '' -> NULL
q = lambda ident: '"' + ident.replace('"', '""') + '"'
conn.execute("BEGIN") # explicit: sqlite3 would otherwise autocommit the DDL
with conn: # commit on success, roll back drop + create + inserts on any error
conn.execute(f"DROP TABLE IF EXISTS {q(table)}")
col_defs = ", ".join(q(c) + " TEXT" for c in cols)
conn.execute(f"CREATE TABLE {q(table)} ({col_defs})")
conn.executemany(f"INSERT INTO {q(table)} VALUES ({', '.join('?' * n)})", rows)
return len(rows)
Scroll sideways to view long lines.
Follow-up
- How would you infer column types? Try int, then float, then date on every non-empty value; fall back to TEXT if any fails or has a leading zero before another digit ('02134' and '007' are identifiers, '0' and '0.5' are not).
- What if the sheet is private? Call the Sheets API with a service account that has been granted read access to that sheet, and keep the service account key out of the repo.
- How would you test it without the network? Stub the download function (or
urlopen) and feed fixture CSVs with quoted commas, embedded newlines, blank rows, a BOM and an HTML error page.
Create a functional full-stack TTL cache implementation within a strict 30-minute window.
Create a functional full-stack TTL cache implementation within a strict 30-minute window.
Approach
- Interpretation: 'full-stack' means a cache service with an HTTP API plus a small UI to set, read and watch entries expire; here Flask and one HTML page. Budget ~3 minutes for the API contract, ~12 for the core and tests, ~8 for routes, ~7 for the UI; get one path working end to end first.
- API:
PUT /cache/<key>with{value, ttlSeconds}returns 204;GET /cache/<key>returns{value}or 404 when missing or expired;DELETE /cache/<key>;GET /cachelists live entries withexpiresInMs. Return 400 for booleans, non-numbers, NaN, infinity, TTLs <= 0 and TTLs over a 30-day cap. - Core: a map of key to
(value, expires_at)on a monotonic clock, lazy expiry insideget, and a background sweeper thread that pops a min-heap of(expires_at, seq, key)once a second so unread keys get freed. Theseqtie-breaker stops the heap comparing keys; one lock guards every method. - Inject the clock so tests never sleep: set with ttl 10, advance a fake clock by 9.999 and expect a hit, advance to 10 and expect a miss. Also test that an overwrite resets expiry and that the first write's leftover heap row doesn't evict the new value.
- UI: one HTML page served at
/with a form for key, value and TTL and a table of live keys fromGET /cache, refreshed after each write and once a second. Fill cells withtextContent, notinnerHTML, so a stored value can't inject script, and show a 404 as 'expired or missing'. - Close by naming what was cut and the order you'd add it back: a size cap first, since memory is unbounded; then auth; then a shared store such as Redis if entries must survive restarts or be shared across instances.
Follow-up
- How would the UI show a live countdown without calling
GET /cacheevery second? FetchexpiresInMsonce, count down locally fromperformance.now(), and refetch after a write, when a row hits zero, or on a slow poll for others' writes. - Why does the cache need a lock? Flask's dev server runs each request on its own thread by default, and the sweeper is one more: unlocked,
items()can fail mid-iteration andsweepcan delete a fresh write. - Why not one
threading.Timerper key? Each timer is a thread and every overwrite must cancel the old one, so threads and memory grow with the key count, while one sweeper over a heap stays a single thread.
Hold a per-tenant active cap against concurrent creates
A tenant on the standard plan may hold at most 50 resources with status='active'. The create handler runs SELECT count(*) FROM resource WHERE tenant_id = $1 AND status = 'active', compares to 50, then inserts. Two creates arrive 3 ms apart on different instances and the tenant lands at 51. Name the anomaly, say whether PostgreSQL 16 READ COMMITTED or REPEATABLE READ prevents it and why, then give an implementation that holds the cap at READ COMMITTED with the exact statements. Finally, say what changes when the cap is 'at most one running export per tenant' on job_run.
Approach
- Name it: write skew. The two transactions read an overlapping set and write disjoint rows, so there is no row-level conflict for the engine to detect and each commit is individually legal.
- Rule out the levels precisely. READ COMMITTED takes a fresh snapshot per statement and takes no lock on the counted rows, so both see 49. PostgreSQL's REPEATABLE READ is snapshot isolation: it removes non-repeatable reads and phantoms within the snapshot but still admits write skew, because the anomaly is not a re-read of a changed row, it is a read of a set that a concurrent transaction invalidates. Only SERIALIZABLE closes it, by tracking the read dependency and aborting one transaction with SQLSTATE 40001 — a guarantee that exists only if the application re-runs the whole transaction from the read.
- Convert the set predicate into a single-row conflict: keep tenant.active_resource_count and run UPDATE tenant SET active_resource_count = active_resource_count + 1 WHERE tenant_id = $1 AND active_resource_count < 50 in the same transaction as the INSERT. Zero affected rows is the cap, returned as 409. The row lock serialises the decision at any isolation level, and contention is bounded to one tenant's row — which is also the fair-scheduling unit, unlike a global counter that would convoy every tenant behind one row.
- State the cost you just took on: a counter is a second source of truth that can drift, so every path that changes status must adjust it inside the same transaction, and a periodic reconciliation has to exist, with resource_revision as the authority for what the count should have been.
Follow-up
- A resource moves from archived back to active. Which statements change, and what breaks if the counter update and the status change land in different transactions?
- The cap becomes plan-dependent and a plan can change mid-month. Where does the number 50 live, and who reads it?
Find version gaps and relay lag with window functions
outbox_event holds event_id, aggregate_type, aggregate_id, aggregate_version, event_type, payload, status ('pending','published','dead'), attempts, created_at, published_at. A projection is missing rows and you must decide whether the relay skipped events or the consumer dropped them. Write three queries over the last seven days: one listing every aggregate_id whose published aggregate_version sequence has a hole, one giving per-day counts with a running total, and one returning the newest published event per aggregate. For each, say where the window function is evaluated relative to WHERE and LIMIT. PostgreSQL 16.
Approach
- Gaps: compute lead(aggregate_version) OVER (PARTITION BY aggregate_id ORDER BY aggregate_version) in a subquery, then filter next_version <> aggregate_version + 1 in the outer query. Window functions are evaluated after WHERE, GROUP BY and HAVING and before the outer ORDER BY and LIMIT, so the predicate cannot sit in the same WHERE clause and PostgreSQL 16 has no QUALIFY.
- Say what the seven-day filter does to the answer: it truncates every partition, so the first row per aggregate has no predecessor inside the window and a hole spanning the boundary is invisible. Widen the window, or join to resource.version as the authority for the true maximum.
- Running total: SELECT date_trunc('day', created_at) AS d, count() AS n, sum(count()) OVER (ORDER BY date_trunc('day', created_at) ROWS UNBOUNDED PRECEDING). An aggregate inside a window call is legal because grouping runs before windowing. The grouping key is unique per row here so ROWS and RANGE agree, but write the frame anyway — over ungrouped rows with tied timestamps the default RANGE frame pulls in every peer row and the total jumps.
- Newest per aggregate: DISTINCT ON (aggregate_id) ... ORDER BY aggregate_id, aggregate_version DESC is the cheap PostgreSQL-only form when an index matches that order; row_number() OVER (PARTITION BY aggregate_id ORDER BY aggregate_version DESC) = 1 is the portable form and needs a subquery for the same evaluation-order reason as the gap query.
Follow-up
- Relay failover redelivers events. Does a duplicate break the gap query, and how would you detect one from this table alone?
- Turn the gap check into a continuous monitor rather than a query someone runs after an incident. What does it watch?
Explain the difference between a stack and a queue, and describe a real-world scenario where you would use eac
Explain the difference between a stack and a queue, and describe a real-world scenario where you would use each.
Approach
- A stack is LIFO:
pushandpopboth work at the top, so the most recent item leaves first. A queue is FIFO:enqueueat the back,dequeueat the front, so the oldest item leaves first. Both cost O(1) per operation, amortized when a growable array backs them, since a resize copies everything. - A stack maps directly onto a dynamic array, pushing and popping at the end. A queue needs O(1) removal from the front, and Python's
list.pop(0)shifts every remaining element (O(n)); usecollections.deque, a linked list or a circular buffer. - Stack scenario: undo/redo in an editor. Each edit is pushed; Undo pops the latest edit and pushes it onto a redo stack, and any new edit clears the redo stack. The same shape explains the call stack, DFS and bracket matching in a parser.
- Queue scenario: a background job worker, e.g. uploads or emails processed in arrival order so no request waits behind ones that came later. BFS, print spoolers and a buffer between a fast producer and a slower consumer follow the same pattern.
- Tie the choice to the ordering rule: 'most recent first' means a stack, 'first come, first served' means a queue. Mention bounds too: a fixed-size ring buffer that blocks or drops when full, since an unbounded queue hides a consumer that can't keep up.
Follow-up
- How would you implement a queue using two stacks? Push onto an inbox stack; when the outbox is empty, pop everything across, which reverses the order and makes dequeue amortized O(1).
- How is a priority queue different? It removes the highest-priority item regardless of arrival order, typically via a binary heap with O(log n) push and pop.
- How would you share a queue between producer and consumer threads? Use
queue.Queue(maxsize=n):putblocks when full andgetblocks when empty, giving locking and backpressure.
How does a Time-To-Live (TTL) cache work, and how would you structure its eviction policy?
How does a Time-To-Live (TTL) cache work, and how would you structure its eviction policy?
Approach
- Each entry stores its value plus an absolute
expires_at = now + ttl, read from a monotonic clock so NTP or manual clock changes can't shift expiry. A read afterexpires_atis a miss even if the entry is still in memory. Writes reset expiry; a sliding TTL also extends it on each read. - Expiry and eviction are separate policies: expiry removes entries whose time is up, eviction removes live entries when the cache reaches its size limit. TTL alone doesn't bound memory, since a burst of writes with long TTLs can still exhaust it, so a complete answer has both.
- Expiry mechanics: lazy deletion checks
expires_aton eachget(cheap, but keys nobody reads linger), and an active sweeper cleans the rest. A min-heap ordered byexpires_atlets the sweeper pop only expired entries at O(log n) each; Redis instead samples keys with TTLs about 10 times a second. - Capacity eviction: when full, drop anything already expired first, then evict by LRU using a hash map plus doubly linked list (or
OrderedDict) for O(1)get,putand evict. LFU fits better when a few keys stay hot for a long time and one-off scans would flush an LRU. - Heap caveat: overwriting a key leaves its old heap entry behind. Store the expiry each heap entry was pushed with and skip mismatches when popping; with millions of keys, a timing wheel (one bucket per second) gives O(1) scheduling instead of O(log n).
- Choose TTLs from how stale the data may safely be, not from memory pressure: short for permissions or prices, long for static lookups. Add a few percent of random jitter so keys written together don't all expire in the same instant and hit the backing store at once.
Follow-up
- What happens when a hot key expires and a thousand requests miss at once? Let one request recompute it (single-flight, a lock per key) while the others wait or get the stale value (stale-while-revalidate).
- How does expiry work across several cache nodes? Keep the expiry with the entry on the node that owns the key, e.g. Redis
SET key value EX 60, rather than per-process timers, so every client sees one deadline. - Should a
getthat finds an expired entry delete it? Only if it already holds the write lock; under a read lock, return a miss and leave removal to the sweeper to avoid lock upgrades on the hot path.
What are the time and space complexities of inserting and deleting elements from a doubly linked list?
What are the time and space complexities of inserting and deleting elements from a doubly linked list?
Approach
- Given a reference to the node, or at the head or tail when both pointers are kept, insert and delete are O(1) time: an insert sets
new.prev,new.next,prev.nextandnext.prev; a delete setsnode.prev.nextandnode.next.prev. No other nodes are touched. - By index or by value it is O(n), because you must walk to the node first; for an index with a stored length you can start from the nearer end, which halves the walk but is still O(n). 'Insert is O(1)' holds only once you already hold the node.
- Space: each operation needs O(1) extra memory, one new node for an insert and nothing for a delete. The list itself is O(n) and carries two pointers per element, twice the link overhead of a singly linked list.
- Edge cases that break naive code: inserting into an empty list, deleting the head, tail or only node, and forgetting to update
headortail. Sentinel head and tail nodes remove every special case, because each real node always has both neighbors. - Contrast to show why it matters: a singly linked list needs the predecessor to delete, so removing a given node costs O(n) to find it; a dynamic array inserts in the middle in O(n) from shifting, but offers O(1) indexing and far better cache locality.
Follow-up
- Can a singly linked list delete a given node in O(1)? Copy the next node's value into it and unlink the next node; this fails for the tail and breaks any outside reference to the next node.
- Where does the O(1) unlink pay off in practice? An LRU cache: a hash map finds the node in O(1), then the node is unlinked and moved to the front in O(1).
- Is a linked list faster than an array in practice? Often not, because contiguous arrays are cache-friendly and shifting a few thousand elements can beat pointer-chasing, so lists win mainly when you already hold node references.
How does memory management differ when executing processes in a monolithic architecture versus microservices?
How does memory management differ when executing processes in a monolithic architecture versus microservices?
Approach
- Interpretation: both run ordinary OS processes with the same virtual memory; what changes is where the boundaries fall. A monolith instance is one process, or a pool of forked workers that share preloaded pages copy-on-write but each have their own heap; each microservice runs in its own processes.
- Monolith: all modules in a process share one heap, so calls pass references with no copying or serialization, and GC and memory limits are tuned once. The cost is shared fate: a leak or unbounded cache in one module can OOM the whole application, and threads sharing objects need synchronization.
- Microservices: each service pays its own runtime baseline (a JVM or Node process often starts at tens to hundreds of MB), loads its own copy of libraries and caches, and serializes data to cross the network, which adds allocation per call. Total footprint is usually higher, not lower.
- What microservices buy is isolation and independent sizing: each container has its own cgroup memory limit, so an OOM kills only that service, and a memory-hungry service can get 4 GB while a small one runs in 256 MB. Scaling a monolith for one hot path replicates its entire footprint.
- Container gotcha: cap the runtime's heap below the container limit (e.g.
-XX:MaxRAMPercentagefor the JVM,--max-old-space-sizefor Node) so the GC works before the kernel OOM-kills the process, and leave headroom for off-heap memory such as thread stacks and buffers. - State moves too: sessions or caches that modules shared in-process must go to a shared store such as Redis or be passed explicitly, since services can't read each other's memory. Per-instance caches also drift apart across replicas, so decide on TTLs or invalidation.
Follow-up
- How would you find a memory leak in one service? Track RSS and heap per instance over time, take two heap snapshots under steady load and diff the retained objects; unbounded caches and never-removed listeners are the usual culprits.
- How would you choose a service's memory limit? Load-test it, take peak RSS plus roughly 25% headroom, and alert on OOMKilled restarts (exit code 137) so an undersized limit shows up quickly.
- Can services share memory for speed? Only on one host, via POSIX shared memory or containers in a pod sharing
/dev/shm, which ties their deployment and failure together; normally share through a cache, or merge two chatty services.
How would you handle real-time state synchronization between a React frontend and a fast-updating backend data
How would you handle real-time state synchronization between a React frontend and a fast-updating backend data stream?
Approach
- Interpretation: updates arrive faster than a person can read (say hundreds per second) and the UI must stay correct and responsive. Push, don't poll: SSE when data only flows server-to-client (built-in reconnect), a WebSocket when the client also sends messages.
- Protocol: on connect the server sends a full snapshot with a version, then deltas tagged with an increasing sequence number and keyed by entity id. The client applies deltas in order, ignores duplicates, and requests a fresh snapshot when it sees a gap or after reconnecting.
- Keep the socket out of React's render path: write incoming deltas into a buffer or an external store (read via
useSyncExternalStore, or a library like Zustand) and flush to React at most once per animation frame or every 100-250 ms. OnesetStateper message can mean one render per message. - Render only what changed: normalize state as a map by id, replace only updated entries so unchanged rows keep referential equality, wrap rows in
React.memo, let components subscribe to their own slice via selectors, and virtualize long lists. - Coalesce under load: when several updates for the same id land in one flush window, keep only the latest; if the client still falls behind, have the server conflate per key. User edits apply optimistically and are reconciled against the version the server acknowledges.
- Handle the lifecycle: open the connection in
useEffectand close it in cleanup (StrictMode mounts twice in development), reconnect with exponential backoff plus jitter, use heartbeats to detect dead sockets, and show a 'reconnecting, data may be stale' indicator.
Follow-up
- The tab sat in the background for ten minutes; what happens? Browsers pause
requestAnimationFrameand throttle timers in hidden tabs, so the buffer grows; cap it, and onvisibilitychangedrop it and fetch a fresh snapshot. - How would you test this? Drive the store with a fake socket that emits scripted sequences including gaps, duplicates and a disconnect, assert on the resulting state, and profile renders under a burst with React DevTools.
- How does the backend fan out to many clients? Publish updates through pub/sub (e.g. Redis or Kafka) to stateless socket servers, each keeping a bounded per-client send buffer that conflates or disconnects slow consumers.
What Linux commands would you use to find and terminate a process running on a specific port?
What Linux commands would you use to find and terminate a process running on a specific port?
Approach
- Find the listener first:
sudo ss -ltnp 'sport = :8080'orsudo lsof -nP -iTCP:8080 -sTCP:LISTENshows the PID and program name.sudomatters because without it you can't see processes owned by other users;netstat -tulpnworks where net-tools is still installed. - Stop it gracefully:
kill <pid>sends SIGTERM so the app can close connections and flush; confirm withkill -0 <pid>orps -p <pid>, and escalate tokill -9(SIGKILL) only after a few seconds, because SIGKILL can't be caught and skips all cleanup. - Know the one-liners' traps:
lsof -i :8080also matches clients connected to a remote port 8080 (your browser, a proxy), sokill $(lsof -t -i :8080)can kill the wrong process; add-sTCP:LISTEN.fuser -k 8080/tcpworks but sends SIGKILL by default unless you pass-TERM. - Check who owns the process before killing it: if systemd, Docker or a manager like pm2 supervises it, it will simply restart.
systemctl status <pid>names the unit andps -o pid,ppid,user,cmd -p <pid>shows the parent; stop it withsystemctl stop <unit>ordocker stop <container>. - Empty
sudo ss -ltnpoutput but still 'Address already in use'? If the failing server runs in a container, it binds in the container's own port table, so check there (docker exec <c> ss -ltnp). Otherwise look for TIME_WAIT sockets, which block the bind unless the server setsSO_REUSEADDR.
Follow-up
- How would you script this for a dev tool? Resolve PIDs with
lsof -t -iTCP:$PORT -sTCP:LISTEN, exit if empty, send TERM, poll withkill -0for a few seconds, then send KILL only to survivors. - Why might
killfail with 'Operation not permitted'? The process belongs to another user or root; check withps -o user= -p <pid>that you really should kill it, then usesudo. - What changes on macOS? There is no
ss, so uselsof -nP -iTCP:8080 -sTCP:LISTENto find the PID, thenkillit the same way.
How do you optimize Python scripts for handling large, continuous streams of JSON data?
How do you optimize Python scripts for handling large, continuous streams of JSON data?
Approach
- Profile before optimizing: measure records per second and peak RSS, then use
cProfileorpy-spyto see whether time goes to reading, JSON decoding, per-record logic or writes. Streams in Python are usually bound by decoding and per-record interpreter overhead, not by I/O. - Never
json.loadthe whole input: use JSON Lines and iterate line by line through generators, so memory stays flat however long the stream runs. For one huge top-level array use an incremental parser such asijson; for concatenated objects without newlines, loop overJSONDecoder.raw_decode. - Swap the decoder:
orjson.loadsis typically several times faster than the standardjsonmodule and takes bytes directly;msgspecdecodes straight into typed structs and validates in the same pass. Read fromsys.stdin.bufferor binary files so you skip a separate decode tostr. - Cut per-record work: pull only the fields you need, avoid per-record logging, regexes and object churn, and batch outputs, writing to the database or network in chunks of hundreds to thousands per transaction or request instead of one call per record.
- Decoding is CPU-bound, so threads don't help under the standard GIL build: give
multiprocessingworkers batches of raw lines, decode and process there, and return only small results, since pickling parsed dicts back costs about as much as parsing. For network waits, useasyncio. - Make it robust for a stream that never ends: catch decode errors per line and send the raw line to a dead-letter file, checkpoint offsets so a restart resumes rather than reprocesses, and use bounded buffers so a slow sink pushes back instead of growing memory.
Follow-up
- How do you prove an optimization helped? Replay a fixed sample of the stream and compare records per second and peak memory (
tracemallocor RSS) before and after, over several runs rather than one timing. - One machine is no longer enough; how do you scale? Partition the stream (e.g. Kafka topic partitions) and run one consumer process per partition, which also preserves ordering per key.
- How do you handle a record that is valid JSON but the wrong shape? Validate against a schema (
msgspecstructs orpydanticmodels) and route failures to the dead-letter path with the reason, so a bad producer shows up in metrics.
Explain how you would deploy and configure an on-premise monolithic application securely.
Explain how you would deploy and configure an on-premise monolithic application securely.
Approach
- Interpretation: 'on-premise' means the app runs in a data center that you or a client operate, often with limited or no internet, so the deployment must be self-contained, least-privilege and verifiable by whoever runs the host. Assume one Linux host with the app, database and reverse proxy.
- Harden the host and process: a minimal, patched OS, a dedicated non-login service user, and a systemd unit with
NoNewPrivileges=yes,ProtectSystem=strict,PrivateTmp=yesand explicitReadWritePaths=. Never run as root; let the proxy own ports 80/443 or grantCAP_NET_BIND_SERVICE. - Shrink the network surface: the app listens on
127.0.0.1or an internal interface; a reverse proxy such as nginx terminates TLS 1.2+ with the organization's certificate; the database accepts connections only from the app host; the host firewall allows 443 plus SSH from a management network. - Secrets and config: keep them in a file owned by the service user with
0600permissions, or in systemd credentials or a vault, never in the shipped artifact, git or command-line arguments visible inps. Give the app a database account with only the privileges it needs, not superuser. - Supply chain and upgrades: ship signed packages whose checksums the installer verifies, pin dependencies, and make installs idempotent with Ansible or the OS package manager. An upgrade backs up the database, runs migrations and keeps the previous release so rollback is one command.
- Operate it: structured logs with rotation, forwarded to the operator's SIEM; audit logs for admin actions; a health endpoint for monitoring; and encrypted backups with a restore that has actually been tested. Ship a hardening checklist the operator can verify line by line.
Follow-up
- The organization requires certificates from its internal CA; what changes? Install the CA-issued certificate on the proxy, trust that CA for outbound calls, and document renewal, since a public ACME CA can't issue under a private root.
- How would the monolith scale if one box isn't enough? Run several stateless instances behind the proxy or a load balancer, and move sessions and uploads off local disk into the database or shared storage.
- How do you verify the deployment is actually hardened? Run a CIS benchmark scan, an external port scan and a TLS configuration check, and confirm the process user and file permissions match the checklist.
How do you manage environment variables and secrets safely across both development and production cloud enviro
How do you manage environment variables and secrets safely across both development and production cloud environments?
Approach
- Split configuration from secrets: non-sensitive settings (log level, feature flags, URLs) can live in env vars or committed per-environment config; secrets (DB passwords, API keys, signing keys) belong in a secrets manager such as AWS Secrets Manager or Vault, encrypted at rest and access-audited.
- Development: a git-ignored
.envfile loaded by the app, with a committed.env.examplelisting every variable and no values. Developers use their own low-privilege dev credentials, never production secrets, and pre-commit plus CI secret scanning (e.g. gitleaks) catches slips. - Production: the workload authenticates by identity (an IAM role, a Kubernetes service account, a managed identity) and fetches secrets at startup or reads them from a mounted volume. No long-lived static keys on hosts, and CI deploys with OIDC federation to get short-lived credentials.
- Know the env-var tradeoff: env vars are simple and portable, but child processes inherit them, the same user can read
/proc/<pid>/environ, and crash reporters or a debug dump ofos.environleak them. For high-value secrets prefer files with tight permissions or fetching at runtime. - Separate environments hard: different cloud accounts or projects for dev, staging and prod, distinct secrets per environment, and IAM policies so dev workloads can't read prod secret paths. Load and validate all config at startup against a typed schema and fail fast on anything missing.
- Plan for rotation and leaks: rotate on a schedule and whenever someone with access leaves, log every secret read and alert on unusual access. If a secret lands in git, rotate it first; rewriting history doesn't un-leak a value that was already pushed.
Follow-up
- Are Kubernetes Secrets secure by default? They are only base64-encoded; enable encryption at rest for etcd, restrict RBAC
get/liston secrets, or sync them from an external secrets manager. - How do you rotate a database password with zero downtime? Keep two valid credentials during the switch: create the new one, roll out apps that read it, verify, then revoke the old one.
- How does an app pick up a rotated secret without restarting? Re-read the mounted file or re-fetch from the manager on a timer or after an auth failure, caching the value with a short TTL.
Listing latency scales with page size, not with filters
The tenant listing endpoint reads resource filtered by tenant_id and status, ordered by updated_at DESC, and returns each row plus the owner's display name from app_user and the actor of that resource's latest resource_revision. p99 is 55 ms at 10 rows per page and 1.4 s at 200. Database telemetry shows 401 statements per request, each under 1 ms, and nothing in the slow-query log. Diagnose the cause and give the fix, stating the statement count per request and the p99 you expect afterwards.
Approach
- Read the counters before forming a theory. 401 statements for 200 rows is one driver query plus two per row, and sub-millisecond execution with an empty slow-query log rules out a bad plan. The time is round trips, which is why it is invisible in every per-query metric and scales with rows returned rather than with filter selectivity.
- Name the two per-row statements from their normalised text: a single-row app_user lookup by user_id, and a resource_revision lookup by resource_id ordered by version DESC LIMIT 1. Confirm by dropping those two response fields and watching the statement count fall to one. That locates the calls in the serialisation layer, not the repository.
- Check that the arithmetic accounts for the whole gap. Measure one round trip to the replica in isolation; 400 trips at roughly 3 ms of network plus 0.2 ms of execution is about 1.3 s on top of a 55 ms baseline, which matches. If the multiplication had fallen short, the N+1 would only be part of the story and you would keep looking.
- Batch both lookups. Collect owner_user_ids and resource_ids from the driver query, then issue WHERE tenant_id = $1 AND user_id = ANY($2) for the users, and PostgreSQL's SELECT DISTINCT ON (resource_id) ... WHERE resource_id = ANY($2) ORDER BY resource_id, version DESC for the latest revision, which the UNIQUE (resource_id, version) index serves directly. On an engine without DISTINCT ON, use a lateral join or a row_number window. Three statements per request at any page size.
Follow-up
- The page size is capped at 200 today. What breaks first if it is raised to 2,000, and is it still this bug?
- How do you stop the next N+1 from reaching production, given that no individual query is slow and the endpoint's tests pass?
Built from the rounds and topics Teramind candidates report.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Map the Teramind loop
- Write out the reported sequence: HR Screening, Online Assessment, Technical Round, Final Stage.
- For each round, write one sentence on what it is judging, from the description above, and mark the one you are least ready for.
Deliverable: A one-page map of the 4 reported rounds, with the weakest marked.
02Work Python
- Spend the session on Python, which Teramind candidates report being tested on.
- Write one worked example in Python and time yourself on it.
Deliverable: One timed worked example in Python.
03Work Live Coding
- Spend the session on Live Coding, which Teramind candidates report being tested on.
- Write one worked example in Live Coding and time yourself on it.
Deliverable: One timed worked example in Live Coding.
04Work React
- Spend the session on React, which Teramind candidates report being tested on.
- Write one worked example in React and time yourself on it.
Deliverable: One timed worked example in React.
05Answer out loud: Data Structures & Computer Science Fundamentals
- Answer aloud, timed: Explain the difference between a stack and a queue, and describe a real-world scenario where you would use each.
- Answer aloud, timed: How does a Time-To-Live (TTL) cache work, and how would you structure its eviction policy?
Deliverable: Spoken answers to 2 reported Data Structures & Computer Science Fundamentals question(s), under time.
06Answer out loud: Full-Stack & Live Coding
- Answer aloud, timed: Implement a small end-to-end service with a Node.js backend and a React frontend that fetches, filters, and displays a list of active system processes.
- Answer aloud, timed: Write a program to download a CSV file from a provided Google Sheets URL, parse the data, and import it into a local database using your preferred language and tools.
Deliverable: Spoken answers to 2 reported Full-Stack & Live Coding question(s), under time.
07Answer out loud: Systems, Scripting & Environment
- Answer aloud, timed: What Linux commands would you use to find and terminate a process running on a specific port?
- Answer aloud, timed: How do you optimize Python scripts for handling large, continuous streams of JSON data?
Deliverable: Spoken answers to 2 reported Systems, Scripting & Environment question(s), under time.
Expand any day for tasks and deliverables. Your progress is saved on this device.
Behavioural rounds judge the decision you made and what it cost.
Estimate work you have never done and defend the range
You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.
Approach
- Decompose into independently deployable steps before estimating anything: add the column nullable, write both shapes, backfill in batches, verify, move reads, stop writing the old shape, drop it. That is four deploys spread over days, and the calendar estimate is dominated by them rather than by the loop's runtime.
- Do the arithmetic aloud for the part that has arithmetic in it: batch size times number of batches times per-batch duration, at a write rate the primary can absorb alongside roughly 1.2k writes per second of production traffic. The loop is throttled by replication lag and lock waits, not by how fast it can issue statements.
- Price the schema step by its lock rather than its statement duration. In PostgreSQL an ALTER TABLE taking ACCESS EXCLUSIVE waits for every open transaction on that table while later queries queue behind it, so a millisecond change issued during a thirty-second analytics query stalls that table for thirty seconds. Adding a nullable column with a non-volatile default avoids a rewrite from version 11; a new index wants CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block and leaves an invalid index behind if it fails.
- Express the answer as a range whose endpoints each trace to a stated assumption, then name the cheapest experiment that collapses it, which is almost always running one real batch against the real table and multiplying.
Follow-up
- How do you verify the backfill genuinely finished, given rows written by production traffic while it ran?
- Where does the backfill resume from after a worker is killed mid-batch, and what makes that resume point trustworthy?
Reverse your own decision and price the reversal
Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.
Approach
- State the original rationale without irony, in the version you would still defend given what was known then. If it is not defensible, the story is about carelessness rather than judgement, and a different example serves you better.
- Give the measurement that moved with a before and after: the p99 that did not improve, the cache hit rate that sat at 40%, the plan that flipped to a sequential scan once the table passed a size you can name.
- Cost the reversal in steps, not adjectives: expand-and-contract deploys, the dual-write window, the callers who had to be notified, the rows already written in the wrong shape that had to be backfilled or abandoned.
- Distinguish reversal from rewrite by naming what you kept. Most good reversals preserve the schema or the interface and undo one decision inside it, which is also why they were affordable.
Follow-up
- What in that decision was irreversible, and did you know it was irreversible when you made it?
- How did you tell the people who had already built on top of the original decision?
Tell callers you do not own that their integration breaks
A field in a write endpoint's response must change shape. You own the endpoint; you do not own the four internal callers or the outbound webhook consumers who read it. Describe a deprecation you were responsible for: what you shipped first, how you established who was actually reading the field, the window you gave and what set its length, what you did about the consumer who never moved, and how you decided removal was safe. Name the signal you used, not the announcement you sent.
Approach
- Establish the reader set empirically rather than from a wiki of owners: per-field usage counters keyed by principal, or access logs attributed to a consumer. State the blind spot of whichever you pick, since a consumer that reads the field only on a monthly job will not appear in a week of logs.
- Ship additive first. Populate the new field alongside the old one so no reader is forced to move, which is also what keeps a rolling deploy safe, because old and new instances answer the same requests at the same time and a rollback must still find the old shape present.
- Set the window from the slowest legitimate consumer's release cadence, not from your calendar, and decide separately what to do for a consumer with no release process at all, such as an external webhook endpoint you can only email.
- Convert silence into evidence before you rely on it: a short, low-traffic removal window that makes a still-dependent consumer fail visibly and loudly while you are watching, rather than at three in the morning after you have moved on.
Follow-up
- How would you detect a consumer that reads the field only during a monthly export?
- One caller refuses to move and has a commercial relationship behind it. What changes in your plan and what does not?
- 01
You are asked to estimate a change you have never attempted: add a column to a 100-million-row table, populate it, move reads across, and drop the old shape. Give a range with the assumptions that generate it, including batch size, the signal your backfill throttles on, and wall-clock hours, and name the three unknowns that would move the number most. Then describe a real estimate you gave under comparable ignorance: how you expressed its uncertainty, what you committed to, and how wrong you turned out to be.
- 02
Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.
- 03
A field in a write endpoint's response must change shape. You own the endpoint; you do not own the four internal callers or the outbound webhook consumers who read it. Describe a deprecation you were responsible for: what you shipped first, how you established who was actually reading the field, the window you gave and what set its length, what you did about the consumer who never moved, and how you decided removal was safe. Name the signal you used, not the announcement you sent.
What is the company culture like regarding remote work?
Teramind operates as a fully remote company, primarily hiring engineers on a B2B contractor basis. They employ a strict micromanagement and time-tracking paradigm, utilizing their own software to monitor keyboard and mouse activity. You are compensated strictly for tracked, active working hours.
Teramind Software Engineer candidate reports ↗How difficult are the technical interviews?
The technical interviews are of average difficulty but are highly practical. They focus heavily on your ability to implement working full-stack features, write clean code under time pressure, and demonstrate core computer science definitions.
Teramind Software Engineer candidate reports ↗Are AI tools allowed during the live coding sessions?
Yes. In many technical rounds, candidates are explicitly encouraged to use AI tools like ChatGPT to assist with coding speed. The interviewers evaluate how effectively you integrate these tools to solve the problem at hand.
Teramind Software Engineer candidate reports ↗How quickly does Teramind make hiring decisions?
The process is typically very fast, often concluding within 1 to 2 weeks from the initial application. However, because they use strict elimination filters, you can expect to be rejected rapidly if you do not meet their specific technical or operational criteria.
Teramind Software Engineer candidate reports ↗How hard is the Teramind interview?
Candidates most commonly rate Teramind interviews as medium, based on 36 reported interviews. About 25% of candidates who interview go on to receive an offer.
Teramind Software Engineer candidate reports ↗What topics does Teramind test in interviews?
Teramind interviews most often cover Full-Stack Development, React, Python, Marketing Analytics, and Sales Metrics (Quota & Attainment). The exact emphasis depends on the specific role you apply for.
Teramind Software Engineer candidate reports ↗Where is Teramind headquartered?
Teramind is headquartered in Cheyenne, US.
Teramind Software Engineer candidate reports ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01Teramind Software Engineer candidate reports ↗
Company-reported rounds, questions and FAQ.
candidate · Accessed 2026-09-22 - 02PracHub Software Engineer practice ↗
PracHub practice material, not company-reported.
platform · Accessed 2026-09-22 - 03PracHub preparation framework ↗
PracHub preparation guidance.
platform · Accessed 2026-09-22