Skip to content

Top 30 Elixir Interview Questions and Answers [2026]

September 24, 2026 14 min read
Elixir interview guide by Second Talent
TL;DR: Elixir interviews in 2026 test OTP supervision, Phoenix contexts and scopes, LiveView's process model and Ecto changesets. Candidates should know that Elixir 1.20 now infers types across the whole language without annotations, and that Phoenix 1.8 made scoped data access and magic-link login the generator defaults.

Elixir 1.20, released on June 3, 2026, type-checks every language construct and infers types from guards and function bodies, with no type signatures required.

For an interviewer, that changes the question from "do you write specs?" to "do you read the compiler's warnings?". The OTP, Phoenix and Ecto fundamentals below have not changed, and they still decide most interviews.

Key takeaways
  1. 1Current versions as of September 2026: Elixir 1.20.4, Phoenix 1.8.14, LiveView 1.2.12, Ecto 3.14.2 and Erlang/OTP 29.1.
  2. 2Elixir 1.19 cut compile times in large codebases by up to 4 times, mainly by no longer loading every module right after it compiles.
  3. 3LiveView 1.1 made change tracking inside for comprehensions the default, which changes how large lists re-render.
  4. 4Ecto 3.13 added Repo.transact/2, a simpler alternative to Ecto.Multi for most multi-step writes.

Elixir Core and OTP

1. Explain the supervision strategies in OTP.

A supervisor's strategy decides which children restart when one child crashes. There are three:

  • :one_for_one: only the crashed child restarts. Use it when children are independent. This is the default.
  • :one_for_all: every child is terminated and restarted. Use it when children depend on each other's state and cannot run alone.
  • :rest_for_one: the crashed child and every child started after it restart. Use it when later children depend on earlier ones, such as a cache that depends on a connection process started before it.
Grid of which children restart when child B crashes: one_for_one restarts only B; one_for_all restarts A, B and C; rest_for_one restarts B and C while A keeps running.

Start order therefore matters for :rest_for_one. A strong candidate also mentions max_restarts and max_seconds (3 restarts in 5 seconds by default).

If children crash faster than that, the supervisor itself exits and the failure moves up the tree. See the Supervisor documentation.

2. What is the difference between a GenServer, an Agent and a Task?

All three are processes; they differ in what they are for.

  • GenServer: a long-running process with state and a message protocol. Clients use call for a synchronous reply and cast for fire-and-forget. It is the building block for most stateful services.
  • Agent: a GenServer reduced to holding state, with Agent.get/2 and Agent.update/2. Good for simple shared state; a GenServer once the state needs its own logic.
  • Task: a process that runs one computation and exits. Task.async/1 plus Task.await/2 returns a result; Task.Supervisor.start_child/2 runs work under supervision without linking it to the caller.

For parallel work over a collection, Task.async_stream/3 adds a concurrency limit and timeouts, which a hand-rolled list of Task.async calls lacks.

3. How does the BEAM scheduler work, and why does it matter?

The BEAM runs one scheduler thread per CPU core by default, and each scheduler preempts processes after a fixed budget of reductions, roughly a count of function calls.

No process can hold a scheduler indefinitely, so one slow request cannot freeze the others.

This is why an Elixir service keeps steady latency under load where a cooperative event loop would stall. The exception is native code: a long-running NIF blocks its scheduler, which is why heavy NIFs should run on dirty schedulers.

4. How is "let it crash" different from try/catch error handling?

Instead of anticipating every error inside a function, Elixir code handles the errors it expects and lets unexpected ones crash the process. A supervisor then restarts that process in a known good state.

This works because processes share no memory: a crash cannot corrupt another process's state. It does not mean ignoring errors.

Expected failures, such as a user submitting invalid input, are still returned as {:error, reason} tuples and handled with with or case.

5. What are macros, and when should you write one?

A macro receives code as a quoted AST at compile time and returns new AST that replaces the call. Functions receive values at runtime.

Macros power much of the ecosystem: Ecto's query syntax, Phoenix's router and ExUnit's test all compile to ordinary function calls.

The rule most teams follow is to write a macro only when a function cannot do the job, because macros are harder to read, debug and type-check.

defmacro unless_nil(value, do: block) do
  quote do
    if unquote(value) != nil, do: unquote(block)
  end
end

6. What is a protocol?

A protocol defines a function whose implementation is chosen by the data type of its first argument. It is Elixir's tool for polymorphism across types you do not own.

Enumerable, String.Chars and Jason.Encoder are protocols. Implementing Jason.Encoder for a struct makes it encodable to JSON without changing Jason.

Since Elixir 1.19, the compiler also type-checks protocol dispatch, so passing a type with no implementation shows up as a warning at compile time.

7. How do you test a GenServer?

Start it with start_supervised!/1, which stops it after each test and keeps tests isolated. Then test it through its public API, not by sending raw messages.

test "increments the counter" do
  pid = start_supervised!({Counter, 0})
  assert Counter.increment(pid) == 1
end

Keep the business logic in pure functions that the GenServer calls, and most tests will not need a process at all.

Phoenix Framework

8. What is a Plug, and how does the Plug pipeline work?

A plug is a module with init/1 and call/2, or a function, that takes a %Plug.Conn{} and returns a changed one. A Phoenix request passes through a chain of plugs: the endpoint's plugs first, then the router's pipeline, then the controller.

Any plug can stop the chain with halt/1, which is how authentication plugs reject a request before it reaches the controller.

9. What is the difference between the Endpoint and the Router?

The Endpoint is the entry point for every request. It runs plugs that apply to the whole application, such as static files, request logging, body parsing and sessions, then hands the request to the Router.

The Router matches the path and HTTP method to a controller action or LiveView, and applies pipelines (for example :browser or :api) to groups of routes.

10. What are Phoenix contexts for?

A context is a plain module that groups the functions for one area of the business, such as Accounts or Billing, and hides Ecto and other details from the web layer.

Controllers and LiveViews call Accounts.register_user/1, never Repo directly.

The payoff is a boundary: the web layer can change without touching the business logic, and each context can be tested without HTTP. See the Contexts guide.

11. How are Phoenix Channels structured?

A client opens one socket, over WebSocket or the LongPoll fallback, and joins any number of topics on it, such as "room:42".

Each joined topic runs as its own channel process on the server, with join/3, handle_in/3 for client messages and broadcast/3 to push to every subscriber.

Because every channel is a separate process, one crashed channel does not drop the socket's other topics.

12. What does Phoenix.PubSub do?

PubSub delivers messages to every process subscribed to a topic, on every node in the cluster. Channels and LiveView both use it: a LiveView subscribes in mount/3 and receives broadcasts in handle_info/2.

The default adapter uses Erlang's :pg process groups, so a cluster needs no Redis or other broker for PubSub to work across nodes.

Phoenix LiveView

13. Walk through the LiveView lifecycle.

A LiveView renders twice before it is interactive. The first request is plain HTTP: mount/3, handle_params/3 and render/1 return complete HTML, which gives a fast first paint and a page search engines can read.

The browser then connects over a WebSocket, a stateful LiveView process starts, and mount/3 runs again.

Five steps of a LiveView request: HTTP GET renders full HTML, the socket connects, mount runs again with connected? true, events and messages arrive, and only changed parts are sent.

Work that only makes sense for a live connection, such as PubSub subscriptions or timers, belongs behind if connected?(socket). Doing it unconditionally runs it twice. See the Phoenix.LiveView docs.

14. When do you use handle_event/3, and when handle_info/2?

handle_event/3 receives events from the browser, sent by bindings such as phx-click or phx-submit. handle_info/2 receives Elixir messages sent to the LiveView process: PubSub broadcasts, Process.send_after/3 timers, or results from a Task.

A common pattern uses both: handle_event starts slow work with start_async/3, and the result arrives in handle_async/3 without blocking the page.

15. How do you render a very large or infinite list in LiveView?

With streams. stream/3 sends items to the client and then drops them from the server's memory, so a feed of 10,000 rows does not keep 10,000 rows in every connected process.

Items are added or removed with stream_insert/3 and stream_delete/3, and the container needs phx-update="stream".

Plain assigns are fine for short lists. The trade-off is that the server no longer holds streamed items, so filtering or sorting them means resetting the stream.

16. What is the difference between a function component and a LiveComponent?

A function component is a function that takes assigns and returns HEEx. It has no state and no process, and it should be the default.

A LiveComponent has its own state and event handlers inside the parent LiveView's process. Use one only when a piece of UI needs state the parent should not manage, such as a self-contained form wizard.

Function component
  • A function: assigns in, HEEx out
  • No state, no callbacks
  • The default for buttons, cards, tables
LiveComponent
  • Own state and handle_event/3
  • Runs in the parent's process, so no extra concurrency
  • For self-contained stateful UI

Ecto and Data

17. What is an Ecto changeset?

A changeset records the changes to apply to a struct and whether they are valid. cast/4 filters and converts incoming params, validate_* functions check them in memory, and *_constraint functions turn database constraint errors into changeset errors instead of exceptions.

def changeset(user, attrs) do
  user
  |> cast(attrs, [:email, :name])
  |> validate_required([:email])
  |> unique_constraint(:email)
end

unique_constraint/2 still needs a unique index in the database. Validation in memory cannot prevent two concurrent inserts of the same email.

18. How does preload prevent N+1 queries?

Repo.preload(posts, :comments) loads the comments for every post in one extra query using WHERE post_id IN (...), instead of one query per post.

Ecto also never loads associations lazily, so an N+1 cannot happen by accident: an unloaded association raises when accessed.

A preload inside a query with a join, preload: [comments: c], loads everything in a single query. It is faster for small result sets but duplicates parent rows, so separate-query preloads are usually better for large ones.

19. When would you use Ecto.Multi instead of Repo.transact/2?

Both run several operations in one database transaction. Repo.transact/2, added in Ecto 3.13, takes a function and commits if it returns {:ok, value}, which reads like ordinary code and suits most cases.

Ecto.Multi builds the steps as data first. It is worth it in three cases: the list of operations is built dynamically, you want to test the steps before running them, or the error must name the exact step that failed.

20. How do you model a many-to-many relationship with extra fields on the join table?

Give the join table its own schema, such as Membership with user_id, team_id and role. Then declare has_many :memberships on both sides, plus has_many :teams, through: [:memberships, :team] where you need direct access.

many_to_many with a table name works only when the join table has no data of its own.

21. What is the Ecto SQL Sandbox?

The sandbox wraps each test in a database transaction that is rolled back at the end, so tests can run in parallel with async: true without seeing each other's data.

Processes started by a test, such as a Task or a LiveView, also need the test's connection. Grant it with Ecto.Adapters.SQL.Sandbox.allow/3, or start the process as a child of the test, which grants it automatically.

Configuration and Production

22. How does configuration work across environments?

config/config.exs and the environment files are evaluated at compile time, so they must not read secrets. config/runtime.exs is evaluated when the application boots, including inside a release, and is where environment variables such as DATABASE_URL belong.

The common production bug. A value read with System.get_env/1 in config.exs or prod.exs is baked into the build, so the release keeps the value from the build machine. Read it in runtime.exs instead.

23. What is a release, and why deploy with one?

mix release builds a self-contained directory with compiled code, dependencies and the Erlang runtime. The server does not need Elixir or Mix installed.

A release also ships scripts to start, stop and open a remote shell (bin/app remote) and runs runtime.exs at boot. Phoenix's mix phx.gen.release adds a module for running migrations inside the release, and --docker adds a Dockerfile.

24. How would you find what is slowing down a running Elixir node?

Connect to the node with a remote shell and inspect it live, which few runtimes allow. :observer or the LiveDashboard show process counts, memory and message queue lengths.

A process with a growing message queue is the usual culprit, because it receives work faster than it handles it.

:recon and Process.info/2 find the processes using the most memory or reductions, and telemetry events from Phoenix and Ecto show which requests and queries are slow.

What Changed Recently

Jun 18, 2025
Ecto 3.13 adds Repo.transact/2
Jul 30, 2025
LiveView 1.1: colocated hooks, keyed comprehensions, portals
Aug 5, 2025
Phoenix 1.8: scopes, magic-link auth, daisyUI
Oct 16, 2025
Elixir 1.19: typed protocols and anonymous functions, faster builds
May 13, 2026
Erlang/OTP 29.0
Jun 3, 2026
Elixir 1.20: type inference across the whole language

25. What does the type system in Elixir 1.20 check, and what does it not?

According to the 1.20 release announcement, the compiler now understands every language construct and infers types from guards and function bodies, using type information from the standard library and dependencies.

It reports "verified bugs and dead code": code that will fail for every input of the inferred type.

def label(user) when not is_map_key(user, :name) do
  user.name  # typing violation: the guard proves :name is absent
end

What it does not do yet is accept type signatures written by developers; @spec is still read only by Dialyzer. The system is gradual, so code the compiler cannot prove wrong compiles as before.

26. What did Elixir 1.19 change for large codebases?

Two things. It extended type checking to protocol dispatch and anonymous functions. It also made compilation up to 4 times faster in large codebases.

Most of the gain comes from no longer loading each module right after compiling it; the rest from parallel dependency builds through MIX_OS_DEPS_COMPILE_PARTITION_COUNT.

The lazy loading can surface code that called a module at compile time without declaring the dependency, so an upgrade may need require or Code.ensure_compiled!/1 in a few places.

27. Which Elixir and OTP versions should a new project target in 2026?

Elixir 1.20 on Erlang/OTP 28 or 29. Elixir 1.20 requires OTP 27 or later and is compatible with OTP 29, released on May 13, 2026.

Per Elixir's support policy, only the latest minor branch gets bug fixes, and security patches cover the last five (1.16 to 1.20 today). A service on 1.15 or older no longer gets security fixes.

28. What are scopes in Phoenix 1.8?

A scope is a struct, generated as %Scope{}, that carries who is making a request, usually the current user and optionally their organization.

Phoenix 1.8 generators pass it as the first argument to every context function, as in Blog.list_posts(scope), and generate queries filtered by it.

The release announcement describes the aim as making "secure data access the default". A generated context cannot accidentally list another tenant's records, which was one of the most common authorization bugs in hand-written Phoenix apps.

29. How does authentication generated by phx.gen.auth work in Phoenix 1.8?

New apps log users in with magic links by default: the user enters an email address and receives a one-time login link, with no password to set. Password login is still available and can be enabled from the user settings page.

A candidate should know the security details the generator handles: tokens are stored hashed, expire, and are single-use, and sessions are reissued on login to prevent session fixation.

30. What changed in LiveView 1.1?

The LiveView 1.1 announcement lists four headline changes:

  • Colocated hooks: a component's JavaScript hook lives in the same file as its HEEx.
  • Keyed comprehensions: change tracking inside for is on by default, with an optional :key attribute.
  • Portals: <.portal> renders elements such as modals elsewhere in the DOM.
  • TypeScript: type declarations ship for the whole JavaScript API.

The comprehension change matters most in interviews. A list that used to re-send every item on each change now sends only the changed items, so plain assigns now handle medium-sized lists that once needed streams.

Signs of a Strong Answer

  • They say which strategy they would pick for a real tree and why, rather than reciting the three definitions.
  • They put PubSub subscriptions behind connected?(socket) without being asked, and can say why.
  • They reach for pure functions first and processes only for state, concurrency or isolation.
  • They know the database, not the changeset, enforces uniqueness.
  • They name a tool for inspecting a live node (:observer, LiveDashboard, :recon) and have used it.
  • They can say what the 1.19 and 1.20 type checks caught in their own code, or why a warning was a false alarm.

Hiring Elixir Developers

Elixir developers are a small pool, and many strong ones work remotely from Asia. Second Talent matches companies with pre-vetted back-end engineers, including Elixir and Phoenix specialists, screened with questions like these.

Tell us the stack and we send a shortlist within 24 hours. Start hiring, or see our event-driven architecture and PostgreSQL interview guides.

Hiring developers in Southeast Asia?

Get Cost Guide

How would you like to talk?

WhatsApp us Prefer texting at your own pace? Just hit us up on WhatsApp. We promise no spam and a hassle-free experience.

Loading available times…